feat: add repo planning/binary repo support and Meson build integration

- add repos.toml source/binary repo configuration with arch overrides and priorities
- add dependency planner/execution plan flow with interactive provider selection and --yes support
- expand binary repo DB metadata (sha512, dependencies, file lists) and repo owns/search helpers
- add rootfs-scoped fd locking and minisign detached signing/verification helpers
- add Meson build/test support for the project with builddir-local CARGO_HOME/CARGO_TARGET_DIR
- install contrib example configs into sysconfdir (/etc/depot.d layout) via Meson
- improve builder env handling (sanitized PATH, tool env passthrough, CXXFLAGS support)
- support += appends in package specs and add build.flags.cxxflags parsing
- prompt for git credentials when private repo auth is required
- update docs/examples and dependency setup (system libs, zstdmt, tempfile pin)
This commit is contained in:
2026-02-24 21:38:12 -06:00
parent d63ad03e98
commit 9953d4b2ee
30 changed files with 4536 additions and 111 deletions
+331 -14
View File
@@ -1,10 +1,203 @@
//! Global configuration for Depot
use anyhow::{Context, Result};
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::{Path, PathBuf};
fn default_true() -> bool {
true
}
fn default_repo_db_filename() -> String {
"repo.db.zst".to_string()
}
fn resolve_rootfs_base(rootfs: &Path) -> PathBuf {
if rootfs.exists() {
rootfs.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.map(|cwd| cwd.join(rootfs))
.unwrap_or_else(|_| rootfs.to_path_buf())
})
} else {
std::env::current_dir()
.map(|cwd| cwd.join(rootfs))
.unwrap_or_else(|_| rootfs.to_path_buf())
}
}
/// Global repo behavior settings loaded from `repos.toml`.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RepoSettings {
/// Prefer binary repos over source repos when both can satisfy a request.
#[serde(default)]
pub prefer_binary: bool,
}
impl Default for RepoSettings {
fn default() -> Self {
Self {
prefer_binary: false,
}
}
}
/// Source repository configuration entry loaded from `repos.toml`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceRepo {
/// Git URL for the package repo.
pub url: String,
/// Whether the repo is enabled for lookups/sync.
#[serde(default = "default_true")]
pub enabled: bool,
/// Lower numbers are higher priority.
#[serde(default)]
pub priority: i32,
/// Optional subdirectories to scan/index inside the git checkout.
#[serde(default)]
pub subdirs: Vec<String>,
}
/// Binary repository configuration entry loaded from `repos.toml`.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BinaryRepoArchEntry {
/// Whether this architecture is enabled for this repo.
#[serde(default = "default_true")]
pub enabled: bool,
/// Optional URL override for this architecture.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Optional repo DB path override for this architecture.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo_db: Option<String>,
}
/// Binary repository configuration entry loaded from `repos.toml`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BinaryRepo {
/// Base URL for the binary repository.
pub url: String,
/// Whether the repo is enabled for lookups.
#[serde(default = "default_true")]
pub enabled: bool,
/// Lower numbers are higher priority.
#[serde(default)]
pub priority: i32,
/// Repo database filename/path relative to `url`.
#[serde(default = "default_repo_db_filename")]
pub repo_db: String,
/// Architecture-specific overrides/enablement.
///
/// Example:
/// `[binary.core.arch.x86_64]`
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub arch: BTreeMap<String, BinaryRepoArchEntry>,
/// Allow unsigned repo metadata for this repo.
#[serde(default)]
pub allow_unsigned: bool,
}
impl Default for BinaryRepo {
fn default() -> Self {
Self {
url: String::new(),
enabled: true,
priority: 0,
repo_db: default_repo_db_filename(),
arch: BTreeMap::new(),
allow_unsigned: false,
}
}
}
impl BinaryRepo {
/// Return true if this repo is enabled for the requested machine architecture.
pub fn supports_arch(&self, machine_arch: &str) -> bool {
if self.arch.is_empty() {
return true;
}
self.arch
.get(machine_arch)
.map(|entry| entry.enabled)
.unwrap_or(false)
}
/// Return the effective base URL for `machine_arch`, including arch overrides.
pub fn effective_url_for_arch<'a>(&'a self, machine_arch: &str) -> Option<&'a str> {
if self.arch.is_empty() {
return Some(self.url.as_str());
}
let entry = self.arch.get(machine_arch)?;
if !entry.enabled {
return None;
}
Some(entry.url.as_deref().unwrap_or(self.url.as_str()))
}
/// Return the effective repo DB path for `machine_arch`, including arch overrides.
pub fn effective_repo_db_for_arch<'a>(&'a self, machine_arch: &str) -> Option<&'a str> {
if self.arch.is_empty() {
return Some(self.repo_db.as_str());
}
let entry = self.arch.get(machine_arch)?;
if !entry.enabled {
return None;
}
Some(entry.repo_db.as_deref().unwrap_or(self.repo_db.as_str()))
}
}
/// Parsed contents of `/etc/depot.d/repos.toml`.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RepoConfigFile {
/// Global repo behavior settings.
#[serde(default, skip_serializing_if = "repo_settings_is_default")]
pub settings: RepoSettings,
/// Source repos keyed by repo name.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub source: BTreeMap<String, SourceRepo>,
/// Binary repos keyed by repo name.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub binary: BTreeMap<String, BinaryRepo>,
}
fn repo_settings_is_default(settings: &RepoSettings) -> bool {
!settings.prefer_binary
}
/// Return the canonical `repos.toml` path for the given rootfs.
pub fn repos_toml_path(rootfs: &Path) -> PathBuf {
resolve_rootfs_base(rootfs).join("etc/depot.d/repos.toml")
}
/// Load `repos.toml` for the given rootfs. Missing file returns defaults.
pub fn load_repos_config_file(rootfs: &Path) -> Result<RepoConfigFile> {
let path = repos_toml_path(rootfs);
if !path.exists() {
return Ok(RepoConfigFile::default());
}
let content =
fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
let parsed: RepoConfigFile =
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(parsed)
}
/// Save `repos.toml` for the given rootfs, creating `/etc/depot.d` as needed.
pub fn save_repos_config_file(rootfs: &Path, repos: &RepoConfigFile) -> Result<PathBuf> {
let path = repos_toml_path(rootfs);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
let content = toml::to_string_pretty(repos).context("Failed to serialize repos.toml")?;
fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(path)
}
/// Global configuration settings
#[derive(Clone)]
pub struct Config {
@@ -20,43 +213,43 @@ pub struct Config {
pub package_overrides: toml::Value,
/// Appends found in system TOML files (key -> values to append)
pub appends: HashMap<String, Vec<toml::Value>>,
/// Parsed repo settings from `/etc/depot.d/repos.toml`.
pub repo_settings: RepoSettings,
/// Source repos from `/etc/depot.d/repos.toml` (and legacy mirrors fallback).
pub source_repos: BTreeMap<String, SourceRepo>,
/// Binary repos from `/etc/depot.d/repos.toml`.
pub binary_repos: BTreeMap<String, BinaryRepo>,
/// Mirrors mapping read from /etc/depot.d/mirrors.toml (reponame -> git url)
pub mirrors: std::collections::HashMap<String, String>,
/// Directory where git mirrors are cloned (absolute path). Defaults to
/// <rootfs>/usr/src/depot unless overridden in depot.toml
pub repo_clone_dir: PathBuf,
/// Cache directory for binary packages and repo metadata.
pub package_cache_dir: PathBuf,
}
impl Config {
/// Create config with paths relative to the given rootfs
pub fn for_rootfs(rootfs: &Path) -> Self {
let abs_rootfs = if rootfs.exists() {
rootfs.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.map(|cwd| cwd.join(rootfs))
.unwrap_or_else(|_| rootfs.to_path_buf())
})
} else {
std::env::current_dir()
.map(|cwd| cwd.join(rootfs))
.unwrap_or_else(|_| rootfs.to_path_buf())
};
let abs_rootfs = resolve_rootfs_base(rootfs);
let is_system_root = abs_rootfs == Path::new("/") || abs_rootfs.as_os_str() == "/";
let is_root = crate::fakeroot::is_root();
let (cache_dir, build_dir, db_dir) = if is_system_root && !is_root {
let (cache_dir, package_cache_dir, build_dir, db_dir) = if is_system_root && !is_root {
let home = std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"));
(
home.join(".cache/depot/sources"),
home.join(".cache/depot/packages"),
home.join(".cache/depot/build"),
home.join(".local/share/depot"),
)
} else {
(
abs_rootfs.join("var/cache/depot/sources"),
abs_rootfs.join("var/cache/depot/packages"),
abs_rootfs.join("var/cache/depot/build"),
abs_rootfs.join("var/lib/depot"),
)
@@ -69,8 +262,12 @@ impl Config {
build_overrides: toml::Value::Table(toml::map::Map::new()),
package_overrides: toml::Value::Table(toml::map::Map::new()),
appends: HashMap::new(),
repo_settings: RepoSettings::default(),
source_repos: BTreeMap::new(),
binary_repos: BTreeMap::new(),
mirrors: std::collections::HashMap::new(),
repo_clone_dir: abs_rootfs.join("usr/src/depot"),
package_cache_dir,
};
if let Err(e) = config.load_system(&abs_rootfs) {
@@ -185,7 +382,27 @@ impl Config {
}
}
// Load mirrors file: /etc/depot.d/mirrors.toml
// Load new repos file: /etc/depot.d/repos.toml (host fallback then rootfs override).
let host_repos_path = PathBuf::from("/etc/depot.d/repos.toml");
let repos_path = rootfs.join("etc/depot.d/repos.toml");
for path in [host_repos_path, repos_path] {
if !path.exists() {
continue;
}
let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read repos config: {}", path.display()))?;
let parsed: RepoConfigFile = toml::from_str(&content)
.with_context(|| format!("Failed to parse repos config: {}", path.display()))?;
self.repo_settings = parsed.settings;
for (name, repo) in parsed.source {
self.source_repos.insert(name, repo);
}
for (name, repo) in parsed.binary {
self.binary_repos.insert(name, repo);
}
}
// Load legacy mirrors file: /etc/depot.d/mirrors.toml
let mirrors_path = rootfs.join("etc/depot.d/mirrors.toml");
if mirrors_path.exists() {
let content = fs::read_to_string(&mirrors_path).with_context(|| {
@@ -196,6 +413,12 @@ impl Config {
for (k, v) in table {
if let Some(s) = v.as_str() {
self.mirrors.insert(k.clone(), s.to_string());
self.source_repos.entry(k.clone()).or_insert(SourceRepo {
url: s.to_string(),
enabled: true,
priority: 0,
subdirs: Vec::new(),
});
}
}
}
@@ -213,6 +436,17 @@ impl Config {
Ok(())
}
/// Return enabled source repos as `name -> git URL` pairs for legacy sync/status code.
pub fn enabled_source_mirror_map(&self) -> std::collections::HashMap<String, String> {
let mut out = std::collections::HashMap::new();
for (name, repo) in &self.source_repos {
if repo.enabled {
out.insert(name.clone(), repo.url.clone());
}
}
out
}
}
fn merge_toml_values(base: &mut toml::Value, over: &toml::Value) {
@@ -460,4 +694,87 @@ cflags = ["-O3"]
Some(1)
);
}
#[test]
fn test_load_repos_toml() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let etc = root.join("etc/depot.d");
fs::create_dir_all(&etc).unwrap();
fs::write(
etc.join("repos.toml"),
r#"
[settings]
prefer_binary = true
[source.vertex]
url = "https://gitlab.com/vertex-linux/packages.git"
enabled = true
priority = 10
subdirs = ["core", "extra"]
[binary.vertex]
url = "https://repo.example.invalid"
enabled = false
priority = 5
repo_db = "repo.db.zst"
[binary.vertex.arch.x86_64]
enabled = true
"#,
)
.unwrap();
let config = Config::for_rootfs(root);
assert!(config.repo_settings.prefer_binary);
let src = config.source_repos.get("vertex").unwrap();
assert_eq!(src.url, "https://gitlab.com/vertex-linux/packages.git");
assert_eq!(src.subdirs, vec!["core".to_string(), "extra".to_string()]);
let bin = config.binary_repos.get("vertex").unwrap();
assert_eq!(bin.url, "https://repo.example.invalid");
assert!(!bin.enabled);
assert!(bin.arch.contains_key("x86_64"));
assert!(bin.supports_arch("x86_64"));
assert!(!bin.supports_arch("aarch64"));
}
#[test]
fn test_save_and_load_repos_config_file_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mut repos = RepoConfigFile::default();
repos.source.insert(
"vertex".to_string(),
SourceRepo {
url: "https://gitlab.com/vertex-linux/packages.git".to_string(),
enabled: true,
priority: 1,
subdirs: vec!["core".to_string()],
},
);
repos.binary.insert(
"vertex".to_string(),
BinaryRepo {
url: "https://repo.example.invalid".to_string(),
enabled: true,
priority: 2,
repo_db: "repo.db.zst".to_string(),
arch: {
let mut map = BTreeMap::new();
map.insert("x86_64".to_string(), BinaryRepoArchEntry::default());
map
},
allow_unsigned: false,
},
);
let path = save_repos_config_file(root, &repos).unwrap();
assert!(path.exists());
let loaded = load_repos_config_file(root).unwrap();
assert!(loaded.source.contains_key("vertex"));
assert!(loaded.binary.contains_key("vertex"));
}
}