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:
+331
-14
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user