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
+4 -4
View File
@@ -60,7 +60,7 @@ pub fn build(
let mut configure_cmd = Command::new(&configure_path);
configure_cmd.current_dir(&build_dir);
crate::builder::prepare_command(&mut configure_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
configure_cmd.arg(format!("--prefix={}", flags.prefix));
@@ -143,7 +143,7 @@ pub fn build(
make_cmd.arg(target);
}
crate::builder::prepare_command(&mut make_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut make_cmd, &env_vars);
let status = make_cmd.status().with_context(|| {
format!("Failed to run {} in {}", make_exec, make_dir.display())
@@ -194,7 +194,7 @@ pub fn build(
for test_target in &test_targets {
test_cmd.arg(test_target);
}
crate::builder::prepare_command(&mut test_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let test_targets_display = test_targets.join(" ");
let status = test_cmd.status().with_context(|| {
@@ -305,7 +305,7 @@ pub fn build(
"DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(),
));
crate::builder::prepare_command(&mut install_cmd, &install_env);
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd.status().with_context(|| {
format!(
+4 -4
View File
@@ -82,7 +82,7 @@ pub fn build(
cmake_cmd.arg(&expanded);
}
crate::builder::prepare_command(&mut cmake_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars);
let status = cmake_cmd.status().context("Failed to run cmake")?;
if !status.success() {
@@ -109,7 +109,7 @@ pub fn build(
}
}
crate::builder::prepare_command(&mut build_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut build_cmd, &env_vars);
let status = build_cmd
.status()
@@ -131,7 +131,7 @@ pub fn build(
for target in &test_targets {
test_cmd.arg(target);
}
crate::builder::prepare_command(&mut test_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let status = test_cmd.status().with_context(|| {
format!(
@@ -182,7 +182,7 @@ pub fn build(
"DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(),
));
crate::builder::prepare_command(&mut install_cmd, &install_env);
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
+66 -7
View File
@@ -118,7 +118,7 @@ pub fn build(
};
cmd.current_dir(&build_dir);
crate::builder::prepare_command(&mut cmd, &env_vars);
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
// Run the command and include the OS error on spawn failures for clearer diagnostics
let status = cmd.status().map_err(|e| {
@@ -176,9 +176,16 @@ fn build_function_mode_command(
) -> Result<Command> {
let mut wrapper = String::new();
wrapper.push_str("set -eu\n");
wrapper.push_str("depot_has_function() {\n");
wrapper.push_str(" case \"$(type \"$1\" 2>/dev/null || :)\" in\n");
wrapper.push_str(" *function*) return 0 ;;\n");
wrapper.push_str(" *) return 1 ;;\n");
wrapper.push_str(" esac\n");
wrapper.push_str("}\n");
wrapper.push_str("depot_build_ran=0\n");
wrapper.push_str(". \"$1\"\n");
wrapper.push_str("if command -v depot_build >/dev/null 2>&1; then depot_build;\n");
wrapper.push_str("elif command -v build >/dev/null 2>&1; then build;\n");
wrapper.push_str("if depot_has_function depot_build; then depot_build; depot_build_ran=1;\n");
wrapper.push_str("elif depot_has_function build; then build; depot_build_ran=1;\n");
wrapper.push_str("fi\n");
let primary = &spec.package.name;
@@ -195,18 +202,28 @@ fn build_function_mode_command(
wrapper.push_str(&format!(
"DEPOT_OUTPUT_NAME='{q_name}'; DEPOT_OUTPUT_DESTDIR='{q_dest}'; DESTDIR=\"$DEPOT_OUTPUT_DESTDIR\"; export DEPOT_OUTPUT_NAME DEPOT_OUTPUT_DESTDIR DESTDIR\n"
));
wrapper.push_str("depot_output_installed=0\n");
wrapper.push_str(&format!(
"if command -v depot_install_{fn_suffix} >/dev/null 2>&1; then depot_install_{fn_suffix};\n"
"if depot_has_function depot_install_{fn_suffix}; then depot_install_{fn_suffix}; depot_output_installed=1;\n"
));
wrapper.push_str(&format!(
"elif command -v install_{fn_suffix} >/dev/null 2>&1; then install_{fn_suffix};\n"
"elif depot_has_function install_{fn_suffix}; then install_{fn_suffix}; depot_output_installed=1;\n"
));
if out.name == *primary {
wrapper
.push_str("elif command -v depot_install >/dev/null 2>&1; then depot_install;\n");
wrapper.push_str("elif command -v install >/dev/null 2>&1; then install;\n");
.push_str("elif depot_has_function depot_install; then depot_install; depot_output_installed=1;\n");
wrapper.push_str(
"elif depot_has_function install; then install; depot_output_installed=1;\n",
);
wrapper.push_str("elif [ \"$depot_build_ran\" = 1 ]; then depot_output_installed=1;\n");
}
wrapper.push_str("fi\n");
wrapper.push_str("if [ \"$depot_output_installed\" != 1 ]; then\n");
wrapper.push_str(
" echo \"depot: missing install handler for output '$DEPOT_OUTPUT_NAME' in custom function mode\" >&2\n",
);
wrapper.push_str(" exit 1\n");
wrapper.push_str("fi\n");
}
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
@@ -363,4 +380,46 @@ depot_install_dev_pkg() {
);
Ok(())
}
#[test]
fn test_build_function_mode_errors_when_output_handler_missing() -> Result<()> {
let tmp_src = tempdir()?;
let tmp_dest = tempdir()?;
let build_sh = tmp_src.path().join("build.sh");
std::fs::write(
&build_sh,
r#"#!/bin/sh
depot_build() {
:
}
depot_install() {
mkdir -p "$DESTDIR/usr/share"
echo primary > "$DESTDIR/usr/share/primary.txt"
}
"#,
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&build_sh)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&build_sh, perms)?;
}
let mut spec = mk_spec("demo", "1.0");
spec.packages.push(crate::package::PackageInfo {
name: "dev-pkg".into(),
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
});
let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true)
.expect_err("missing per-output install handler should fail");
assert!(err.to_string().contains("Custom build script failed"));
Ok(())
}
}
+2 -2
View File
@@ -43,7 +43,7 @@ pub fn build(
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(&cmd_str);
cmd.current_dir(src_dir);
crate::builder::prepare_command(&mut cmd, &env_vars);
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
let status = cmd
.status()
@@ -84,7 +84,7 @@ pub fn build(
"DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(),
));
crate::builder::prepare_command(&mut cmd, &install_env);
crate::builder::prepare_tool_command(&mut cmd, &install_env);
let status = cmd
.status()
+3 -3
View File
@@ -51,7 +51,7 @@ pub fn build(
meson_cmd.arg(arg);
}
crate::builder::prepare_command(&mut meson_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars);
let status = meson_cmd.status().context("Failed to run meson setup")?;
if !status.success() {
@@ -71,7 +71,7 @@ pub fn build(
ninja_cmd.current_dir(&build_dir);
ninja_cmd.arg("-j").arg(num_cpus().to_string());
crate::builder::prepare_command(&mut ninja_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut ninja_cmd, &env_vars);
let status = ninja_cmd
.status()
@@ -106,7 +106,7 @@ pub fn build(
"DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(),
));
crate::builder::prepare_command(&mut install_cmd, &install_env);
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
+103 -3
View File
@@ -13,8 +13,9 @@ pub mod state;
use crate::cross::CrossConfig;
use crate::package::{BuildType, PackageSpec};
use anyhow::Result;
use std::ffi::OsString;
use std::path::Path;
use std::process::Command;
use std::process::{Command, Stdio};
pub type EnvVars = Vec<(String, String)>;
@@ -41,6 +42,9 @@ pub fn standard_build_env(
if !flags.cflags.is_empty() {
set_env_var(&mut env_vars, "CFLAGS", flags.cflags.join(" "));
}
if !flags.cxxflags.is_empty() {
set_env_var(&mut env_vars, "CXXFLAGS", flags.cxxflags.join(" "));
}
let ldflags = if !flags.ldflags.is_empty() || !flags.libc.is_empty() {
if flags.libc.is_empty() {
@@ -123,8 +127,29 @@ pub fn standard_build_env(
/// Prepare a Command with a hermetic environment and some essential variables preserved.
pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
cmd.env_clear();
if let Some(path) = sanitized_build_path() {
cmd.env("PATH", path);
}
// Preserve essential environment variables
for var in &["PATH", "LANG", "HOME", "DESTDIR", "DEPOT_ROOTFS"] {
for var in &[
"LANG",
"HOME",
"DESTDIR",
"DEPOT_ROOTFS",
"CARGO_HOME",
"RUSTUP_HOME",
"RUSTUP_TOOLCHAIN",
"RUSTC",
"RUSTDOC",
"TERM",
"COLORTERM",
"NO_COLOR",
"CLICOLOR",
"CLICOLOR_FORCE",
"FORCE_COLOR",
] {
if let Ok(val) = std::env::var(var) {
cmd.env(var, val);
}
@@ -139,6 +164,35 @@ pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
}
}
fn sanitized_build_path() -> Option<OsString> {
use std::path::PathBuf;
let mut parts: Vec<PathBuf> = std::env::var_os("PATH")
.map(|raw| std::env::split_paths(&raw).collect())
.unwrap_or_default();
for dir in ["/bin", "/usr/bin", "/sbin", "/usr/sbin"] {
let path = PathBuf::from(dir);
if path.exists() && !parts.iter().any(|p| p == &path) {
parts.push(path);
}
}
if parts.is_empty() {
return None;
}
std::env::join_paths(parts).ok()
}
/// Prepare a Command for interactive tool execution with live terminal output.
pub fn prepare_tool_command(cmd: &mut Command, env_vars: &EnvVars) {
prepare_command(cmd, env_vars);
cmd.stdin(Stdio::inherit());
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
}
/// Build a package using the appropriate build system
pub fn build(
spec: &PackageSpec,
@@ -230,6 +284,8 @@ mod tests {
std::env::set_var("HOME", "/home/test");
std::env::set_var("SHELL", "/bin/zsh");
std::env::set_var("DEPOT_ROOTFS", "/my/rootfs");
std::env::set_var("TERM", "xterm-256color");
std::env::set_var("CLICOLOR_FORCE", "1");
}
prepare_command(&mut cmd, &vec![("MYVAR".to_string(), "myval".to_string())]);
@@ -251,6 +307,16 @@ mod tests {
envs.get(OsStr::new("DEPOT_ROOTFS")),
Some(&Some(std::ffi::OsString::from("/my/rootfs").as_os_str()))
);
assert_eq!(
envs.get(OsStr::new("TERM")),
Some(&Some(
std::ffi::OsString::from("xterm-256color").as_os_str()
))
);
assert_eq!(
envs.get(OsStr::new("CLICOLOR_FORCE")),
Some(&Some(std::ffi::OsString::from("1").as_os_str()))
);
}
#[test]
@@ -267,15 +333,45 @@ mod tests {
);
}
#[test]
fn test_prepare_command_preserves_rust_toolchain_homes() {
let mut cmd = std::process::Command::new("ls");
unsafe {
std::env::set_var("CARGO_HOME", "/var/cache/cargo-home");
std::env::set_var("RUSTUP_HOME", "/var/cache/rustup-home");
}
prepare_command(&mut cmd, &Vec::new());
let envs: HashMap<_, _> = cmd.get_envs().collect();
assert_eq!(
envs.get(OsStr::new("CARGO_HOME")),
Some(&Some(
std::ffi::OsString::from("/var/cache/cargo-home").as_os_str()
))
);
assert_eq!(
envs.get(OsStr::new("RUSTUP_HOME")),
Some(&Some(
std::ffi::OsString::from("/var/cache/rustup-home").as_os_str()
))
);
}
#[test]
fn test_standard_build_env_respects_export_compiler_flags_toggle() {
let spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into(), "-fno-exceptions".into()];
let enabled = standard_build_env(&spec, None, true, true);
assert!(
enabled.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
"expected CFLAGS to be exported when enabled"
);
assert!(
enabled
.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -fno-exceptions"),
"expected CXXFLAGS to be exported when enabled"
);
assert!(
enabled
.iter()
@@ -304,6 +400,10 @@ mod tests {
!disabled_env.iter().any(|(k, _)| k == "CFLAGS"),
"expected CFLAGS to be omitted when no_flags is set in spec"
);
assert!(
!disabled_env.iter().any(|(k, _)| k == "CXXFLAGS"),
"expected CXXFLAGS to be omitted when no_flags is set in spec"
);
assert!(
!disabled_env.iter().any(|(k, _)| k == "LDFLAGS"),
"expected LDFLAGS to be omitted when no_flags is set in spec"
+1 -1
View File
@@ -245,7 +245,7 @@ fn build_wheel_setup_py(
.arg("bdist_wheel")
.arg("--dist-dir")
.arg(dist_dir);
crate::builder::prepare_command(&mut cmd, &env_vars.to_vec());
crate::builder::prepare_tool_command(&mut cmd, &env_vars.to_vec());
let status = cmd
.status()
+1 -1
View File
@@ -99,7 +99,7 @@ pub fn build(
}
// Set environment
crate::builder::prepare_command(&mut cargo_cmd, &env_vars);
crate::builder::prepare_tool_command(&mut cargo_cmd, &env_vars);
let status = cargo_cmd
.status()