Compare commits

..

7 Commits

60 changed files with 19932 additions and 18986 deletions
Generated
+13 -13
View File
@@ -256,9 +256,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.1"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
dependencies = [
"clap_builder",
"clap_derive",
@@ -266,9 +266,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
@@ -481,7 +481,7 @@ checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
[[package]]
name = "depot"
version = "1.0.0"
version = "1.0.2"
dependencies = [
"anyhow",
"ar",
@@ -1388,9 +1388,9 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "lz4_flex"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226"
dependencies = [
"twox-hash",
]
@@ -2158,9 +2158,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "suppaftp"
version = "10.0.0"
version = "10.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b8286a5639930b8fc0400f8a5111439ffb57e5fba6e8276a8bce857ec47b18"
checksum = "9c890e698eaf58526b6e7105d74c5d91ebe76a4da1faac2e20ff10e8e5c8bcff"
dependencies = [
"chrono",
"lazy-regex",
@@ -2352,9 +2352,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
dependencies = [
"indexmap",
"serde_core",
@@ -2385,9 +2385,9 @@ dependencies = [
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "tower"
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "depot"
version = "1.0.0"
version = "1.0.2"
edition = "2024"
[lints.rust]
@@ -22,7 +22,7 @@ static-except-libc = [
anyhow = "1.0.103"
ar = "0.9.0"
bzip2 = "0.6.1"
clap = { version = "4.6.0", features = ["derive"] }
clap = { version = "4.6.2", features = ["derive"] }
flate2 = "1.1.9"
git2 = "0.21.0"
indicatif = "0.18.6"
@@ -35,7 +35,7 @@ sha2 = "0.11.0"
tar = "0.4.46"
tempfile = "=3.27.0"
thiserror = "2.0.18"
toml = "1.1.2"
toml = "1.1.3"
url = "2.5.8"
walkdir = "2.5.0"
xz2 = "0.1.7"
@@ -43,7 +43,7 @@ zip = "8.5.0"
zstd = { version = "0.13.3", features = ["zstdmt"] }
inquire = "0.9.4"
md5 = "0.8.1"
suppaftp = "10.0.0"
suppaftp = "10.0.1"
minisign = "0.9.1"
petgraph = "0.8.3"
fd-lock = "4.0.4"
@@ -54,7 +54,7 @@ sys-mount = { version = "3.1.0", default-features = false }
time = { version = "0.3.53", features = ["formatting", "parsing"] }
b2sum-rust = "0.3.0"
serde_ignored = "0.1.14"
lz4_flex = "0.13.1"
lz4_flex = "0.14.0"
lzma-rust2 = "0.16.5"
signal-hook = "0.4.4"
sha1 = "0.11.0"
+30
View File
@@ -12,6 +12,7 @@ const BUILD_OPTION_KEYS: &[&str] = &[
fn main() {
println!("cargo:rerun-if-env-changed=CC");
println!("cargo:rerun-if-changed=src/fakeroot_preload.c");
for key in BUILD_OPTION_KEYS {
println!("cargo:rerun-if-env-changed={key}");
if let Ok(value) = std::env::var(key) {
@@ -19,6 +20,10 @@ fn main() {
}
}
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") {
build_fakeroot_preload();
}
if std::env::var_os("CARGO_FEATURE_STATIC_EXCEPT_LIBC").is_some()
&& std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux")
{
@@ -32,6 +37,31 @@ fn main() {
}
}
fn build_fakeroot_preload() {
let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into());
let output_path = std::path::PathBuf::from(
std::env::var_os("OUT_DIR").expect("Cargo must provide OUT_DIR to build.rs"),
)
.join("libdepot_fakeroot.so");
let output = std::process::Command::new(compiler)
.args([
"-std=c11", "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror", "-o",
])
.arg(&output_path)
.arg("src/fakeroot_preload.c")
.output()
.unwrap_or_else(|error| {
panic!("failed to launch C compiler for fakeroot preload: {error}")
});
if !output.status.success() {
panic!(
"failed to build fakeroot preload library:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
}
fn gcc_runtime_archive(name: &str) -> Option<String> {
let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into());
let output = std::process::Command::new(compiler)
Executable → Regular
+3 -549
View File
@@ -339,7 +339,7 @@ pub fn build(
Some("install"),
);
for install_dir in install_dirs {
let mut install_cmd = fakeroot::wrap_install_command(make_exec, &install_destdir);
let mut install_cmd = fakeroot::wrap_install_command(make_exec, &install_destdir)?;
install_cmd.current_dir(&install_dir);
if make_exec_supports_make_assignments(make_exec)
&& !has_make_variable_override(&flags.make_install_vars, "DESTDIR")
@@ -652,7 +652,7 @@ pub(crate) fn run_helper_install(
);
for install_dir in install_dirs {
let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir));
let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir))?;
install_cmd.current_dir(&install_dir);
if make_exec_supports_make_assignments(make_exec)
&& !has_make_variable_override(&flags.make_install_vars, "DESTDIR")
@@ -1201,550 +1201,4 @@ fn expand_configure_arg(spec: &PackageSpec, arg: &str, envs: &[(String, String)]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn test_expand_shell_commands_simple() -> Result<()> {
let out = expand_shell_commands("x $(echo foo) y", "gcc")?;
assert_eq!(out, "x foo y");
Ok(())
}
#[test]
fn test_expand_shell_commands_replace_cc() -> Result<()> {
// The command contains $CC which should be replaced with provided cc
let out = expand_shell_commands("start $($CC -v >/dev/null; echo OK) end", "mycc")?;
// Since the inner command echoes OK, after replacing $CC it should run and include OK
assert!(out.contains("OK") || out.contains(""));
Ok(())
}
#[test]
fn test_expand_with_envs_prefers_provided_envs() {
let envs = vec![
("CARCH".to_string(), "x86_64".to_string()),
("CHOST".to_string(), "x86_64-sfg-linux-gnu".to_string()),
];
let out = expand_with_envs("--with-gcc-arch=$CARCH --host=${CHOST}", &envs);
assert!(out.contains("--with-gcc-arch=x86_64"));
assert!(out.contains("--host=x86_64-sfg-linux-gnu"));
}
#[test]
fn test_expand_configure_arg_expands_spec_and_env_vars() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.2.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let envs = vec![("CARCH".to_string(), "aarch64".to_string())];
let expanded =
expand_configure_arg(&spec, "--program-prefix=$name-$version-$CARCH-", &envs);
assert_eq!(expanded, "--program-prefix=foo-1.2.3-aarch64-");
}
#[test]
fn test_expand_configure_arg_expands_host_build_dir_env() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.2.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let envs = vec![(
crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(),
"/tmp/build-host".to_string(),
)];
let expanded = expand_configure_arg(
&spec,
"--with-build-tools=$DEPOT_BUILD_HOST_DIR/tools",
&envs,
);
assert_eq!(expanded, "--with-build-tools=/tmp/build-host/tools");
}
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_configure_help_supports_host_build() {
let help = "Usage: configure [OPTION]...\n --host=HOST cross host\n --build=BUILD";
assert!(configure_help_supports_option(help, "--host"));
assert!(configure_help_supports_option(help, "--build"));
assert!(!configure_help_supports_option(help, "--target"));
}
#[test]
fn test_configure_help_supports_enable_disable_aliases() {
let help = " --enable-static build static libraries\n --with-zlib=DIR";
assert!(configure_help_supports_option(help, "--disable-static"));
assert!(configure_help_supports_option(help, "--without-zlib"));
}
#[test]
fn test_configure_help_supports_option_requires_exact_match() {
let help = "\
--host-cc=HOSTCC use host C compiler
--build-suffix=SUFFIX library name suffix []";
assert!(!configure_help_supports_option(help, "--host"));
assert!(!configure_help_supports_option(help, "--build"));
}
#[test]
fn test_looks_like_configure_help_text_accepts_bootstrap_style_output() {
let help = "\
Usage: ./bootstrap [<options>...]
Options:
--help
--prefix=PREFIX";
assert!(looks_like_configure_help_text(help));
}
#[test]
fn test_looks_like_configure_help_text_rejects_non_help_output() {
assert!(!looks_like_configure_help_text(""));
assert!(!looks_like_configure_help_text(
"Unknown option: --disable-static"
));
}
#[test]
fn test_configure_supports_option_defaults_by_configure_file_usage() {
assert!(configure_supports_option(None, "--host", ""));
assert!(!configure_supports_option(
None,
"--host",
"build-aux/Configure"
));
}
#[test]
fn test_default_configure_install_dirs_injects_expected_paths() {
let flags = BuildFlags::default();
let help = "\
--bindir=DIR
--sbindir=DIR
--libdir=DIR
--libexecdir=DIR
--sysconfdir=DIR
--localstatedir=DIR
--sharedstatedir=DIR
--includedir=DIR
--datarootdir=DIR
--datadir=DIR
--mandir=DIR
--infodir=DIR";
let args = default_configure_install_dirs(&flags, Some(help));
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--sbindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--libdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--sysconfdir=/etc"));
assert!(args.iter().any(|a| a == "--localstatedir=/var"));
assert!(args.iter().any(|a| a == "--sharedstatedir=/var/lib"));
assert!(args.iter().any(|a| a == "--includedir=/usr/include"));
assert!(args.iter().any(|a| a == "--datarootdir=/usr/share"));
assert!(args.iter().any(|a| a == "--datadir=/usr/share"));
assert!(args.iter().any(|a| a == "--mandir=/usr/share/man"));
assert!(args.iter().any(|a| a == "--infodir=/usr/share/info"));
}
#[test]
fn test_default_configure_install_dirs_respects_explicit_user_overrides() {
let flags = BuildFlags {
configure: vec![
"--sbindir=/sbin".to_string(),
"--libdir=/custom/lib".to_string(),
"--datadir=/custom/share".to_string(),
],
..BuildFlags::default()
};
let help = "--bindir=DIR --sbindir=DIR --libdir=DIR --datadir=DIR";
let args = default_configure_install_dirs(&flags, Some(help));
assert!(!args.iter().any(|a| a.starts_with("--sbindir=")));
assert!(!args.iter().any(|a| a.starts_with("--libdir=")));
assert!(!args.iter().any(|a| a.starts_with("--datadir=")));
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
}
#[test]
fn test_default_configure_install_dirs_lib32_uses_lib32_dirs() {
let help = "--libdir=DIR --libexecdir=DIR";
let flags = BuildFlags {
lib32_variant: true,
..BuildFlags::default()
};
let args = default_configure_install_dirs(&flags, Some(help));
assert!(args.iter().any(|a| a == "--libdir=/usr/lib32"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32"));
}
#[test]
fn test_default_configure_install_dirs_skips_when_not_advertised() {
let flags = BuildFlags::default();
let args = default_configure_install_dirs(&flags, Some("--prefix=PREFIX"));
assert!(args.is_empty());
}
#[test]
fn test_configure_long_option_extracts_long_option_name() {
assert_eq!(
configure_long_option(" --disable-static "),
Some("--disable-static")
);
assert_eq!(
configure_long_option("--with-zlib=/usr"),
Some("--with-zlib")
);
assert_eq!(configure_long_option("prefix=/usr"), None);
assert_eq!(configure_long_option("-C"), None);
}
#[test]
fn test_add_auto_configure_arg_if_supported_skips_unsupported_long_option() {
let mut cmd = Command::new("configure");
add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "--disable-nls");
let args: Vec<String> = cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect();
assert!(args.is_empty());
}
#[test]
fn test_add_auto_configure_arg_if_supported_keeps_supported_alias_and_non_option_args() {
let mut cmd = Command::new("configure");
add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "--disable-static");
add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "srcdir");
let args: Vec<String> = cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect();
assert_eq!(args, vec!["--disable-static", "srcdir"]);
}
#[test]
fn test_install_destdir_path_uses_build_dir_for_lib32() {
let build_dir = Path::new("/tmp/build");
let destdir = Path::new("/tmp/pkg");
assert_eq!(
install_destdir_path(build_dir, destdir, false),
destdir.to_path_buf()
);
assert_eq!(
install_destdir_path(build_dir, destdir, true),
build_dir.join("destdir")
);
}
#[test]
fn test_makefile_content_has_target_detects_check_and_test() {
let content = r#"
.PHONY: all check
all:
@echo all
check:
@echo check
"#;
assert!(makefile_content_has_target(content, "check"));
assert!(!makefile_content_has_target(content, "test"));
}
#[test]
fn test_makefile_content_has_target_ignores_assignments() {
let content = r#"
TEST := value
VAR:=$(shell echo hi)
foo: bar
@true
"#;
assert!(!makefile_content_has_target(content, "TEST"));
assert!(!makefile_content_has_target(content, "VAR"));
assert!(!makefile_content_has_target(content, "check"));
}
#[test]
fn test_maybe_find_autotools_test_target_respects_skip_tests() -> Result<()> {
let tmp = tempdir().unwrap();
std::fs::write(tmp.path().join("Makefile"), "check:\n\t@true\n").unwrap();
let skipped = maybe_find_autotools_test_target(tmp.path(), true)?;
assert_eq!(skipped, None);
let detected = maybe_find_autotools_test_target(tmp.path(), false)?;
assert_eq!(detected, Some("check"));
Ok(())
}
#[test]
fn test_resolve_make_dirs_defaults_to_build_dir() -> Result<()> {
let tmp = tempdir().unwrap();
let dirs = resolve_make_dirs(tmp.path(), &[], "build.flags.make_dirs")?;
assert_eq!(dirs, vec![tmp.path().to_path_buf()]);
Ok(())
}
#[test]
fn test_resolve_make_dirs_resolves_multiple_relative_dirs() -> Result<()> {
let tmp = tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("lib"))?;
std::fs::create_dir_all(tmp.path().join("libelf"))?;
let dirs = resolve_make_dirs(
tmp.path(),
&["lib".to_string(), "libelf".to_string()],
"build.flags.make_dirs",
)?;
assert_eq!(
dirs,
vec![tmp.path().join("lib"), tmp.path().join("libelf")]
);
Ok(())
}
#[test]
fn test_add_make_variable_overrides_accepts_valid_assignments() -> Result<()> {
let mut cmd = Command::new("make");
add_make_variable_overrides(
&mut cmd,
&[
"CC=clang".to_string(),
"V=1".to_string(),
" CFLAGS=-O2 -pipe ".to_string(),
],
"build",
)?;
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
assert_eq!(args, vec!["CC=clang", "V=1", "CFLAGS=-O2 -pipe"]);
Ok(())
}
#[test]
fn test_add_make_variable_overrides_rejects_invalid_assignment() {
let mut cmd = Command::new("make");
let err = add_make_variable_overrides(&mut cmd, &["not-an-assignment".to_string()], "test")
.expect_err("expected invalid assignment to fail");
assert!(err.to_string().contains("expected NAME=VALUE"));
}
#[test]
fn test_has_make_variable_override_detects_destdir() {
assert!(has_make_variable_override(
&["DESTDIR=/tmp/pkg".to_string()],
"DESTDIR"
));
assert!(has_make_variable_override(
&[" DESTDIR =/tmp/pkg ".to_string()],
"DESTDIR"
));
assert!(!has_make_variable_override(
&["V=1".to_string(), "PREFIX=/usr".to_string()],
"DESTDIR"
));
}
#[test]
fn test_resolve_make_exec_defaults_and_trims() {
assert_eq!(resolve_make_exec(""), "make");
assert_eq!(resolve_make_exec(" "), "make");
assert_eq!(resolve_make_exec(" ninja "), "ninja");
}
#[test]
fn test_make_exec_supports_make_assignments_detects_make_variants() {
assert!(make_exec_supports_make_assignments("make"));
assert!(make_exec_supports_make_assignments("/usr/bin/gmake"));
assert!(!make_exec_supports_make_assignments("ninja"));
}
#[test]
fn test_phase_targets_merges_singular_plural_and_default() {
assert_eq!(
phase_targets("bootstrap", &["stage1".into(), "stage2".into()], None),
vec![
"bootstrap".to_string(),
"stage1".to_string(),
"stage2".to_string()
]
);
assert_eq!(
phase_targets("", &[], Some("install")),
vec!["install".to_string()]
);
}
#[test]
fn test_add_make_variable_overrides_if_supported_rejects_ninja_vars() {
let mut cmd = Command::new("ninja");
let err = add_make_variable_overrides_if_supported(
&mut cmd,
"ninja",
&["V=1".to_string()],
"build",
)
.expect_err("ninja should reject make variable override syntax");
assert!(err.to_string().contains("build.flags.make_vars"));
}
#[test]
fn test_resolve_actual_src_expands_source_subdir_vars() {
let tmp = tempdir().unwrap();
let src_root = tmp.path().join("srcroot");
let expanded = src_root.join("expect5.45.4").join("unix");
std::fs::create_dir_all(&expanded).unwrap();
let spec = PackageSpec {
package: PackageInfo {
name: "expect".into(),
real_name: None,
version: "5.45.4".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags {
source_subdir: "$name$version/unix".into(),
..BuildFlags::default()
},
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let resolved = resolve_actual_src(&spec, &src_root).unwrap();
assert_eq!(resolved, expanded);
}
#[test]
fn test_resolve_configure_path_defaults_to_source_configure() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let actual_src = PathBuf::from("/tmp/src");
let configure = resolve_configure_path(&spec, &actual_src);
assert_eq!(configure, actual_src.join("configure"));
}
#[test]
fn test_resolve_configure_path_uses_configure_file_and_expands_vars() {
let flags = BuildFlags {
configure_file: "build-aux/$name-configure".into(),
..BuildFlags::default()
};
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let actual_src = PathBuf::from("/tmp/src");
let configure = resolve_configure_path(&spec, &actual_src);
assert_eq!(configure, actual_src.join("build-aux/foo-configure"));
}
}
mod tests;
+540
View File
@@ -0,0 +1,540 @@
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn test_expand_shell_commands_simple() -> Result<()> {
let out = expand_shell_commands("x $(echo foo) y", "gcc")?;
assert_eq!(out, "x foo y");
Ok(())
}
#[test]
fn test_expand_shell_commands_replace_cc() -> Result<()> {
// The command contains $CC which should be replaced with provided cc
let out = expand_shell_commands("start $($CC -v >/dev/null; echo OK) end", "mycc")?;
// Since the inner command echoes OK, after replacing $CC it should run and include OK
assert!(out.contains("OK") || out.contains(""));
Ok(())
}
#[test]
fn test_expand_with_envs_prefers_provided_envs() {
let envs = vec![
("CARCH".to_string(), "x86_64".to_string()),
("CHOST".to_string(), "x86_64-sfg-linux-gnu".to_string()),
];
let out = expand_with_envs("--with-gcc-arch=$CARCH --host=${CHOST}", &envs);
assert!(out.contains("--with-gcc-arch=x86_64"));
assert!(out.contains("--host=x86_64-sfg-linux-gnu"));
}
#[test]
fn test_expand_configure_arg_expands_spec_and_env_vars() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.2.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let envs = vec![("CARCH".to_string(), "aarch64".to_string())];
let expanded = expand_configure_arg(&spec, "--program-prefix=$name-$version-$CARCH-", &envs);
assert_eq!(expanded, "--program-prefix=foo-1.2.3-aarch64-");
}
#[test]
fn test_expand_configure_arg_expands_host_build_dir_env() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.2.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let envs = vec![(
crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(),
"/tmp/build-host".to_string(),
)];
let expanded = expand_configure_arg(
&spec,
"--with-build-tools=$DEPOT_BUILD_HOST_DIR/tools",
&envs,
);
assert_eq!(expanded, "--with-build-tools=/tmp/build-host/tools");
}
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_configure_help_supports_host_build() {
let help = "Usage: configure [OPTION]...\n --host=HOST cross host\n --build=BUILD";
assert!(configure_help_supports_option(help, "--host"));
assert!(configure_help_supports_option(help, "--build"));
assert!(!configure_help_supports_option(help, "--target"));
}
#[test]
fn test_configure_help_supports_enable_disable_aliases() {
let help = " --enable-static build static libraries\n --with-zlib=DIR";
assert!(configure_help_supports_option(help, "--disable-static"));
assert!(configure_help_supports_option(help, "--without-zlib"));
}
#[test]
fn test_configure_help_supports_option_requires_exact_match() {
let help = "\
--host-cc=HOSTCC use host C compiler
--build-suffix=SUFFIX library name suffix []";
assert!(!configure_help_supports_option(help, "--host"));
assert!(!configure_help_supports_option(help, "--build"));
}
#[test]
fn test_looks_like_configure_help_text_accepts_bootstrap_style_output() {
let help = "\
Usage: ./bootstrap [<options>...]
Options:
--help
--prefix=PREFIX";
assert!(looks_like_configure_help_text(help));
}
#[test]
fn test_looks_like_configure_help_text_rejects_non_help_output() {
assert!(!looks_like_configure_help_text(""));
assert!(!looks_like_configure_help_text(
"Unknown option: --disable-static"
));
}
#[test]
fn test_configure_supports_option_defaults_by_configure_file_usage() {
assert!(configure_supports_option(None, "--host", ""));
assert!(!configure_supports_option(
None,
"--host",
"build-aux/Configure"
));
}
#[test]
fn test_default_configure_install_dirs_injects_expected_paths() {
let flags = BuildFlags::default();
let help = "\
--bindir=DIR
--sbindir=DIR
--libdir=DIR
--libexecdir=DIR
--sysconfdir=DIR
--localstatedir=DIR
--sharedstatedir=DIR
--includedir=DIR
--datarootdir=DIR
--datadir=DIR
--mandir=DIR
--infodir=DIR";
let args = default_configure_install_dirs(&flags, Some(help));
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--sbindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--libdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--sysconfdir=/etc"));
assert!(args.iter().any(|a| a == "--localstatedir=/var"));
assert!(args.iter().any(|a| a == "--sharedstatedir=/var/lib"));
assert!(args.iter().any(|a| a == "--includedir=/usr/include"));
assert!(args.iter().any(|a| a == "--datarootdir=/usr/share"));
assert!(args.iter().any(|a| a == "--datadir=/usr/share"));
assert!(args.iter().any(|a| a == "--mandir=/usr/share/man"));
assert!(args.iter().any(|a| a == "--infodir=/usr/share/info"));
}
#[test]
fn test_default_configure_install_dirs_respects_explicit_user_overrides() {
let flags = BuildFlags {
configure: vec![
"--sbindir=/sbin".to_string(),
"--libdir=/custom/lib".to_string(),
"--datadir=/custom/share".to_string(),
],
..BuildFlags::default()
};
let help = "--bindir=DIR --sbindir=DIR --libdir=DIR --datadir=DIR";
let args = default_configure_install_dirs(&flags, Some(help));
assert!(!args.iter().any(|a| a.starts_with("--sbindir=")));
assert!(!args.iter().any(|a| a.starts_with("--libdir=")));
assert!(!args.iter().any(|a| a.starts_with("--datadir=")));
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
}
#[test]
fn test_default_configure_install_dirs_lib32_uses_lib32_dirs() {
let help = "--libdir=DIR --libexecdir=DIR";
let flags = BuildFlags {
lib32_variant: true,
..BuildFlags::default()
};
let args = default_configure_install_dirs(&flags, Some(help));
assert!(args.iter().any(|a| a == "--libdir=/usr/lib32"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32"));
}
#[test]
fn test_default_configure_install_dirs_skips_when_not_advertised() {
let flags = BuildFlags::default();
let args = default_configure_install_dirs(&flags, Some("--prefix=PREFIX"));
assert!(args.is_empty());
}
#[test]
fn test_configure_long_option_extracts_long_option_name() {
assert_eq!(
configure_long_option(" --disable-static "),
Some("--disable-static")
);
assert_eq!(
configure_long_option("--with-zlib=/usr"),
Some("--with-zlib")
);
assert_eq!(configure_long_option("prefix=/usr"), None);
assert_eq!(configure_long_option("-C"), None);
}
#[test]
fn test_add_auto_configure_arg_if_supported_skips_unsupported_long_option() {
let mut cmd = Command::new("configure");
add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "--disable-nls");
let args: Vec<String> = cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect();
assert!(args.is_empty());
}
#[test]
fn test_add_auto_configure_arg_if_supported_keeps_supported_alias_and_non_option_args() {
let mut cmd = Command::new("configure");
add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "--disable-static");
add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "srcdir");
let args: Vec<String> = cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect();
assert_eq!(args, vec!["--disable-static", "srcdir"]);
}
#[test]
fn test_install_destdir_path_uses_build_dir_for_lib32() {
let build_dir = Path::new("/tmp/build");
let destdir = Path::new("/tmp/pkg");
assert_eq!(
install_destdir_path(build_dir, destdir, false),
destdir.to_path_buf()
);
assert_eq!(
install_destdir_path(build_dir, destdir, true),
build_dir.join("destdir")
);
}
#[test]
fn test_makefile_content_has_target_detects_check_and_test() {
let content = r#"
.PHONY: all check
all:
@echo all
check:
@echo check
"#;
assert!(makefile_content_has_target(content, "check"));
assert!(!makefile_content_has_target(content, "test"));
}
#[test]
fn test_makefile_content_has_target_ignores_assignments() {
let content = r#"
TEST := value
VAR:=$(shell echo hi)
foo: bar
@true
"#;
assert!(!makefile_content_has_target(content, "TEST"));
assert!(!makefile_content_has_target(content, "VAR"));
assert!(!makefile_content_has_target(content, "check"));
}
#[test]
fn test_maybe_find_autotools_test_target_respects_skip_tests() -> Result<()> {
let tmp = tempdir().unwrap();
std::fs::write(tmp.path().join("Makefile"), "check:\n\t@true\n").unwrap();
let skipped = maybe_find_autotools_test_target(tmp.path(), true)?;
assert_eq!(skipped, None);
let detected = maybe_find_autotools_test_target(tmp.path(), false)?;
assert_eq!(detected, Some("check"));
Ok(())
}
#[test]
fn test_resolve_make_dirs_defaults_to_build_dir() -> Result<()> {
let tmp = tempdir().unwrap();
let dirs = resolve_make_dirs(tmp.path(), &[], "build.flags.make_dirs")?;
assert_eq!(dirs, vec![tmp.path().to_path_buf()]);
Ok(())
}
#[test]
fn test_resolve_make_dirs_resolves_multiple_relative_dirs() -> Result<()> {
let tmp = tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("lib"))?;
std::fs::create_dir_all(tmp.path().join("libelf"))?;
let dirs = resolve_make_dirs(
tmp.path(),
&["lib".to_string(), "libelf".to_string()],
"build.flags.make_dirs",
)?;
assert_eq!(
dirs,
vec![tmp.path().join("lib"), tmp.path().join("libelf")]
);
Ok(())
}
#[test]
fn test_add_make_variable_overrides_accepts_valid_assignments() -> Result<()> {
let mut cmd = Command::new("make");
add_make_variable_overrides(
&mut cmd,
&[
"CC=clang".to_string(),
"V=1".to_string(),
" CFLAGS=-O2 -pipe ".to_string(),
],
"build",
)?;
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
assert_eq!(args, vec!["CC=clang", "V=1", "CFLAGS=-O2 -pipe"]);
Ok(())
}
#[test]
fn test_add_make_variable_overrides_rejects_invalid_assignment() {
let mut cmd = Command::new("make");
let err = add_make_variable_overrides(&mut cmd, &["not-an-assignment".to_string()], "test")
.expect_err("expected invalid assignment to fail");
assert!(err.to_string().contains("expected NAME=VALUE"));
}
#[test]
fn test_has_make_variable_override_detects_destdir() {
assert!(has_make_variable_override(
&["DESTDIR=/tmp/pkg".to_string()],
"DESTDIR"
));
assert!(has_make_variable_override(
&[" DESTDIR =/tmp/pkg ".to_string()],
"DESTDIR"
));
assert!(!has_make_variable_override(
&["V=1".to_string(), "PREFIX=/usr".to_string()],
"DESTDIR"
));
}
#[test]
fn test_resolve_make_exec_defaults_and_trims() {
assert_eq!(resolve_make_exec(""), "make");
assert_eq!(resolve_make_exec(" "), "make");
assert_eq!(resolve_make_exec(" ninja "), "ninja");
}
#[test]
fn test_make_exec_supports_make_assignments_detects_make_variants() {
assert!(make_exec_supports_make_assignments("make"));
assert!(make_exec_supports_make_assignments("/usr/bin/gmake"));
assert!(!make_exec_supports_make_assignments("ninja"));
}
#[test]
fn test_phase_targets_merges_singular_plural_and_default() {
assert_eq!(
phase_targets("bootstrap", &["stage1".into(), "stage2".into()], None),
vec![
"bootstrap".to_string(),
"stage1".to_string(),
"stage2".to_string()
]
);
assert_eq!(
phase_targets("", &[], Some("install")),
vec!["install".to_string()]
);
}
#[test]
fn test_add_make_variable_overrides_if_supported_rejects_ninja_vars() {
let mut cmd = Command::new("ninja");
let err =
add_make_variable_overrides_if_supported(&mut cmd, "ninja", &["V=1".to_string()], "build")
.expect_err("ninja should reject make variable override syntax");
assert!(err.to_string().contains("build.flags.make_vars"));
}
#[test]
fn test_resolve_actual_src_expands_source_subdir_vars() {
let tmp = tempdir().unwrap();
let src_root = tmp.path().join("srcroot");
let expanded = src_root.join("expect5.45.4").join("unix");
std::fs::create_dir_all(&expanded).unwrap();
let spec = PackageSpec {
package: PackageInfo {
name: "expect".into(),
real_name: None,
version: "5.45.4".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags {
source_subdir: "$name$version/unix".into(),
..BuildFlags::default()
},
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let resolved = resolve_actual_src(&spec, &src_root).unwrap();
assert_eq!(resolved, expanded);
}
#[test]
fn test_resolve_configure_path_defaults_to_source_configure() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let actual_src = PathBuf::from("/tmp/src");
let configure = resolve_configure_path(&spec, &actual_src);
assert_eq!(configure, actual_src.join("configure"));
}
#[test]
fn test_resolve_configure_path_uses_configure_file_and_expands_vars() {
let flags = BuildFlags {
configure_file: "build-aux/$name-configure".into(),
..BuildFlags::default()
};
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let actual_src = PathBuf::from("/tmp/src");
let configure = resolve_configure_path(&spec, &actual_src);
assert_eq!(configure, actual_src.join("build-aux/foo-configure"));
}
Executable → Regular
+3 -439
View File
@@ -227,7 +227,7 @@ pub fn build(
})?;
}
let mut install_cmd = fakeroot::wrap_install_command("cmake", &install_destdir);
let mut install_cmd = fakeroot::wrap_install_command("cmake", &install_destdir)?;
if !install_targets.is_empty() {
install_cmd.arg("--build").arg(&build_dir);
install_cmd.arg("--target");
@@ -466,7 +466,7 @@ pub(crate) fn run_helper_install(
let destdir = std::env::var("DESTDIR").context("DESTDIR must be set for cmake_install")?;
let install_targets = phase_targets(&flags.make_install_target, &flags.make_install_targets);
let mut install_cmd = fakeroot::wrap_install_command("cmake", Path::new(&destdir));
let mut install_cmd = fakeroot::wrap_install_command("cmake", Path::new(&destdir))?;
if install_targets.is_empty() {
install_cmd.arg("--install").arg(&build_dir);
} else {
@@ -810,440 +810,4 @@ fn resolve_actual_src(
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, PackageInfo, PackageSpec};
use crate::test_support::TestEnv;
use tempfile::tempdir;
#[test]
fn test_expand_env_vars_replaces_vars() {
// Set a test env var
let mut env = TestEnv::new();
env.set_var("DEPOT_TEST_FOO", "bar");
let input = "$DEPOT_TEST_FOO and ${DEPOT_TEST_FOO}";
let out = expand_env_vars(input);
assert!(out.contains("bar"));
assert_eq!(out, "bar and bar");
}
#[test]
fn test_expand_with_envs_prefers_provided_envs() {
let envs = vec![
("CXX".to_string(), "my-cxx".to_string()),
("CC".to_string(), "my-cc".to_string()),
];
let s = "-DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=${CXX} -DROOT=$HOME";
let out = expand_with_envs(s, &envs);
assert!(out.contains("my-cc"));
assert!(out.contains("my-cxx"));
// $HOME should be expanded from process env (may be present)
}
#[test]
fn test_expand_with_envs_expands_host_build_dir() {
let envs = vec![(
crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(),
"/tmp/build-host".to_string(),
)];
let out = expand_with_envs("-DTOOLS_DIR=$DEPOT_BUILD_HOST_DIR/bin", &envs);
assert_eq!(out, "-DTOOLS_DIR=/tmp/build-host/bin");
}
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_phase_targets_merges_singular_and_plural() {
assert_eq!(
phase_targets("bootstrap", &["stage1".into(), "stage2".into()]),
vec![
"bootstrap".to_string(),
"stage1".to_string(),
"stage2".to_string()
]
);
assert!(phase_targets("", &[]).is_empty());
}
#[test]
fn test_cmake_uses_default_ctest_without_explicit_targets() {
assert!(cmake_uses_default_ctest(&BuildFlags::default()));
let explicit_single = BuildFlags {
make_test_target: "test".into(),
..BuildFlags::default()
};
assert!(!cmake_uses_default_ctest(&explicit_single));
let explicit_many = BuildFlags {
make_test_targets: vec!["check".into()],
..BuildFlags::default()
};
assert!(!cmake_uses_default_ctest(&explicit_many));
}
#[test]
fn test_cmake_generator_for_make_exec_detects_ninja_and_make() {
assert_eq!(cmake_generator_for_make_exec("ninja"), Some("Ninja"));
assert_eq!(
cmake_generator_for_make_exec("/usr/bin/gmake"),
Some("Unix Makefiles")
);
assert_eq!(cmake_generator_for_make_exec("samurai"), None);
}
#[test]
fn test_cmake_configure_flag_detectors() {
assert!(cmake_configure_flags_specify_generator(&[
"-G".to_string(),
"Ninja".to_string()
]));
assert!(cmake_configure_flags_specify_generator(&[
"--generator=Unix Makefiles".to_string()
]));
assert!(!cmake_configure_flags_specify_generator(&[
"-DCMAKE_BUILD_TYPE=Release".to_string()
]));
assert!(cmake_configure_flags_set_make_program(&[
"-DCMAKE_MAKE_PROGRAM=/usr/bin/ninja".to_string()
]));
assert!(!cmake_configure_flags_set_make_program(&[
"-DCMAKE_C_COMPILER=clang".to_string()
]));
}
#[test]
fn test_cmake_cache_entry_value_supports_plain_and_typed_entries() {
let flags = vec![
"-DCMAKE_INSTALL_PREFIX=/usr".to_string(),
"-DCMAKE_INSTALL_LIBDIR:PATH=/usr/lib64".to_string(),
];
assert_eq!(
cmake_cache_entry_value(&flags, "CMAKE_INSTALL_PREFIX"),
Some("/usr")
);
assert_eq!(
cmake_cache_entry_value(&flags, "CMAKE_INSTALL_LIBDIR"),
Some("/usr/lib64")
);
assert_eq!(
cmake_cache_entry_value(&flags, "CMAKE_INSTALL_BINDIR"),
None
);
}
#[test]
fn test_effective_cmake_install_prefix_prefers_explicit_configure_flag() {
let flags = BuildFlags {
prefix: "/usr".into(),
configure: vec!["-DCMAKE_INSTALL_PREFIX=/opt/soundtouch".into()],
..BuildFlags::default()
};
assert_eq!(effective_cmake_install_prefix(&flags), "/opt/soundtouch");
}
#[test]
fn test_cmake_dir_value_for_prefix_converts_prefix_owned_absolute_paths() {
assert_eq!(cmake_dir_value_for_prefix("/usr", "/usr/lib".into()), "lib");
assert_eq!(
cmake_dir_value_for_prefix("/usr", "/usr/share/man".into()),
"share/man"
);
}
#[test]
fn test_cmake_dir_value_for_prefix_keeps_non_prefix_absolute_paths() {
assert_eq!(cmake_dir_value_for_prefix("/usr", "/etc".into()), "/etc");
assert_eq!(
cmake_dir_value_for_prefix("/opt/pkg", "/usr/bin".into()),
"/usr/bin"
);
}
#[test]
fn test_cmake_install_dir_args_include_prefix_relative_defaults() {
let args = cmake_install_dir_args(&BuildFlags::default(), "/usr");
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_LOCALSTATEDIR=/var")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_SHAREDSTATEDIR=/var/lib")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_DATAROOTDIR=share")
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INFODIR=share/info")
);
}
#[test]
fn test_cmake_install_dir_args_use_lib32_defaults() {
let flags = BuildFlags {
lib32_variant: true,
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, "/usr");
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib32"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib32"));
}
#[test]
fn test_cmake_lib32_target_args_include_compiler_target_defaults() {
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".into(),
..BuildFlags::default()
};
let args = cmake_lib32_target_args(&flags, None);
assert!(args.iter().any(|a| a == "-DCMAKE_SYSTEM_PROCESSOR=i686"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_C_COMPILER_TARGET=i686-sfg-linux-gnu")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_CXX_COMPILER_TARGET=i686-sfg-linux-gnu")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_ASM_COMPILER_TARGET=i686-sfg-linux-gnu")
);
}
#[test]
fn test_cmake_lib32_target_args_respect_explicit_overrides() {
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".into(),
configure: vec![
"-DCMAKE_C_COMPILER_TARGET=i686-custom-linux-gnu".into(),
"-DCMAKE_SYSTEM_PROCESSOR=i686".into(),
],
..BuildFlags::default()
};
let args = cmake_lib32_target_args(&flags, None);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_C_COMPILER_TARGET="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_SYSTEM_PROCESSOR="))
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_CXX_COMPILER_TARGET=i686-sfg-linux-gnu")
);
}
#[test]
fn test_cmake_depot_sysroot_args_skip_live_rootfs() {
let args = cmake_depot_sysroot_args(&BuildFlags::default(), "/");
assert!(args.is_empty());
}
#[test]
fn test_cmake_depot_sysroot_args_include_non_live_rootfs_defaults() {
let args = cmake_depot_sysroot_args(&BuildFlags::default(), "/tmp/depot-root");
assert!(args.iter().any(|a| a == "-DCMAKE_SYSROOT=/tmp/depot-root"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY")
);
}
#[test]
fn test_cmake_depot_sysroot_args_respect_explicit_configure_overrides() {
let flags = BuildFlags {
configure: vec![
"-DCMAKE_SYSROOT=/opt/custom-root".into(),
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY:STRING=BOTH".into(),
],
..BuildFlags::default()
};
let args = cmake_depot_sysroot_args(&flags, "/tmp/depot-root");
assert!(!args.iter().any(|a| a.starts_with("-DCMAKE_SYSROOT=")));
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY="))
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
);
}
#[test]
fn test_cmake_install_dir_args_respect_explicit_user_overrides() {
let flags = BuildFlags {
configure: vec![
"-DCMAKE_INSTALL_SBINDIR=/sbin".to_string(),
"-DCMAKE_INSTALL_LIBDIR:PATH=/custom/lib".to_string(),
"-DCMAKE_INSTALL_DATADIR=/custom/share".to_string(),
],
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, "/usr");
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_SBINDIR="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_LIBDIR="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR="))
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
}
#[test]
fn test_cmake_install_dir_args_convert_custom_prefix_owned_dirs() {
let flags = BuildFlags {
prefix: "/opt/soundtouch".into(),
bindir: "/opt/soundtouch/bin".into(),
libdir: "/opt/soundtouch/lib64".into(),
includedir: "/opt/soundtouch/include/soundtouch".into(),
datadir: "/opt/soundtouch/share".into(),
mandir: "/opt/soundtouch/share/man".into(),
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, effective_cmake_install_prefix(&flags));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib64"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include/soundtouch")
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man"));
}
#[test]
fn test_cmake_install_dir_args_keep_absolute_dirs_outside_prefix() {
let flags = BuildFlags {
prefix: "/opt/soundtouch".into(),
bindir: "/usr/bin".into(),
sysconfdir: "/etc".into(),
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, effective_cmake_install_prefix(&flags));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
}
#[test]
fn resolve_actual_src_prefers_srcdir_then_specdir_and_handles_absolute() {
let tmp = tempdir().unwrap();
let src_root = tmp.path().join("srcroot");
let spec_dir = tmp.path().join("specdir");
let external = tmp.path().join("external");
let expanded = src_root.join("x-1.0").join("sub");
std::fs::create_dir_all(src_root.join("sub")).unwrap();
std::fs::create_dir_all(&expanded).unwrap();
// create directories for candidates
std::fs::create_dir_all(spec_dir.join("../llvm")).unwrap();
std::fs::create_dir_all(&external).unwrap();
let spec = PackageSpec {
package: PackageInfo {
name: "x".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::CMake,
flags: BuildFlags {
source_subdir: "sub".into(),
..BuildFlags::default()
},
},
dependencies: Default::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.clone(),
};
// case: relative path under src_dir
let p = resolve_actual_src(&spec, &src_root).unwrap();
assert!(p.ends_with("sub"));
// case: ../llvm should resolve relative to spec_dir
let mut spec2 = spec.clone();
spec2.build.flags.source_subdir = "../llvm".into();
let p2 = resolve_actual_src(&spec2, &src_root).unwrap();
assert!(p2.ends_with("llvm"));
// case: absolute path
let mut spec3 = spec.clone();
spec3.build.flags.source_subdir = external.to_string_lossy().into_owned();
let p3 = resolve_actual_src(&spec3, &src_root).unwrap();
assert_eq!(p3, external);
// case: variable expansion in source_subdir
let mut spec4 = spec.clone();
spec4.build.flags.source_subdir = "$name-$version/sub".into();
let p4 = resolve_actual_src(&spec4, &src_root).unwrap();
assert_eq!(p4, expanded);
}
}
mod tests;
+435
View File
@@ -0,0 +1,435 @@
use super::*;
use crate::package::{Build, BuildFlags, BuildType, PackageInfo, PackageSpec};
use crate::test_support::TestEnv;
use tempfile::tempdir;
#[test]
fn test_expand_env_vars_replaces_vars() {
// Set a test env var
let mut env = TestEnv::new();
env.set_var("DEPOT_TEST_FOO", "bar");
let input = "$DEPOT_TEST_FOO and ${DEPOT_TEST_FOO}";
let out = expand_env_vars(input);
assert!(out.contains("bar"));
assert_eq!(out, "bar and bar");
}
#[test]
fn test_expand_with_envs_prefers_provided_envs() {
let envs = vec![
("CXX".to_string(), "my-cxx".to_string()),
("CC".to_string(), "my-cc".to_string()),
];
let s = "-DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=${CXX} -DROOT=$HOME";
let out = expand_with_envs(s, &envs);
assert!(out.contains("my-cc"));
assert!(out.contains("my-cxx"));
// $HOME should be expanded from process env (may be present)
}
#[test]
fn test_expand_with_envs_expands_host_build_dir() {
let envs = vec![(
crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(),
"/tmp/build-host".to_string(),
)];
let out = expand_with_envs("-DTOOLS_DIR=$DEPOT_BUILD_HOST_DIR/bin", &envs);
assert_eq!(out, "-DTOOLS_DIR=/tmp/build-host/bin");
}
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_phase_targets_merges_singular_and_plural() {
assert_eq!(
phase_targets("bootstrap", &["stage1".into(), "stage2".into()]),
vec![
"bootstrap".to_string(),
"stage1".to_string(),
"stage2".to_string()
]
);
assert!(phase_targets("", &[]).is_empty());
}
#[test]
fn test_cmake_uses_default_ctest_without_explicit_targets() {
assert!(cmake_uses_default_ctest(&BuildFlags::default()));
let explicit_single = BuildFlags {
make_test_target: "test".into(),
..BuildFlags::default()
};
assert!(!cmake_uses_default_ctest(&explicit_single));
let explicit_many = BuildFlags {
make_test_targets: vec!["check".into()],
..BuildFlags::default()
};
assert!(!cmake_uses_default_ctest(&explicit_many));
}
#[test]
fn test_cmake_generator_for_make_exec_detects_ninja_and_make() {
assert_eq!(cmake_generator_for_make_exec("ninja"), Some("Ninja"));
assert_eq!(
cmake_generator_for_make_exec("/usr/bin/gmake"),
Some("Unix Makefiles")
);
assert_eq!(cmake_generator_for_make_exec("samurai"), None);
}
#[test]
fn test_cmake_configure_flag_detectors() {
assert!(cmake_configure_flags_specify_generator(&[
"-G".to_string(),
"Ninja".to_string()
]));
assert!(cmake_configure_flags_specify_generator(&[
"--generator=Unix Makefiles".to_string()
]));
assert!(!cmake_configure_flags_specify_generator(&[
"-DCMAKE_BUILD_TYPE=Release".to_string()
]));
assert!(cmake_configure_flags_set_make_program(&[
"-DCMAKE_MAKE_PROGRAM=/usr/bin/ninja".to_string()
]));
assert!(!cmake_configure_flags_set_make_program(&[
"-DCMAKE_C_COMPILER=clang".to_string()
]));
}
#[test]
fn test_cmake_cache_entry_value_supports_plain_and_typed_entries() {
let flags = vec![
"-DCMAKE_INSTALL_PREFIX=/usr".to_string(),
"-DCMAKE_INSTALL_LIBDIR:PATH=/usr/lib64".to_string(),
];
assert_eq!(
cmake_cache_entry_value(&flags, "CMAKE_INSTALL_PREFIX"),
Some("/usr")
);
assert_eq!(
cmake_cache_entry_value(&flags, "CMAKE_INSTALL_LIBDIR"),
Some("/usr/lib64")
);
assert_eq!(
cmake_cache_entry_value(&flags, "CMAKE_INSTALL_BINDIR"),
None
);
}
#[test]
fn test_effective_cmake_install_prefix_prefers_explicit_configure_flag() {
let flags = BuildFlags {
prefix: "/usr".into(),
configure: vec!["-DCMAKE_INSTALL_PREFIX=/opt/soundtouch".into()],
..BuildFlags::default()
};
assert_eq!(effective_cmake_install_prefix(&flags), "/opt/soundtouch");
}
#[test]
fn test_cmake_dir_value_for_prefix_converts_prefix_owned_absolute_paths() {
assert_eq!(cmake_dir_value_for_prefix("/usr", "/usr/lib".into()), "lib");
assert_eq!(
cmake_dir_value_for_prefix("/usr", "/usr/share/man".into()),
"share/man"
);
}
#[test]
fn test_cmake_dir_value_for_prefix_keeps_non_prefix_absolute_paths() {
assert_eq!(cmake_dir_value_for_prefix("/usr", "/etc".into()), "/etc");
assert_eq!(
cmake_dir_value_for_prefix("/opt/pkg", "/usr/bin".into()),
"/usr/bin"
);
}
#[test]
fn test_cmake_install_dir_args_include_prefix_relative_defaults() {
let args = cmake_install_dir_args(&BuildFlags::default(), "/usr");
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_LOCALSTATEDIR=/var")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_SHAREDSTATEDIR=/var/lib")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_DATAROOTDIR=share")
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INFODIR=share/info")
);
}
#[test]
fn test_cmake_install_dir_args_use_lib32_defaults() {
let flags = BuildFlags {
lib32_variant: true,
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, "/usr");
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib32"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib32"));
}
#[test]
fn test_cmake_lib32_target_args_include_compiler_target_defaults() {
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".into(),
..BuildFlags::default()
};
let args = cmake_lib32_target_args(&flags, None);
assert!(args.iter().any(|a| a == "-DCMAKE_SYSTEM_PROCESSOR=i686"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_C_COMPILER_TARGET=i686-sfg-linux-gnu")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_CXX_COMPILER_TARGET=i686-sfg-linux-gnu")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_ASM_COMPILER_TARGET=i686-sfg-linux-gnu")
);
}
#[test]
fn test_cmake_lib32_target_args_respect_explicit_overrides() {
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".into(),
configure: vec![
"-DCMAKE_C_COMPILER_TARGET=i686-custom-linux-gnu".into(),
"-DCMAKE_SYSTEM_PROCESSOR=i686".into(),
],
..BuildFlags::default()
};
let args = cmake_lib32_target_args(&flags, None);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_C_COMPILER_TARGET="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_SYSTEM_PROCESSOR="))
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_CXX_COMPILER_TARGET=i686-sfg-linux-gnu")
);
}
#[test]
fn test_cmake_depot_sysroot_args_skip_live_rootfs() {
let args = cmake_depot_sysroot_args(&BuildFlags::default(), "/");
assert!(args.is_empty());
}
#[test]
fn test_cmake_depot_sysroot_args_include_non_live_rootfs_defaults() {
let args = cmake_depot_sysroot_args(&BuildFlags::default(), "/tmp/depot-root");
assert!(args.iter().any(|a| a == "-DCMAKE_SYSROOT=/tmp/depot-root"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY")
);
}
#[test]
fn test_cmake_depot_sysroot_args_respect_explicit_configure_overrides() {
let flags = BuildFlags {
configure: vec![
"-DCMAKE_SYSROOT=/opt/custom-root".into(),
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY:STRING=BOTH".into(),
],
..BuildFlags::default()
};
let args = cmake_depot_sysroot_args(&flags, "/tmp/depot-root");
assert!(!args.iter().any(|a| a.starts_with("-DCMAKE_SYSROOT=")));
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY="))
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
);
}
#[test]
fn test_cmake_install_dir_args_respect_explicit_user_overrides() {
let flags = BuildFlags {
configure: vec![
"-DCMAKE_INSTALL_SBINDIR=/sbin".to_string(),
"-DCMAKE_INSTALL_LIBDIR:PATH=/custom/lib".to_string(),
"-DCMAKE_INSTALL_DATADIR=/custom/share".to_string(),
],
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, "/usr");
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_SBINDIR="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_LIBDIR="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR="))
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
}
#[test]
fn test_cmake_install_dir_args_convert_custom_prefix_owned_dirs() {
let flags = BuildFlags {
prefix: "/opt/soundtouch".into(),
bindir: "/opt/soundtouch/bin".into(),
libdir: "/opt/soundtouch/lib64".into(),
includedir: "/opt/soundtouch/include/soundtouch".into(),
datadir: "/opt/soundtouch/share".into(),
mandir: "/opt/soundtouch/share/man".into(),
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, effective_cmake_install_prefix(&flags));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib64"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include/soundtouch")
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man"));
}
#[test]
fn test_cmake_install_dir_args_keep_absolute_dirs_outside_prefix() {
let flags = BuildFlags {
prefix: "/opt/soundtouch".into(),
bindir: "/usr/bin".into(),
sysconfdir: "/etc".into(),
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, effective_cmake_install_prefix(&flags));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
}
#[test]
fn resolve_actual_src_prefers_srcdir_then_specdir_and_handles_absolute() {
let tmp = tempdir().unwrap();
let src_root = tmp.path().join("srcroot");
let spec_dir = tmp.path().join("specdir");
let external = tmp.path().join("external");
let expanded = src_root.join("x-1.0").join("sub");
std::fs::create_dir_all(src_root.join("sub")).unwrap();
std::fs::create_dir_all(&expanded).unwrap();
// create directories for candidates
std::fs::create_dir_all(spec_dir.join("../llvm")).unwrap();
std::fs::create_dir_all(&external).unwrap();
let spec = PackageSpec {
package: PackageInfo {
name: "x".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::CMake,
flags: BuildFlags {
source_subdir: "sub".into(),
..BuildFlags::default()
},
},
dependencies: Default::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.clone(),
};
// case: relative path under src_dir
let p = resolve_actual_src(&spec, &src_root).unwrap();
assert!(p.ends_with("sub"));
// case: ../llvm should resolve relative to spec_dir
let mut spec2 = spec.clone();
spec2.build.flags.source_subdir = "../llvm".into();
let p2 = resolve_actual_src(&spec2, &src_root).unwrap();
assert!(p2.ends_with("llvm"));
// case: absolute path
let mut spec3 = spec.clone();
spec3.build.flags.source_subdir = external.to_string_lossy().into_owned();
let p3 = resolve_actual_src(&spec3, &src_root).unwrap();
assert_eq!(p3, external);
// case: variable expansion in source_subdir
let mut spec4 = spec.clone();
spec4.build.flags.source_subdir = "$name-$version/sub".into();
let p4 = resolve_actual_src(&spec4, &src_root).unwrap();
assert_eq!(p4, expanded);
}
+10 -8
View File
@@ -156,7 +156,7 @@ pub fn build(
}
);
// Use POSIX `sh` (doing something wrong if your system doesn't have it...)
let mut cmd = fakeroot::wrap_install_command("sh", &install_destdir);
let mut cmd = fakeroot::wrap_install_command("sh", &install_destdir)?;
let wrapper = crate::shell_helpers::wrap_shell_command(". \"$1\"");
// Run custom scripts through `sh -c` so helper commands like `haul`
// work even when the helper scripts live on a `noexec` mount.
@@ -257,7 +257,7 @@ fn build_function_mode_install_command(
destdir: &Path,
build_script: &Path,
build_ran: bool,
) -> Command {
) -> Result<Command> {
let mut wrapper = function_mode_shell_prelude();
wrapper.push_str(&format!(
"depot_build_ran={}\n",
@@ -311,9 +311,9 @@ fn build_function_mode_install_command(
wrapper.push_str("fi\n");
}
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
let mut cmd = fakeroot::wrap_install_command("sh", destdir)?;
cmd.arg("-c").arg(wrapper).arg("sh").arg(build_script);
cmd
Ok(cmd)
}
fn run_function_mode_build_script(
@@ -357,7 +357,7 @@ fn run_function_mode_build_script(
.unwrap_or(false);
let mut install_cmd =
build_function_mode_install_command(spec, install_destdir, build_script, build_ran);
build_function_mode_install_command(spec, install_destdir, build_script, build_ran)?;
install_cmd.current_dir(build_dir);
crate::builder::prepare_tool_command(&mut install_cmd, env_vars);
let install_status = crate::interrupts::command_status(&mut install_cmd).map_err(|e| {
@@ -613,18 +613,20 @@ depot_install() {
}
#[test]
fn test_build_function_mode_commands_split_fakeroot_boundary() {
fn test_build_function_mode_commands_split_fakeroot_boundary() -> Result<()> {
let tmp_dest = tempdir()?;
let build_script = Path::new("/tmp/build.sh");
let state_file = Path::new("/tmp/build-state");
let install_destdir = Path::new("/tmp/destdir");
let install_destdir = tmp_dest.path();
let build_cmd = build_function_mode_build_command(build_script, state_file);
assert_eq!(build_cmd.get_program(), std::ffi::OsStr::new("sh"));
let spec = mk_spec("custom-function-fakeroot-split", "1.0");
let install_cmd =
build_function_mode_install_command(&spec, install_destdir, build_script, true);
build_function_mode_install_command(&spec, install_destdir, build_script, true)?;
assert_eq!(install_cmd.get_program(), std::ffi::OsStr::new("sh"));
Ok(())
}
#[test]
+1 -1
View File
@@ -100,7 +100,7 @@ pub fn build(
crate::log_info!(" Executing: {}", cmd_str);
// We need to run each command under internal fakeroot
let mut cmd = crate::fakeroot::wrap_install_command("sh", &install_destdir);
let mut cmd = crate::fakeroot::wrap_install_command("sh", &install_destdir)?;
cmd.arg("-c").arg(&cmd_str);
cmd.current_dir(src_dir);
Executable → Regular
+3 -348
View File
@@ -170,7 +170,7 @@ pub fn build(
})?;
}
let mut install_cmd = fakeroot::wrap_install_command("meson", &install_destdir);
let mut install_cmd = fakeroot::wrap_install_command("meson", &install_destdir)?;
install_cmd.arg("install");
install_cmd.arg("-C").arg(&build_dir);
@@ -336,7 +336,7 @@ pub(crate) fn run_helper_install(
.unwrap_or_else(|| resolve_build_dir(&helper_source_dir(), &flags));
let destdir = std::env::var("DESTDIR").context("DESTDIR must be set for meson_install")?;
let mut install_cmd = fakeroot::wrap_install_command("meson", Path::new(&destdir));
let mut install_cmd = fakeroot::wrap_install_command("meson", Path::new(&destdir))?;
install_cmd.arg("install");
install_cmd.arg("-C").arg(&build_dir);
for arg in extra_args {
@@ -736,349 +736,4 @@ fn resolve_actual_src(spec: &crate::package::PackageSpec, src_dir: &Path) -> Res
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source};
use crate::test_support::TestEnv;
use tempfile::tempdir;
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_meson_setup_args_include_configure_flags() {
let flags = BuildFlags {
prefix: "/usr".to_string(),
configure: vec!["-Dmanpages=false".to_string()],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "-Dmanpages=false"));
assert!(args.iter().any(|a| a == "--prefix=/usr"));
assert!(args.iter().any(|a| a == "--buildtype=release"));
}
#[test]
fn test_meson_setup_args_expand_host_build_dir() {
let flags = BuildFlags {
configure: vec!["-Dtools_dir=$DEPOT_BUILD_HOST_DIR/bin".into()],
..BuildFlags::default()
};
let args = meson_setup_args(
&flags,
None,
&[(
crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(),
"/tmp/build-host".to_string(),
)],
);
assert!(args.iter().any(|a| a == "-Dtools_dir=/tmp/build-host/bin"));
}
#[test]
fn test_configure_pkg_config_env_uses_lib32_dirs_and_pkgconf() -> Result<()> {
let tmp = tempdir()?;
let pkgconf = tmp.path().join("pkgconf");
std::fs::write(&pkgconf, "#!/bin/sh\nexit 0\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&pkgconf)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&pkgconf, perms)?;
}
let mut env = TestEnv::new();
env.set_var("PATH", tmp.path());
env.set_var("PKG_CONFIG", "");
let mut env_vars = Vec::new();
let flags = BuildFlags {
lib32_variant: true,
rootfs: "/".into(),
..BuildFlags::default()
};
configure_pkg_config_env(&mut env_vars, &flags, None);
assert!(
env_vars
.iter()
.any(|(k, v)| k == "PKG_CONFIG" && v.ends_with("/pkgconf"))
);
assert!(env_vars.iter().any(|(k, v)| {
k == "PKG_CONFIG_LIBDIR" && v == "/usr/lib32/pkgconfig:/usr/share/pkgconfig"
}));
Ok(())
}
#[test]
fn test_generate_lib32_meson_cross_file_writes_pkg_config_binary_when_available() -> Result<()>
{
let tmp = tempdir()?;
let tools = tempdir()?;
let pkgconf = tools.path().join("pkgconf");
std::fs::write(&pkgconf, "#!/bin/sh\nexit 0\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&pkgconf)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&pkgconf, perms)?;
}
let mut env = TestEnv::new();
env.set_var("PATH", tools.path());
env.set_var("PKG_CONFIG", "");
let flags = BuildFlags {
chost: "x86_64-sfg-linux-gnu".into(),
..BuildFlags::default()
};
let path = generate_lib32_meson_cross_file(&flags, tmp.path())?;
let content = std::fs::read_to_string(path)?;
assert!(content.contains("pkg-config = '/"));
assert!(content.contains("pkgconf"));
Ok(())
}
#[test]
fn test_meson_setup_args_include_install_dirs() {
let args = meson_setup_args(&BuildFlags::default(), None, &[]);
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--sbindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--libdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--sysconfdir=/etc"));
assert!(args.iter().any(|a| a == "--localstatedir=/var"));
assert!(args.iter().any(|a| a == "--sharedstatedir=/var/lib"));
assert!(args.iter().any(|a| a == "--includedir=/usr/include"));
assert!(args.iter().any(|a| a == "--datadir=/usr/share"));
assert!(args.iter().any(|a| a == "--mandir=/usr/share/man"));
assert!(args.iter().any(|a| a == "--infodir=/usr/share/info"));
}
#[test]
fn test_meson_setup_args_derive_dirs_from_datarootdir() {
let flags = BuildFlags {
datarootdir: "/opt/share-root".to_string(),
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(!args.iter().any(|a| a.starts_with("--datarootdir=")));
assert!(args.iter().any(|a| a == "--datadir=/opt/share-root"));
assert!(args.iter().any(|a| a == "--mandir=/opt/share-root/man"));
assert!(args.iter().any(|a| a == "--infodir=/opt/share-root/info"));
}
#[test]
fn test_meson_setup_args_honor_explicit_prefix() {
let flags = BuildFlags {
prefix: "/usr".to_string(),
configure: vec!["--prefix=/opt".to_string()],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert_eq!(args.iter().filter(|a| a.starts_with("--prefix")).count(), 1);
assert!(args.iter().any(|a| a == "--prefix=/opt"));
}
#[test]
fn test_meson_setup_args_honor_explicit_install_dirs() {
let flags = BuildFlags {
configure: vec![
"--sbindir=/sbin".to_string(),
"--libdir=/custom/lib".to_string(),
"--datadir=/custom/share".to_string(),
],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(!args.iter().any(|a| a == "--sbindir=/usr/bin"));
assert!(!args.iter().any(|a| a == "--libdir=/usr/lib"));
assert!(!args.iter().any(|a| a == "--datadir=/usr/share"));
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
}
#[test]
fn test_meson_setup_args_use_lib32_defaults() {
let flags = BuildFlags {
lib32_variant: true,
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "--libdir=/usr/lib32"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32"));
}
#[test]
fn test_meson_setup_args_include_linker_override() {
let flags = BuildFlags {
ld: "ld.lld".to_string(),
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "-Dc_ld=ld.lld"));
assert!(args.iter().any(|a| a == "-Dcpp_ld=ld.lld"));
}
#[test]
fn test_meson_setup_args_honor_explicit_linker_override() {
let flags = BuildFlags {
ld: "ld.lld".to_string(),
configure: vec!["-Dc_ld=gold".to_string(), "-Dcpp_ld=gold".to_string()],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert_eq!(args.iter().filter(|a| *a == "-Dc_ld=gold").count(), 1);
assert_eq!(args.iter().filter(|a| *a == "-Dcpp_ld=gold").count(), 1);
assert!(!args.iter().any(|a| a == "-Dc_ld=ld.lld"));
assert!(!args.iter().any(|a| a == "-Dcpp_ld=ld.lld"));
}
#[test]
fn test_compiler_command_with_lib32_target_adds_clang_target() {
let parts = compiler_command_with_lib32_target("clang -m32", "i686-sfg-linux-gnu");
assert_eq!(
parts,
vec![
"clang".to_string(),
"-m32".to_string(),
"--target=i686-sfg-linux-gnu".to_string()
]
);
}
#[test]
fn test_compiler_command_with_lib32_target_skips_non_clang_compilers() {
let parts = compiler_command_with_lib32_target("gcc -m32", "i686-sfg-linux-gnu");
assert_eq!(parts, vec!["gcc".to_string(), "-m32".to_string()]);
}
#[test]
fn test_generate_lib32_meson_cross_file_sets_x86_host_machine() -> Result<()> {
let tmp = tempdir()?;
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".to_string(),
cc: "clang -m32".to_string(),
cxx: "clang++ -m32".to_string(),
ar: "llvm-ar".to_string(),
strip: "llvm-strip".to_string(),
ld: "ld.lld".to_string(),
nm: "llvm-nm".to_string(),
objcopy: "llvm-objcopy".to_string(),
objdump: "llvm-objdump".to_string(),
readelf: "llvm-readelf".to_string(),
..BuildFlags::default()
};
let path = generate_lib32_meson_cross_file(&flags, tmp.path())?;
let content = fs::read_to_string(path)?;
assert!(content.contains("Generated by depot for target: i686-sfg-linux-gnu"));
assert!(content.contains("c = ['clang', '-m32', '--target=i686-sfg-linux-gnu']"));
assert!(content.contains("cpp = ['clang++', '-m32', '--target=i686-sfg-linux-gnu']"));
assert!(content.contains("strip = 'llvm-strip'"));
assert!(content.contains("ld = 'ld.lld'"));
assert!(content.contains("nm = 'llvm-nm'"));
assert!(content.contains("objcopy = 'llvm-objcopy'"));
assert!(content.contains("objdump = 'llvm-objdump'"));
assert!(content.contains("readelf = 'llvm-readelf'"));
assert!(content.contains("cpu_family = 'x86'"));
assert!(content.contains("cpu = 'i686'"));
Ok(())
}
#[test]
fn test_resolve_build_dir_uses_flag() {
let flags = BuildFlags {
build_dir: Some("build".to_string()),
..BuildFlags::default()
};
let src = Path::new("/tmp/src");
assert_eq!(
resolve_build_dir(src, &flags),
PathBuf::from("/tmp/src/build")
);
}
#[test]
fn test_meson_test_suites_uses_single_and_multiple_targets() {
let flags = BuildFlags {
make_test_target: "unit".to_string(),
make_test_targets: vec!["integration".to_string(), " smoke ".to_string()],
..BuildFlags::default()
};
assert_eq!(
meson_test_suites(&flags),
vec![
"unit".to_string(),
"integration".to_string(),
"smoke".to_string()
]
);
}
#[test]
fn test_meson_test_suites_empty_without_targets() {
assert!(meson_test_suites(&BuildFlags::default()).is_empty());
}
#[test]
fn test_resolve_actual_src_uses_source_subdir_under_source() -> Result<()> {
let src = tempdir()?;
let spec_dir = tempdir()?;
fs::create_dir_all(src.path().join("sub"))?;
let spec = PackageSpec {
package: PackageInfo {
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "u".into(),
sha256: "s".into(),
extract_dir: "e".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Meson,
flags: BuildFlags {
source_subdir: "sub".into(),
..BuildFlags::default()
},
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.path().to_path_buf(),
};
let resolved = resolve_actual_src(&spec, src.path())?;
assert_eq!(resolved, src.path().join("sub"));
Ok(())
}
}
mod tests;
+343
View File
@@ -0,0 +1,343 @@
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source};
use crate::test_support::TestEnv;
use tempfile::tempdir;
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_meson_setup_args_include_configure_flags() {
let flags = BuildFlags {
prefix: "/usr".to_string(),
configure: vec!["-Dmanpages=false".to_string()],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "-Dmanpages=false"));
assert!(args.iter().any(|a| a == "--prefix=/usr"));
assert!(args.iter().any(|a| a == "--buildtype=release"));
}
#[test]
fn test_meson_setup_args_expand_host_build_dir() {
let flags = BuildFlags {
configure: vec!["-Dtools_dir=$DEPOT_BUILD_HOST_DIR/bin".into()],
..BuildFlags::default()
};
let args = meson_setup_args(
&flags,
None,
&[(
crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(),
"/tmp/build-host".to_string(),
)],
);
assert!(args.iter().any(|a| a == "-Dtools_dir=/tmp/build-host/bin"));
}
#[test]
fn test_configure_pkg_config_env_uses_lib32_dirs_and_pkgconf() -> Result<()> {
let tmp = tempdir()?;
let pkgconf = tmp.path().join("pkgconf");
std::fs::write(&pkgconf, "#!/bin/sh\nexit 0\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&pkgconf)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&pkgconf, perms)?;
}
let mut env = TestEnv::new();
env.set_var("PATH", tmp.path());
env.set_var("PKG_CONFIG", "");
let mut env_vars = Vec::new();
let flags = BuildFlags {
lib32_variant: true,
rootfs: "/".into(),
..BuildFlags::default()
};
configure_pkg_config_env(&mut env_vars, &flags, None);
assert!(
env_vars
.iter()
.any(|(k, v)| k == "PKG_CONFIG" && v.ends_with("/pkgconf"))
);
assert!(env_vars.iter().any(|(k, v)| {
k == "PKG_CONFIG_LIBDIR" && v == "/usr/lib32/pkgconfig:/usr/share/pkgconfig"
}));
Ok(())
}
#[test]
fn test_generate_lib32_meson_cross_file_writes_pkg_config_binary_when_available() -> Result<()> {
let tmp = tempdir()?;
let tools = tempdir()?;
let pkgconf = tools.path().join("pkgconf");
std::fs::write(&pkgconf, "#!/bin/sh\nexit 0\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&pkgconf)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&pkgconf, perms)?;
}
let mut env = TestEnv::new();
env.set_var("PATH", tools.path());
env.set_var("PKG_CONFIG", "");
let flags = BuildFlags {
chost: "x86_64-sfg-linux-gnu".into(),
..BuildFlags::default()
};
let path = generate_lib32_meson_cross_file(&flags, tmp.path())?;
let content = std::fs::read_to_string(path)?;
assert!(content.contains("pkg-config = '/"));
assert!(content.contains("pkgconf"));
Ok(())
}
#[test]
fn test_meson_setup_args_include_install_dirs() {
let args = meson_setup_args(&BuildFlags::default(), None, &[]);
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--sbindir=/usr/bin"));
assert!(args.iter().any(|a| a == "--libdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib"));
assert!(args.iter().any(|a| a == "--sysconfdir=/etc"));
assert!(args.iter().any(|a| a == "--localstatedir=/var"));
assert!(args.iter().any(|a| a == "--sharedstatedir=/var/lib"));
assert!(args.iter().any(|a| a == "--includedir=/usr/include"));
assert!(args.iter().any(|a| a == "--datadir=/usr/share"));
assert!(args.iter().any(|a| a == "--mandir=/usr/share/man"));
assert!(args.iter().any(|a| a == "--infodir=/usr/share/info"));
}
#[test]
fn test_meson_setup_args_derive_dirs_from_datarootdir() {
let flags = BuildFlags {
datarootdir: "/opt/share-root".to_string(),
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(!args.iter().any(|a| a.starts_with("--datarootdir=")));
assert!(args.iter().any(|a| a == "--datadir=/opt/share-root"));
assert!(args.iter().any(|a| a == "--mandir=/opt/share-root/man"));
assert!(args.iter().any(|a| a == "--infodir=/opt/share-root/info"));
}
#[test]
fn test_meson_setup_args_honor_explicit_prefix() {
let flags = BuildFlags {
prefix: "/usr".to_string(),
configure: vec!["--prefix=/opt".to_string()],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert_eq!(args.iter().filter(|a| a.starts_with("--prefix")).count(), 1);
assert!(args.iter().any(|a| a == "--prefix=/opt"));
}
#[test]
fn test_meson_setup_args_honor_explicit_install_dirs() {
let flags = BuildFlags {
configure: vec![
"--sbindir=/sbin".to_string(),
"--libdir=/custom/lib".to_string(),
"--datadir=/custom/share".to_string(),
],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(!args.iter().any(|a| a == "--sbindir=/usr/bin"));
assert!(!args.iter().any(|a| a == "--libdir=/usr/lib"));
assert!(!args.iter().any(|a| a == "--datadir=/usr/share"));
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
}
#[test]
fn test_meson_setup_args_use_lib32_defaults() {
let flags = BuildFlags {
lib32_variant: true,
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "--libdir=/usr/lib32"));
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32"));
}
#[test]
fn test_meson_setup_args_include_linker_override() {
let flags = BuildFlags {
ld: "ld.lld".to_string(),
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "-Dc_ld=ld.lld"));
assert!(args.iter().any(|a| a == "-Dcpp_ld=ld.lld"));
}
#[test]
fn test_meson_setup_args_honor_explicit_linker_override() {
let flags = BuildFlags {
ld: "ld.lld".to_string(),
configure: vec!["-Dc_ld=gold".to_string(), "-Dcpp_ld=gold".to_string()],
..BuildFlags::default()
};
let args = meson_setup_args(&flags, None, &[]);
assert_eq!(args.iter().filter(|a| *a == "-Dc_ld=gold").count(), 1);
assert_eq!(args.iter().filter(|a| *a == "-Dcpp_ld=gold").count(), 1);
assert!(!args.iter().any(|a| a == "-Dc_ld=ld.lld"));
assert!(!args.iter().any(|a| a == "-Dcpp_ld=ld.lld"));
}
#[test]
fn test_compiler_command_with_lib32_target_adds_clang_target() {
let parts = compiler_command_with_lib32_target("clang -m32", "i686-sfg-linux-gnu");
assert_eq!(
parts,
vec![
"clang".to_string(),
"-m32".to_string(),
"--target=i686-sfg-linux-gnu".to_string()
]
);
}
#[test]
fn test_compiler_command_with_lib32_target_skips_non_clang_compilers() {
let parts = compiler_command_with_lib32_target("gcc -m32", "i686-sfg-linux-gnu");
assert_eq!(parts, vec!["gcc".to_string(), "-m32".to_string()]);
}
#[test]
fn test_generate_lib32_meson_cross_file_sets_x86_host_machine() -> Result<()> {
let tmp = tempdir()?;
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".to_string(),
cc: "clang -m32".to_string(),
cxx: "clang++ -m32".to_string(),
ar: "llvm-ar".to_string(),
strip: "llvm-strip".to_string(),
ld: "ld.lld".to_string(),
nm: "llvm-nm".to_string(),
objcopy: "llvm-objcopy".to_string(),
objdump: "llvm-objdump".to_string(),
readelf: "llvm-readelf".to_string(),
..BuildFlags::default()
};
let path = generate_lib32_meson_cross_file(&flags, tmp.path())?;
let content = fs::read_to_string(path)?;
assert!(content.contains("Generated by depot for target: i686-sfg-linux-gnu"));
assert!(content.contains("c = ['clang', '-m32', '--target=i686-sfg-linux-gnu']"));
assert!(content.contains("cpp = ['clang++', '-m32', '--target=i686-sfg-linux-gnu']"));
assert!(content.contains("strip = 'llvm-strip'"));
assert!(content.contains("ld = 'ld.lld'"));
assert!(content.contains("nm = 'llvm-nm'"));
assert!(content.contains("objcopy = 'llvm-objcopy'"));
assert!(content.contains("objdump = 'llvm-objdump'"));
assert!(content.contains("readelf = 'llvm-readelf'"));
assert!(content.contains("cpu_family = 'x86'"));
assert!(content.contains("cpu = 'i686'"));
Ok(())
}
#[test]
fn test_resolve_build_dir_uses_flag() {
let flags = BuildFlags {
build_dir: Some("build".to_string()),
..BuildFlags::default()
};
let src = Path::new("/tmp/src");
assert_eq!(
resolve_build_dir(src, &flags),
PathBuf::from("/tmp/src/build")
);
}
#[test]
fn test_meson_test_suites_uses_single_and_multiple_targets() {
let flags = BuildFlags {
make_test_target: "unit".to_string(),
make_test_targets: vec!["integration".to_string(), " smoke ".to_string()],
..BuildFlags::default()
};
assert_eq!(
meson_test_suites(&flags),
vec![
"unit".to_string(),
"integration".to_string(),
"smoke".to_string()
]
);
}
#[test]
fn test_meson_test_suites_empty_without_targets() {
assert!(meson_test_suites(&BuildFlags::default()).is_empty());
}
#[test]
fn test_resolve_actual_src_uses_source_subdir_under_source() -> Result<()> {
let src = tempdir()?;
let spec_dir = tempdir()?;
fs::create_dir_all(src.path().join("sub"))?;
let spec = PackageSpec {
package: PackageInfo {
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "u".into(),
sha256: "s".into(),
extract_dir: "e".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Meson,
flags: BuildFlags {
source_subdir: "sub".into(),
..BuildFlags::default()
},
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.path().to_path_buf(),
};
let resolved = resolve_actual_src(&spec, src.path())?;
assert_eq!(resolved, src.path().join("sub"));
Ok(())
}
Executable → Regular
+1 -914
View File
@@ -1141,917 +1141,4 @@ pub fn build(
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
use crate::test_support::TestEnv;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
fn mk_spec(cflags: Vec<&str>, ldflags: Vec<&str>) -> PackageSpec {
let flags = BuildFlags {
cflags: cflags.into_iter().map(String::from).collect(),
ldflags: ldflags.into_iter().map(String::from).collect(),
..BuildFlags::default()
};
PackageSpec {
package: PackageInfo {
name: "env-test".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![crate::package::Source {
url: "https://example.test/src.tar.gz".into(),
sha256: "abc".into(),
extract_dir: "src".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
fn env_value<'a>(env: &'a EnvVars, key: &str) -> Option<&'a str> {
env.iter()
.find(|(candidate, _)| candidate == key)
.map(|(_, value)| value.as_str())
}
#[test]
fn test_prepare_command() {
let mut cmd = Command::new("ls");
// Set an env var that should be cleared
cmd.env("FORBIDDEN", "value");
// Set PATH manually in the current process to ensure it's picked up if it exists
let mut env = TestEnv::new();
env.set_var("PATH", "/usr/bin");
env.set_var("HOME", "/home/test");
env.set_var("SHELL", "/bin/zsh");
env.set_var("DEPOT_ROOTFS", "/my/rootfs");
env.set_var("TERM", "xterm-256color");
env.set_var("CLICOLOR_FORCE", "1");
prepare_command(&mut cmd, &vec![("MYVAR".to_string(), "myval".to_string())]);
let envs: HashMap<_, _> = cmd.get_envs().collect();
assert!(envs.contains_key(OsStr::new("PATH")));
assert!(envs.contains_key(OsStr::new("HOME")));
assert!(!envs.contains_key(OsStr::new("FORBIDDEN")));
assert_eq!(
envs.get(OsStr::new("SHELL")),
Some(&Some(std::ffi::OsString::from("/bin/sh").as_os_str()))
);
assert_eq!(
envs.get(OsStr::new("MYVAR")),
Some(&Some(std::ffi::OsString::from("myval").as_os_str()))
);
// DEPOT_ROOTFS should be preserved from the parent environment
assert_eq!(
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]
fn test_prepare_command_preserves_destdir() {
let mut cmd = std::process::Command::new("ls");
let mut env = TestEnv::new();
env.set_var("DESTDIR", "/tmp/dest");
prepare_command(&mut cmd, &Vec::new());
let envs: HashMap<_, _> = cmd.get_envs().collect();
assert_eq!(
envs.get(OsStr::new("DESTDIR")),
Some(&Some(std::ffi::OsString::from("/tmp/dest").as_os_str()))
);
}
#[test]
fn test_prepare_command_preserves_rust_toolchain_homes() {
let mut cmd = std::process::Command::new("ls");
let mut env = TestEnv::new();
env.set_var("CARGO_HOME", "/var/cache/cargo-home");
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_build_tool_package_option_maps_supported_builders() {
assert_eq!(
build_tool_package_option(BuildType::Meson),
Some("DEPOT_MESON_PACKAGE")
);
assert_eq!(
build_tool_package_option(BuildType::CMake),
Some("DEPOT_CMAKE_PACKAGE")
);
assert_eq!(build_tool_package_option(BuildType::Bin), None);
}
#[test]
fn test_static_build_args_skip_disable_static_when_no_delete_static_enabled() {
let args = static_build_args_for_request(BuildType::Autotools, Some(false), true);
assert!(args.is_empty());
let args = static_build_args_for_request(BuildType::CMake, Some(false), true);
assert!(args.is_empty());
}
#[test]
fn test_static_build_args_keep_other_requested_modes() {
assert_eq!(
static_build_args_for_request(BuildType::Autotools, Some(false), false),
vec![
"--enable-shared".to_string(),
"--disable-static".to_string()
]
);
assert_eq!(
static_build_args_for_request(BuildType::CMake, Some(false), false),
vec!["-DBUILD_SHARED_LIBS=ON".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Meson, Some(false), false),
vec!["-Ddefault_library=shared".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Perl, Some(false), false),
vec!["LINKTYPE=dynamic".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Meson, Some(true), true),
vec!["-Ddefault_library=static".to_string()]
);
}
#[test]
fn test_standard_build_env_exports_native_linker_and_cpp() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.ranlib = "llvm-ranlib".to_string();
spec.build.flags.strip = "llvm-strip".to_string();
spec.build.flags.ld = "ld.lld".to_string();
spec.build.flags.nm = "llvm-nm".to_string();
spec.build.flags.objcopy = "llvm-objcopy".to_string();
spec.build.flags.objdump = "llvm-objdump".to_string();
spec.build.flags.readelf = "llvm-readelf".to_string();
spec.build.flags.cpp = "clang-cpp".to_string();
let env = standard_build_env(&spec, None, true, true);
assert!(env.iter().any(|(k, v)| k == "RANLIB" && v == "llvm-ranlib"));
assert!(env.iter().any(|(k, v)| k == "STRIP" && v == "llvm-strip"));
assert!(env.iter().any(|(k, v)| k == "LD" && v == "ld.lld"));
assert!(env.iter().any(|(k, v)| k == "NM" && v == "llvm-nm"));
assert!(
env.iter()
.any(|(k, v)| k == "OBJCOPY" && v == "llvm-objcopy")
);
assert!(
env.iter()
.any(|(k, v)| k == "OBJDUMP" && v == "llvm-objdump")
);
assert!(
env.iter()
.any(|(k, v)| k == "READELF" && v == "llvm-readelf")
);
assert!(env.iter().any(|(k, v)| k == "CPP" && v == "clang-cpp"));
}
#[test]
fn test_standard_build_env_exports_tool_dir_and_expands_tool_commands() {
let mut spec = mk_spec(
vec!["--gcc-toolchain=$TOOL_DIR"],
vec!["-B$TOOL_DIR", "-Wl,--as-needed"],
);
spec.build.flags.tool_dir = "/opt/depot-tools/bin".to_string();
spec.build.flags.cc = "$TOOL_DIR/clang".to_string();
spec.build.flags.cxx = "$TOOL_DIR/clang++".to_string();
spec.build.flags.ar = "$TOOL_DIR/llvm-ar".to_string();
spec.build.flags.ranlib = "$TOOL_DIR/llvm-ranlib".to_string();
spec.build.flags.ld = "$TOOL_DIR/ld.lld".to_string();
spec.build.flags.env_vars = vec!["LLVM_CONFIG=$TOOL_DIR/llvm-config".to_string()];
let env = standard_build_env(&spec, None, true, true);
assert_eq!(env_value(&env, "TOOL_DIR"), Some("/opt/depot-tools/bin"));
assert_eq!(env_value(&env, "CC"), Some("/opt/depot-tools/bin/clang"));
assert_eq!(env_value(&env, "CXX"), Some("/opt/depot-tools/bin/clang++"));
assert_eq!(env_value(&env, "AR"), Some("/opt/depot-tools/bin/llvm-ar"));
assert_eq!(
env_value(&env, "RANLIB"),
Some("/opt/depot-tools/bin/llvm-ranlib")
);
assert_eq!(env_value(&env, "LD"), Some("/opt/depot-tools/bin/ld.lld"));
assert_eq!(
env_value(&env, "CFLAGS"),
Some("--gcc-toolchain=/opt/depot-tools/bin")
);
assert_eq!(
env_value(&env, "LDFLAGS"),
Some("-B/opt/depot-tools/bin -Wl,--as-needed")
);
assert_eq!(
env_value(&env, "LLVM_CONFIG"),
Some("/opt/depot-tools/bin/llvm-config")
);
}
#[test]
fn test_standard_build_env_cross_uses_package_tool_overrides() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.cc = "/tools/bin/clang".to_string();
spec.build.flags.cxx = "/tools/bin/clang++".to_string();
spec.build.flags.ar = "/tools/bin/llvm-ar".to_string();
spec.build.flags.ranlib = "/tools/bin/llvm-ranlib".to_string();
spec.build.flags.strip = "/tools/bin/llvm-strip".to_string();
spec.build.flags.ld = "/tools/bin/ld.lld".to_string();
spec.build.flags.nm = "/tools/bin/llvm-nm".to_string();
spec.build.flags.objcopy = "/tools/bin/llvm-objcopy".to_string();
spec.build.flags.objdump = "/tools/bin/llvm-objdump".to_string();
spec.build.flags.readelf = "/tools/bin/llvm-readelf".to_string();
spec.build.flags.cpp = "/tools/bin/clang-cpp".to_string();
let cross = CrossConfig {
prefix: "x86_64-test-linux-gnu".into(),
cc: "x86_64-test-linux-gnu-gcc".into(),
cxx: "x86_64-test-linux-gnu-g++".into(),
ar: "x86_64-test-linux-gnu-ar".into(),
ranlib: "x86_64-test-linux-gnu-ranlib".into(),
strip: "x86_64-test-linux-gnu-strip".into(),
ld: "x86_64-test-linux-gnu-ld".into(),
nm: "x86_64-test-linux-gnu-nm".into(),
objcopy: "x86_64-test-linux-gnu-objcopy".into(),
objdump: "x86_64-test-linux-gnu-objdump".into(),
readelf: "x86_64-test-linux-gnu-readelf".into(),
};
let env = standard_build_env(&spec, Some(&cross), true, true);
assert_eq!(env_value(&env, "CC"), Some("/tools/bin/clang"));
assert_eq!(env_value(&env, "CXX"), Some("/tools/bin/clang++"));
assert_eq!(env_value(&env, "AR"), Some("/tools/bin/llvm-ar"));
assert_eq!(env_value(&env, "RANLIB"), Some("/tools/bin/llvm-ranlib"));
assert_eq!(env_value(&env, "STRIP"), Some("/tools/bin/llvm-strip"));
assert_eq!(env_value(&env, "LD"), Some("/tools/bin/ld.lld"));
assert_eq!(env_value(&env, "NM"), Some("/tools/bin/llvm-nm"));
assert_eq!(env_value(&env, "OBJCOPY"), Some("/tools/bin/llvm-objcopy"));
assert_eq!(env_value(&env, "OBJDUMP"), Some("/tools/bin/llvm-objdump"));
assert_eq!(env_value(&env, "READELF"), Some("/tools/bin/llvm-readelf"));
assert_eq!(env_value(&env, "CPP"), Some("/tools/bin/clang-cpp"));
assert_eq!(
env_value(&env, "CROSS_PREFIX"),
Some("x86_64-test-linux-gnu")
);
}
#[test]
fn test_standard_build_env_exports_effective_carch_for_cross_and_lib32() {
let spec = mk_spec(Vec::new(), Vec::new());
let cross = CrossConfig {
prefix: "aarch64-linux-gnu".into(),
cc: "aarch64-linux-gnu-gcc".into(),
cxx: "aarch64-linux-gnu-g++".into(),
ar: "aarch64-linux-gnu-ar".into(),
ranlib: "aarch64-linux-gnu-ranlib".into(),
strip: "aarch64-linux-gnu-strip".into(),
ld: "aarch64-linux-gnu-ld".into(),
nm: "aarch64-linux-gnu-nm".into(),
objcopy: "aarch64-linux-gnu-objcopy".into(),
objdump: "aarch64-linux-gnu-objdump".into(),
readelf: "aarch64-linux-gnu-readelf".into(),
};
let cross_env = standard_build_env(&spec, Some(&cross), true, true);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "CARCH" && v == "aarch64"),
"expected cross builds to export target CARCH"
);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "OBJCOPY" && v == "aarch64-linux-gnu-objcopy"),
"expected cross builds to export OBJCOPY"
);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "OBJDUMP" && v == "aarch64-linux-gnu-objdump"),
"expected cross builds to export OBJDUMP"
);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "READELF" && v == "aarch64-linux-gnu-readelf"),
"expected cross builds to export READELF"
);
let mut lib32_spec = spec.clone();
lib32_spec.build.flags.lib32_variant = true;
lib32_spec.build.flags.carch = "x86_64".into();
let lib32_env = standard_build_env(&lib32_spec, None, true, true);
assert!(
lib32_env.iter().any(|(k, v)| k == "CARCH" && v == "i686"),
"expected lib32 builds to export i686 CARCH"
);
}
#[test]
fn test_spec_with_target_configure_appends_matching_arch_args() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.configure = vec!["--base".to_string()];
spec.build
.flags
.configure_arch
.insert("aarch64".to_string(), vec!["--for-aarch64".to_string()]);
spec.build
.flags
.configure_arch
.insert("x86_64".to_string(), vec!["--for-x86".to_string()]);
let cross = CrossConfig {
prefix: "aarch64-linux-gnu".into(),
cc: "aarch64-linux-gnu-gcc".into(),
cxx: "aarch64-linux-gnu-g++".into(),
ar: "aarch64-linux-gnu-ar".into(),
ranlib: "aarch64-linux-gnu-ranlib".into(),
strip: "aarch64-linux-gnu-strip".into(),
ld: "aarch64-linux-gnu-ld".into(),
nm: "aarch64-linux-gnu-nm".into(),
objcopy: "aarch64-linux-gnu-objcopy".into(),
objdump: "aarch64-linux-gnu-objdump".into(),
readelf: "aarch64-linux-gnu-readelf".into(),
};
let adjusted = spec_with_target_configure(&spec, Some(&cross), TargetBuildKind::Primary)
.expect("expected arch-specific configure args");
assert_eq!(
adjusted.build.flags.configure,
vec!["--base".to_string(), "--for-aarch64".to_string()]
);
}
#[test]
fn test_spec_with_target_configure_uses_lib32_arch() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.lib32_variant = true;
spec.build.flags.carch = "x86_64".to_string();
spec.build
.flags
.configure_arch
.insert("i686".to_string(), vec!["--for-lib32".to_string()]);
let adjusted = spec_with_target_configure(&spec, None, TargetBuildKind::Lib32)
.expect("expected lib32 configure args");
assert_eq!(
adjusted.build.flags.configure,
vec!["--for-lib32".to_string()]
);
}
#[test]
fn test_standard_build_env_respects_export_compiler_flags_toggle() {
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()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
"expected LDFLAGS to be exported when enabled"
);
let disabled = standard_build_env(&spec, None, true, false);
assert!(
!disabled.iter().any(|(k, _)| k == "CFLAGS"),
"expected CFLAGS to be omitted when disabled"
);
assert!(
!disabled.iter().any(|(k, _)| k == "CXXFLAGS"),
"expected CXXFLAGS to be omitted when disabled"
);
assert!(
!disabled.iter().any(|(k, _)| k == "LDFLAGS"),
"expected LDFLAGS to be omitted when disabled"
);
let mut disabled_by_spec = spec.clone();
disabled_by_spec.build.flags.no_flags = true;
let disabled_env = standard_build_env(&disabled_by_spec, None, true, true);
assert!(
!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"
);
}
#[test]
fn test_standard_build_env_injects_ltoflags_into_compiler_and_linker_flags() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into(), "-fuse-linker-plugin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| { k == "CFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin" }),
"expected LTOFLAGS to be appended to CFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin"),
"expected LTOFLAGS to be appended to CXXFLAGS"
);
assert!(
env.iter().any(|(k, v)| {
k == "LDFLAGS" && v == "-Wl,--as-needed -flto=auto -fuse-linker-plugin"
}),
"expected LTOFLAGS to be appended to LDFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto -fuse-linker-plugin"),
"expected LTOFLAGS variable to be exported"
);
}
#[test]
fn test_standard_build_env_injects_fuse_ld_into_ldflags() {
let mut spec = mk_spec(Vec::new(), vec!["-Wl,--as-needed"]);
spec.build.flags.fuse_ld = "/usr/bin/ld.lld".into();
let env = standard_build_env(&spec, None, true, true);
assert_eq!(
env_value(&env, "LDFLAGS"),
Some("-fuse-ld=/usr/bin/ld.lld -Wl,--as-needed")
);
}
#[test]
fn test_standard_build_env_normalizes_fuse_ld_tool_names() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.fuse_ld = "ld.lld".into();
let env = standard_build_env(&spec, None, true, true);
assert_eq!(env_value(&env, "LDFLAGS"), Some("-fuse-ld=lld"));
}
#[test]
fn test_standard_build_env_applies_replace_flag_rules() {
let mut spec = mk_spec(vec!["-D_FORTIFY_SOURCE=3", "-O2"], vec!["-Wl,-O3"]);
spec.build.flags.cxxflags = vec!["-O2".into(), "-stdlib=libc++".into()];
spec.build.flags.replace_cflags = vec!["_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".into()];
spec.build.flags.replace_cxxflags = vec!["-stdlib=libc++=>-stdlib=libstdc++".into()];
spec.build.flags.replace_ldflags = vec!["-O3=>-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
spec.build.flags.replace_ltoflags = vec!["auto=>thin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "CFLAGS" && v == "-D_FORTIFY_SOURCE=2 -O2 -flto=thin"),
"expected replace_cflags and replace_ltoflags to be applied"
);
assert!(
env.iter()
.any(|(k, v)| { k == "CXXFLAGS" && v == "-O2 -stdlib=libstdc++ -flto=thin" }),
"expected replace_cxxflags to be applied"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,-O2 -flto=thin"),
"expected replace_ldflags to be applied"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=thin"),
"expected replace_ltoflags to affect exported LTOFLAGS"
);
}
#[test]
fn test_standard_build_env_drops_bare_dash_flags() {
let mut spec = mk_spec(vec!["-O2", "-", ""], vec!["-Wl,--as-needed", " "]);
spec.build.flags.cxxflags = vec!["-O2".into(), "-".into(), "-fno-exceptions".into()];
spec.build.flags.ltoflags = vec!["-".into(), "-flto=thin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "CFLAGS" && v == "-O2 -flto=thin"),
"expected bare dash entries to be removed from CFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -fno-exceptions -flto=thin"),
"expected bare dash entries to be removed from CXXFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed -flto=thin"),
"expected blank and bare dash entries to be removed from LDFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=thin"),
"expected bare dash entries to be removed from LTOFLAGS"
);
}
#[test]
fn test_standard_build_env_skips_lto_injection_when_disabled() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
spec.build.flags.rustltoflags = vec!["-Clinker-plugin-lto".into()];
spec.build.flags.use_lto = false;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
"expected CFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter().any(|(k, v)| k == "CXXFLAGS" && v == "-O2"),
"expected CXXFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
"expected LDFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto"),
"expected LTOFLAGS variable to be exported even when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "RUSTLTOFLAGS" && v == "-Clinker-plugin-lto"),
"expected RUSTLTOFLAGS variable to be exported even when use_lto is false"
);
assert_eq!(effective_rustflags(&spec.build.flags), Vec::<String>::new());
}
#[test]
fn test_standard_build_env_exports_makeflags() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.makeflags = "-j12 --output-sync=target".to_string();
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "MAKEFLAGS" && v == "-j12 --output-sync=target"),
"expected MAKEFLAGS to be exported from build flags"
);
}
#[test]
fn test_standard_build_env_exports_install_dir_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.prefix = "/opt/vertex".into();
spec.build.flags.bindir = "/opt/vertex/bin".into();
spec.build.flags.sbindir = "/opt/vertex/sbin".into();
spec.build.flags.libdir = "/opt/vertex/lib".into();
spec.build.flags.libexecdir = "/opt/vertex/libexec".into();
spec.build.flags.sysconfdir = "/etc/vertex".into();
spec.build.flags.localstatedir = "/var".into();
spec.build.flags.sharedstatedir = "/var/lib".into();
spec.build.flags.includedir = "/opt/vertex/include".into();
spec.build.flags.datarootdir = "/opt/vertex/share".into();
spec.build.flags.datadir = "/opt/vertex/share/data".into();
spec.build.flags.mandir = "/opt/vertex/share/man".into();
spec.build.flags.infodir = "/opt/vertex/share/info".into();
let env = standard_build_env(&spec, None, false, true);
assert_eq!(env_value(&env, "PREFIX"), Some("/opt/vertex"));
assert_eq!(env_value(&env, "BINDIR"), Some("/opt/vertex/bin"));
assert_eq!(env_value(&env, "SBINDIR"), Some("/opt/vertex/sbin"));
assert_eq!(env_value(&env, "LIBDIR"), Some("/opt/vertex/lib"));
assert_eq!(env_value(&env, "LIBEXECDIR"), Some("/opt/vertex/libexec"));
assert_eq!(env_value(&env, "SYSCONFDIR"), Some("/etc/vertex"));
assert_eq!(env_value(&env, "LOCALSTATEDIR"), Some("/var"));
assert_eq!(env_value(&env, "SHAREDSTATEDIR"), Some("/var/lib"));
assert_eq!(env_value(&env, "INCLUDEDIR"), Some("/opt/vertex/include"));
assert_eq!(env_value(&env, "DATAROOTDIR"), Some("/opt/vertex/share"));
assert_eq!(env_value(&env, "DATADIR"), Some("/opt/vertex/share/data"));
assert_eq!(env_value(&env, "MANDIR"), Some("/opt/vertex/share/man"));
assert_eq!(env_value(&env, "INFODIR"), Some("/opt/vertex/share/info"));
}
#[test]
fn test_standard_build_env_install_dir_vars_use_effective_defaults() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.lib32_variant = true;
let env = standard_build_env(&spec, None, false, true);
assert_eq!(env_value(&env, "LIBDIR"), Some("/usr/lib32"));
assert_eq!(env_value(&env, "LIBEXECDIR"), Some("/usr/lib32"));
assert_eq!(env_value(&env, "DATAROOTDIR"), Some("/usr/share"));
assert_eq!(env_value(&env, "DATADIR"), Some("/usr/share"));
}
#[test]
fn test_standard_build_env_exports_passthrough_env() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.passthrough_env = vec!["RUSTFLAGS".into()];
let mut env = TestEnv::new();
env.set_var("RUSTFLAGS", "-C target-cpu=native");
let env = standard_build_env(&spec, None, false, true);
assert!(
env.iter()
.any(|(k, v)| k == "RUSTFLAGS" && v == "-C target-cpu=native"),
"expected RUSTFLAGS to be copied from parent environment"
);
}
#[test]
fn test_standard_build_env_exports_declared_env_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.package.version = "2.4.1".to_string();
spec.spec_dir = PathBuf::from("/tmp/specs/demo");
spec.build.flags.env_vars = vec![
"SETUPTOOLS_SCM_PRETEND_VERSION=$version".into(),
"PYO3_CONFIG_FILE=$specdir/pyo3.toml".into(),
"PKG_CONFIG_PATH=$LIBDIR/pkgconfig".into(),
];
let env = standard_build_env(&spec, None, false, true);
assert!(
env.iter()
.any(|(k, v)| k == "SETUPTOOLS_SCM_PRETEND_VERSION" && v == "2.4.1"),
"expected env_vars values to expand package variables"
);
assert!(
env.iter()
.any(|(k, v)| k == "PYO3_CONFIG_FILE" && v == "/tmp/specs/demo/pyo3.toml"),
"expected env_vars values to expand specdir variables"
);
assert!(
env.iter()
.any(|(k, v)| k == "PKG_CONFIG_PATH" && v == "/usr/lib/pkgconfig"),
"expected env_vars values to expand install directory variables"
);
}
#[test]
fn test_standard_build_env_declared_env_vars_override_defaults_and_passthrough() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.cc = "spec-cc".to_string();
spec.build.flags.passthrough_env = vec!["CC".into()];
spec.build.flags.env_vars = vec!["CC=custom-cc".into()];
let mut env = TestEnv::new();
env.set_var("CC", "host-cc");
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CC" && v == "custom-cc"),
"expected explicit env_vars assignments to override default and passthrough values"
);
}
#[test]
fn test_effective_rustflags_applies_replace_rules() {
let flags = BuildFlags {
rustflags: vec!["-C".into(), "debuginfo=2".into()],
replace_rustflags: vec!["debuginfo=2=>opt-level=2".into()],
..BuildFlags::default()
};
assert_eq!(effective_rustflags(&flags), vec!["-C", "opt-level=2"]);
}
#[test]
fn test_effective_rustflags_appends_rustltoflags_when_enabled() {
let flags = BuildFlags {
rustflags: vec!["-C".into(), "opt-level=3".into()],
rustltoflags: vec!["-Clinker-plugin-lto".into(), "-Cembed-bitcode=yes".into()],
use_lto: true,
..BuildFlags::default()
};
assert_eq!(
effective_rustflags(&flags),
vec![
"-C",
"opt-level=3",
"-Clinker-plugin-lto",
"-Cembed-bitcode=yes"
]
);
}
#[test]
fn test_standard_build_env_passthrough_does_not_override_default_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.cc = "spec-cc".to_string();
spec.build.flags.passthrough_env = vec!["CC".into()];
let mut env = TestEnv::new();
env.set_var("CC", "host-cc");
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CC" && v == "spec-cc"),
"expected default CC to take precedence over passthrough CC"
);
}
#[test]
fn test_install_dirs_use_defaults_and_lib32_fallbacks() {
let default_dirs = install_dirs(&BuildFlags::default());
assert_eq!(default_dirs.bindir, "/usr/bin");
assert_eq!(default_dirs.sbindir, "/usr/bin");
assert_eq!(default_dirs.libdir, "/usr/lib");
assert_eq!(default_dirs.libexecdir, "/usr/lib");
assert_eq!(default_dirs.datarootdir, "/usr/share");
assert_eq!(default_dirs.datadir, "/usr/share");
let lib32_dirs = install_dirs(&BuildFlags {
lib32_variant: true,
..BuildFlags::default()
});
assert_eq!(lib32_dirs.libdir, "/usr/lib32");
assert_eq!(lib32_dirs.libexecdir, "/usr/lib32");
}
#[test]
fn test_build_helper_context_restores_runtime_build_flags() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.lib32_variant = true;
spec.build.flags.host_build_dir = Some("/tmp/build-host".into());
let restored = BuildHelperContext::from_spec(&spec).build_flags();
assert!(restored.lib32_variant);
assert_eq!(restored.host_build_dir.as_deref(), Some("/tmp/build-host"));
}
#[test]
fn test_install_dirs_respect_explicit_overrides_and_derived_defaults() {
let dirs = install_dirs(&BuildFlags {
bindir: "/opt/bin".into(),
libdir: "/opt/lib64".into(),
datarootdir: "/opt/share-root".into(),
..BuildFlags::default()
});
assert_eq!(dirs.bindir, "/opt/bin");
assert_eq!(dirs.libdir, "/opt/lib64");
assert_eq!(dirs.libexecdir, "/opt/lib64");
assert_eq!(dirs.datarootdir, "/opt/share-root");
assert_eq!(dirs.datadir, "/opt/share-root");
}
#[test]
fn test_install_destdir_path_uses_build_dir_for_lib32() {
let build_dir = Path::new("/tmp/build");
let destdir = Path::new("/tmp/pkg");
assert_eq!(install_destdir_path(build_dir, destdir, false), destdir);
assert_eq!(
install_destdir_path(build_dir, destdir, true),
build_dir.join("destdir")
);
}
#[test]
fn test_stage_lib32_install_tree_uses_usr_lib32_when_present() -> Result<()> {
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
let dest = temp.path().join("dest");
fs::create_dir_all(staging.join("usr/lib32"))?;
fs::create_dir_all(staging.join("usr/bin"))?;
fs::write(staging.join("usr/lib32/libfoo.so.1"), "lib32")?;
fs::write(staging.join("usr/bin/foo"), "bin")?;
stage_lib32_install_tree(&staging, &dest)?;
assert_eq!(
fs::read_to_string(dest.join("usr/lib32/libfoo.so.1"))?,
"lib32"
);
assert!(!dest.join("usr/bin/foo").exists());
Ok(())
}
#[test]
#[cfg(unix)]
fn test_stage_lib32_install_tree_relocates_usr_lib_when_needed() -> Result<()> {
use std::os::unix::fs as unix_fs;
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
let dest = temp.path().join("dest");
fs::create_dir_all(staging.join("usr/lib"))?;
fs::create_dir_all(staging.join("usr/share/man/man1"))?;
fs::write(staging.join("usr/lib/libfoo.so.1"), "relocated")?;
fs::write(staging.join("usr/share/man/man1/foo.1"), "manpage")?;
unix_fs::symlink("libfoo.so.1", staging.join("usr/lib/libfoo.so"))?;
stage_lib32_install_tree(&staging, &dest)?;
assert_eq!(
fs::read_to_string(dest.join("usr/lib32/libfoo.so.1"))?,
"relocated"
);
assert_eq!(
fs::read_link(dest.join("usr/lib32/libfoo.so"))?,
PathBuf::from("libfoo.so.1")
);
assert!(!dest.join("usr/share/man/man1/foo.1").exists());
assert!(!dest.join("usr/lib").exists());
Ok(())
}
#[test]
fn test_stage_lib32_install_tree_preserves_hardlinks() -> Result<()> {
use std::os::unix::fs::MetadataExt;
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
let dest = temp.path().join("dest");
fs::create_dir_all(staging.join("usr/lib32"))?;
fs::write(staging.join("usr/lib32/libfoo.so.1"), "lib32")?;
fs::hard_link(
staging.join("usr/lib32/libfoo.so.1"),
staging.join("usr/lib32/libfoo-current.so"),
)?;
stage_lib32_install_tree(&staging, &dest)?;
let first = dest.join("usr/lib32/libfoo.so.1").metadata()?;
let second = dest.join("usr/lib32/libfoo-current.so").metadata()?;
assert_eq!(first.ino(), second.ino());
assert_eq!(first.nlink(), 2);
assert_eq!(second.nlink(), 2);
Ok(())
}
}
mod tests;
+2 -2
View File
@@ -212,7 +212,7 @@ pub fn build(
" (with internal fakeroot for build)"
}
);
let mut install_cmd = fakeroot::wrap_install_command(make_exec, destdir);
let mut install_cmd = fakeroot::wrap_install_command(make_exec, destdir)?;
install_cmd.current_dir(&install_dir);
if autotools::make_exec_supports_make_assignments(make_exec)
&& !autotools::has_make_variable_override(&flags.make_install_vars, "DESTDIR")
@@ -343,7 +343,7 @@ pub(crate) fn run_helper_install(
);
for install_dir in install_dirs {
let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir));
let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir))?;
install_cmd.current_dir(&install_dir);
if autotools::make_exec_supports_make_assignments(make_exec)
&& !autotools::has_make_variable_override(&flags.make_install_vars, "DESTDIR")
+912
View File
@@ -0,0 +1,912 @@
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
use crate::test_support::TestEnv;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
fn mk_spec(cflags: Vec<&str>, ldflags: Vec<&str>) -> PackageSpec {
let flags = BuildFlags {
cflags: cflags.into_iter().map(String::from).collect(),
ldflags: ldflags.into_iter().map(String::from).collect(),
..BuildFlags::default()
};
PackageSpec {
package: PackageInfo {
name: "env-test".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![crate::package::Source {
url: "https://example.test/src.tar.gz".into(),
sha256: "abc".into(),
extract_dir: "src".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
fn env_value<'a>(env: &'a EnvVars, key: &str) -> Option<&'a str> {
env.iter()
.find(|(candidate, _)| candidate == key)
.map(|(_, value)| value.as_str())
}
#[test]
fn test_prepare_command() {
let mut cmd = Command::new("ls");
// Set an env var that should be cleared
cmd.env("FORBIDDEN", "value");
// Set PATH manually in the current process to ensure it's picked up if it exists
let mut env = TestEnv::new();
env.set_var("PATH", "/usr/bin");
env.set_var("HOME", "/home/test");
env.set_var("SHELL", "/bin/zsh");
env.set_var("DEPOT_ROOTFS", "/my/rootfs");
env.set_var("TERM", "xterm-256color");
env.set_var("CLICOLOR_FORCE", "1");
prepare_command(&mut cmd, &vec![("MYVAR".to_string(), "myval".to_string())]);
let envs: HashMap<_, _> = cmd.get_envs().collect();
assert!(envs.contains_key(OsStr::new("PATH")));
assert!(envs.contains_key(OsStr::new("HOME")));
assert!(!envs.contains_key(OsStr::new("FORBIDDEN")));
assert_eq!(
envs.get(OsStr::new("SHELL")),
Some(&Some(std::ffi::OsString::from("/bin/sh").as_os_str()))
);
assert_eq!(
envs.get(OsStr::new("MYVAR")),
Some(&Some(std::ffi::OsString::from("myval").as_os_str()))
);
// DEPOT_ROOTFS should be preserved from the parent environment
assert_eq!(
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]
fn test_prepare_command_preserves_destdir() {
let mut cmd = std::process::Command::new("ls");
let mut env = TestEnv::new();
env.set_var("DESTDIR", "/tmp/dest");
prepare_command(&mut cmd, &Vec::new());
let envs: HashMap<_, _> = cmd.get_envs().collect();
assert_eq!(
envs.get(OsStr::new("DESTDIR")),
Some(&Some(std::ffi::OsString::from("/tmp/dest").as_os_str()))
);
}
#[test]
fn test_prepare_command_preserves_rust_toolchain_homes() {
let mut cmd = std::process::Command::new("ls");
let mut env = TestEnv::new();
env.set_var("CARGO_HOME", "/var/cache/cargo-home");
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_build_tool_package_option_maps_supported_builders() {
assert_eq!(
build_tool_package_option(BuildType::Meson),
Some("DEPOT_MESON_PACKAGE")
);
assert_eq!(
build_tool_package_option(BuildType::CMake),
Some("DEPOT_CMAKE_PACKAGE")
);
assert_eq!(build_tool_package_option(BuildType::Bin), None);
}
#[test]
fn test_static_build_args_skip_disable_static_when_no_delete_static_enabled() {
let args = static_build_args_for_request(BuildType::Autotools, Some(false), true);
assert!(args.is_empty());
let args = static_build_args_for_request(BuildType::CMake, Some(false), true);
assert!(args.is_empty());
}
#[test]
fn test_static_build_args_keep_other_requested_modes() {
assert_eq!(
static_build_args_for_request(BuildType::Autotools, Some(false), false),
vec![
"--enable-shared".to_string(),
"--disable-static".to_string()
]
);
assert_eq!(
static_build_args_for_request(BuildType::CMake, Some(false), false),
vec!["-DBUILD_SHARED_LIBS=ON".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Meson, Some(false), false),
vec!["-Ddefault_library=shared".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Perl, Some(false), false),
vec!["LINKTYPE=dynamic".to_string()]
);
assert_eq!(
static_build_args_for_request(BuildType::Meson, Some(true), true),
vec!["-Ddefault_library=static".to_string()]
);
}
#[test]
fn test_standard_build_env_exports_native_linker_and_cpp() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.ranlib = "llvm-ranlib".to_string();
spec.build.flags.strip = "llvm-strip".to_string();
spec.build.flags.ld = "ld.lld".to_string();
spec.build.flags.nm = "llvm-nm".to_string();
spec.build.flags.objcopy = "llvm-objcopy".to_string();
spec.build.flags.objdump = "llvm-objdump".to_string();
spec.build.flags.readelf = "llvm-readelf".to_string();
spec.build.flags.cpp = "clang-cpp".to_string();
let env = standard_build_env(&spec, None, true, true);
assert!(env.iter().any(|(k, v)| k == "RANLIB" && v == "llvm-ranlib"));
assert!(env.iter().any(|(k, v)| k == "STRIP" && v == "llvm-strip"));
assert!(env.iter().any(|(k, v)| k == "LD" && v == "ld.lld"));
assert!(env.iter().any(|(k, v)| k == "NM" && v == "llvm-nm"));
assert!(
env.iter()
.any(|(k, v)| k == "OBJCOPY" && v == "llvm-objcopy")
);
assert!(
env.iter()
.any(|(k, v)| k == "OBJDUMP" && v == "llvm-objdump")
);
assert!(
env.iter()
.any(|(k, v)| k == "READELF" && v == "llvm-readelf")
);
assert!(env.iter().any(|(k, v)| k == "CPP" && v == "clang-cpp"));
}
#[test]
fn test_standard_build_env_exports_tool_dir_and_expands_tool_commands() {
let mut spec = mk_spec(
vec!["--gcc-toolchain=$TOOL_DIR"],
vec!["-B$TOOL_DIR", "-Wl,--as-needed"],
);
spec.build.flags.tool_dir = "/opt/depot-tools/bin".to_string();
spec.build.flags.cc = "$TOOL_DIR/clang".to_string();
spec.build.flags.cxx = "$TOOL_DIR/clang++".to_string();
spec.build.flags.ar = "$TOOL_DIR/llvm-ar".to_string();
spec.build.flags.ranlib = "$TOOL_DIR/llvm-ranlib".to_string();
spec.build.flags.ld = "$TOOL_DIR/ld.lld".to_string();
spec.build.flags.env_vars = vec!["LLVM_CONFIG=$TOOL_DIR/llvm-config".to_string()];
let env = standard_build_env(&spec, None, true, true);
assert_eq!(env_value(&env, "TOOL_DIR"), Some("/opt/depot-tools/bin"));
assert_eq!(env_value(&env, "CC"), Some("/opt/depot-tools/bin/clang"));
assert_eq!(env_value(&env, "CXX"), Some("/opt/depot-tools/bin/clang++"));
assert_eq!(env_value(&env, "AR"), Some("/opt/depot-tools/bin/llvm-ar"));
assert_eq!(
env_value(&env, "RANLIB"),
Some("/opt/depot-tools/bin/llvm-ranlib")
);
assert_eq!(env_value(&env, "LD"), Some("/opt/depot-tools/bin/ld.lld"));
assert_eq!(
env_value(&env, "CFLAGS"),
Some("--gcc-toolchain=/opt/depot-tools/bin")
);
assert_eq!(
env_value(&env, "LDFLAGS"),
Some("-B/opt/depot-tools/bin -Wl,--as-needed")
);
assert_eq!(
env_value(&env, "LLVM_CONFIG"),
Some("/opt/depot-tools/bin/llvm-config")
);
}
#[test]
fn test_standard_build_env_cross_uses_package_tool_overrides() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.cc = "/tools/bin/clang".to_string();
spec.build.flags.cxx = "/tools/bin/clang++".to_string();
spec.build.flags.ar = "/tools/bin/llvm-ar".to_string();
spec.build.flags.ranlib = "/tools/bin/llvm-ranlib".to_string();
spec.build.flags.strip = "/tools/bin/llvm-strip".to_string();
spec.build.flags.ld = "/tools/bin/ld.lld".to_string();
spec.build.flags.nm = "/tools/bin/llvm-nm".to_string();
spec.build.flags.objcopy = "/tools/bin/llvm-objcopy".to_string();
spec.build.flags.objdump = "/tools/bin/llvm-objdump".to_string();
spec.build.flags.readelf = "/tools/bin/llvm-readelf".to_string();
spec.build.flags.cpp = "/tools/bin/clang-cpp".to_string();
let cross = CrossConfig {
prefix: "x86_64-test-linux-gnu".into(),
cc: "x86_64-test-linux-gnu-gcc".into(),
cxx: "x86_64-test-linux-gnu-g++".into(),
ar: "x86_64-test-linux-gnu-ar".into(),
ranlib: "x86_64-test-linux-gnu-ranlib".into(),
strip: "x86_64-test-linux-gnu-strip".into(),
ld: "x86_64-test-linux-gnu-ld".into(),
nm: "x86_64-test-linux-gnu-nm".into(),
objcopy: "x86_64-test-linux-gnu-objcopy".into(),
objdump: "x86_64-test-linux-gnu-objdump".into(),
readelf: "x86_64-test-linux-gnu-readelf".into(),
};
let env = standard_build_env(&spec, Some(&cross), true, true);
assert_eq!(env_value(&env, "CC"), Some("/tools/bin/clang"));
assert_eq!(env_value(&env, "CXX"), Some("/tools/bin/clang++"));
assert_eq!(env_value(&env, "AR"), Some("/tools/bin/llvm-ar"));
assert_eq!(env_value(&env, "RANLIB"), Some("/tools/bin/llvm-ranlib"));
assert_eq!(env_value(&env, "STRIP"), Some("/tools/bin/llvm-strip"));
assert_eq!(env_value(&env, "LD"), Some("/tools/bin/ld.lld"));
assert_eq!(env_value(&env, "NM"), Some("/tools/bin/llvm-nm"));
assert_eq!(env_value(&env, "OBJCOPY"), Some("/tools/bin/llvm-objcopy"));
assert_eq!(env_value(&env, "OBJDUMP"), Some("/tools/bin/llvm-objdump"));
assert_eq!(env_value(&env, "READELF"), Some("/tools/bin/llvm-readelf"));
assert_eq!(env_value(&env, "CPP"), Some("/tools/bin/clang-cpp"));
assert_eq!(
env_value(&env, "CROSS_PREFIX"),
Some("x86_64-test-linux-gnu")
);
}
#[test]
fn test_standard_build_env_exports_effective_carch_for_cross_and_lib32() {
let spec = mk_spec(Vec::new(), Vec::new());
let cross = CrossConfig {
prefix: "aarch64-linux-gnu".into(),
cc: "aarch64-linux-gnu-gcc".into(),
cxx: "aarch64-linux-gnu-g++".into(),
ar: "aarch64-linux-gnu-ar".into(),
ranlib: "aarch64-linux-gnu-ranlib".into(),
strip: "aarch64-linux-gnu-strip".into(),
ld: "aarch64-linux-gnu-ld".into(),
nm: "aarch64-linux-gnu-nm".into(),
objcopy: "aarch64-linux-gnu-objcopy".into(),
objdump: "aarch64-linux-gnu-objdump".into(),
readelf: "aarch64-linux-gnu-readelf".into(),
};
let cross_env = standard_build_env(&spec, Some(&cross), true, true);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "CARCH" && v == "aarch64"),
"expected cross builds to export target CARCH"
);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "OBJCOPY" && v == "aarch64-linux-gnu-objcopy"),
"expected cross builds to export OBJCOPY"
);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "OBJDUMP" && v == "aarch64-linux-gnu-objdump"),
"expected cross builds to export OBJDUMP"
);
assert!(
cross_env
.iter()
.any(|(k, v)| k == "READELF" && v == "aarch64-linux-gnu-readelf"),
"expected cross builds to export READELF"
);
let mut lib32_spec = spec.clone();
lib32_spec.build.flags.lib32_variant = true;
lib32_spec.build.flags.carch = "x86_64".into();
let lib32_env = standard_build_env(&lib32_spec, None, true, true);
assert!(
lib32_env.iter().any(|(k, v)| k == "CARCH" && v == "i686"),
"expected lib32 builds to export i686 CARCH"
);
}
#[test]
fn test_spec_with_target_configure_appends_matching_arch_args() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.configure = vec!["--base".to_string()];
spec.build
.flags
.configure_arch
.insert("aarch64".to_string(), vec!["--for-aarch64".to_string()]);
spec.build
.flags
.configure_arch
.insert("x86_64".to_string(), vec!["--for-x86".to_string()]);
let cross = CrossConfig {
prefix: "aarch64-linux-gnu".into(),
cc: "aarch64-linux-gnu-gcc".into(),
cxx: "aarch64-linux-gnu-g++".into(),
ar: "aarch64-linux-gnu-ar".into(),
ranlib: "aarch64-linux-gnu-ranlib".into(),
strip: "aarch64-linux-gnu-strip".into(),
ld: "aarch64-linux-gnu-ld".into(),
nm: "aarch64-linux-gnu-nm".into(),
objcopy: "aarch64-linux-gnu-objcopy".into(),
objdump: "aarch64-linux-gnu-objdump".into(),
readelf: "aarch64-linux-gnu-readelf".into(),
};
let adjusted = spec_with_target_configure(&spec, Some(&cross), TargetBuildKind::Primary)
.expect("expected arch-specific configure args");
assert_eq!(
adjusted.build.flags.configure,
vec!["--base".to_string(), "--for-aarch64".to_string()]
);
}
#[test]
fn test_spec_with_target_configure_uses_lib32_arch() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.lib32_variant = true;
spec.build.flags.carch = "x86_64".to_string();
spec.build
.flags
.configure_arch
.insert("i686".to_string(), vec!["--for-lib32".to_string()]);
let adjusted = spec_with_target_configure(&spec, None, TargetBuildKind::Lib32)
.expect("expected lib32 configure args");
assert_eq!(
adjusted.build.flags.configure,
vec!["--for-lib32".to_string()]
);
}
#[test]
fn test_standard_build_env_respects_export_compiler_flags_toggle() {
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()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
"expected LDFLAGS to be exported when enabled"
);
let disabled = standard_build_env(&spec, None, true, false);
assert!(
!disabled.iter().any(|(k, _)| k == "CFLAGS"),
"expected CFLAGS to be omitted when disabled"
);
assert!(
!disabled.iter().any(|(k, _)| k == "CXXFLAGS"),
"expected CXXFLAGS to be omitted when disabled"
);
assert!(
!disabled.iter().any(|(k, _)| k == "LDFLAGS"),
"expected LDFLAGS to be omitted when disabled"
);
let mut disabled_by_spec = spec.clone();
disabled_by_spec.build.flags.no_flags = true;
let disabled_env = standard_build_env(&disabled_by_spec, None, true, true);
assert!(
!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"
);
}
#[test]
fn test_standard_build_env_injects_ltoflags_into_compiler_and_linker_flags() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into(), "-fuse-linker-plugin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| { k == "CFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin" }),
"expected LTOFLAGS to be appended to CFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin"),
"expected LTOFLAGS to be appended to CXXFLAGS"
);
assert!(
env.iter().any(|(k, v)| {
k == "LDFLAGS" && v == "-Wl,--as-needed -flto=auto -fuse-linker-plugin"
}),
"expected LTOFLAGS to be appended to LDFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto -fuse-linker-plugin"),
"expected LTOFLAGS variable to be exported"
);
}
#[test]
fn test_standard_build_env_injects_fuse_ld_into_ldflags() {
let mut spec = mk_spec(Vec::new(), vec!["-Wl,--as-needed"]);
spec.build.flags.fuse_ld = "/usr/bin/ld.lld".into();
let env = standard_build_env(&spec, None, true, true);
assert_eq!(
env_value(&env, "LDFLAGS"),
Some("-fuse-ld=/usr/bin/ld.lld -Wl,--as-needed")
);
}
#[test]
fn test_standard_build_env_normalizes_fuse_ld_tool_names() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.fuse_ld = "ld.lld".into();
let env = standard_build_env(&spec, None, true, true);
assert_eq!(env_value(&env, "LDFLAGS"), Some("-fuse-ld=lld"));
}
#[test]
fn test_standard_build_env_applies_replace_flag_rules() {
let mut spec = mk_spec(vec!["-D_FORTIFY_SOURCE=3", "-O2"], vec!["-Wl,-O3"]);
spec.build.flags.cxxflags = vec!["-O2".into(), "-stdlib=libc++".into()];
spec.build.flags.replace_cflags = vec!["_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".into()];
spec.build.flags.replace_cxxflags = vec!["-stdlib=libc++=>-stdlib=libstdc++".into()];
spec.build.flags.replace_ldflags = vec!["-O3=>-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
spec.build.flags.replace_ltoflags = vec!["auto=>thin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "CFLAGS" && v == "-D_FORTIFY_SOURCE=2 -O2 -flto=thin"),
"expected replace_cflags and replace_ltoflags to be applied"
);
assert!(
env.iter()
.any(|(k, v)| { k == "CXXFLAGS" && v == "-O2 -stdlib=libstdc++ -flto=thin" }),
"expected replace_cxxflags to be applied"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,-O2 -flto=thin"),
"expected replace_ldflags to be applied"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=thin"),
"expected replace_ltoflags to affect exported LTOFLAGS"
);
}
#[test]
fn test_standard_build_env_drops_bare_dash_flags() {
let mut spec = mk_spec(vec!["-O2", "-", ""], vec!["-Wl,--as-needed", " "]);
spec.build.flags.cxxflags = vec!["-O2".into(), "-".into(), "-fno-exceptions".into()];
spec.build.flags.ltoflags = vec!["-".into(), "-flto=thin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "CFLAGS" && v == "-O2 -flto=thin"),
"expected bare dash entries to be removed from CFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -fno-exceptions -flto=thin"),
"expected bare dash entries to be removed from CXXFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed -flto=thin"),
"expected blank and bare dash entries to be removed from LDFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=thin"),
"expected bare dash entries to be removed from LTOFLAGS"
);
}
#[test]
fn test_standard_build_env_skips_lto_injection_when_disabled() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
spec.build.flags.rustltoflags = vec!["-Clinker-plugin-lto".into()];
spec.build.flags.use_lto = false;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
"expected CFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter().any(|(k, v)| k == "CXXFLAGS" && v == "-O2"),
"expected CXXFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
"expected LDFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto"),
"expected LTOFLAGS variable to be exported even when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "RUSTLTOFLAGS" && v == "-Clinker-plugin-lto"),
"expected RUSTLTOFLAGS variable to be exported even when use_lto is false"
);
assert_eq!(effective_rustflags(&spec.build.flags), Vec::<String>::new());
}
#[test]
fn test_standard_build_env_exports_makeflags() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.makeflags = "-j12 --output-sync=target".to_string();
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "MAKEFLAGS" && v == "-j12 --output-sync=target"),
"expected MAKEFLAGS to be exported from build flags"
);
}
#[test]
fn test_standard_build_env_exports_install_dir_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.prefix = "/opt/vertex".into();
spec.build.flags.bindir = "/opt/vertex/bin".into();
spec.build.flags.sbindir = "/opt/vertex/sbin".into();
spec.build.flags.libdir = "/opt/vertex/lib".into();
spec.build.flags.libexecdir = "/opt/vertex/libexec".into();
spec.build.flags.sysconfdir = "/etc/vertex".into();
spec.build.flags.localstatedir = "/var".into();
spec.build.flags.sharedstatedir = "/var/lib".into();
spec.build.flags.includedir = "/opt/vertex/include".into();
spec.build.flags.datarootdir = "/opt/vertex/share".into();
spec.build.flags.datadir = "/opt/vertex/share/data".into();
spec.build.flags.mandir = "/opt/vertex/share/man".into();
spec.build.flags.infodir = "/opt/vertex/share/info".into();
let env = standard_build_env(&spec, None, false, true);
assert_eq!(env_value(&env, "PREFIX"), Some("/opt/vertex"));
assert_eq!(env_value(&env, "BINDIR"), Some("/opt/vertex/bin"));
assert_eq!(env_value(&env, "SBINDIR"), Some("/opt/vertex/sbin"));
assert_eq!(env_value(&env, "LIBDIR"), Some("/opt/vertex/lib"));
assert_eq!(env_value(&env, "LIBEXECDIR"), Some("/opt/vertex/libexec"));
assert_eq!(env_value(&env, "SYSCONFDIR"), Some("/etc/vertex"));
assert_eq!(env_value(&env, "LOCALSTATEDIR"), Some("/var"));
assert_eq!(env_value(&env, "SHAREDSTATEDIR"), Some("/var/lib"));
assert_eq!(env_value(&env, "INCLUDEDIR"), Some("/opt/vertex/include"));
assert_eq!(env_value(&env, "DATAROOTDIR"), Some("/opt/vertex/share"));
assert_eq!(env_value(&env, "DATADIR"), Some("/opt/vertex/share/data"));
assert_eq!(env_value(&env, "MANDIR"), Some("/opt/vertex/share/man"));
assert_eq!(env_value(&env, "INFODIR"), Some("/opt/vertex/share/info"));
}
#[test]
fn test_standard_build_env_install_dir_vars_use_effective_defaults() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.lib32_variant = true;
let env = standard_build_env(&spec, None, false, true);
assert_eq!(env_value(&env, "LIBDIR"), Some("/usr/lib32"));
assert_eq!(env_value(&env, "LIBEXECDIR"), Some("/usr/lib32"));
assert_eq!(env_value(&env, "DATAROOTDIR"), Some("/usr/share"));
assert_eq!(env_value(&env, "DATADIR"), Some("/usr/share"));
}
#[test]
fn test_standard_build_env_exports_passthrough_env() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.passthrough_env = vec!["RUSTFLAGS".into()];
let mut env = TestEnv::new();
env.set_var("RUSTFLAGS", "-C target-cpu=native");
let env = standard_build_env(&spec, None, false, true);
assert!(
env.iter()
.any(|(k, v)| k == "RUSTFLAGS" && v == "-C target-cpu=native"),
"expected RUSTFLAGS to be copied from parent environment"
);
}
#[test]
fn test_standard_build_env_exports_declared_env_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.package.version = "2.4.1".to_string();
spec.spec_dir = PathBuf::from("/tmp/specs/demo");
spec.build.flags.env_vars = vec![
"SETUPTOOLS_SCM_PRETEND_VERSION=$version".into(),
"PYO3_CONFIG_FILE=$specdir/pyo3.toml".into(),
"PKG_CONFIG_PATH=$LIBDIR/pkgconfig".into(),
];
let env = standard_build_env(&spec, None, false, true);
assert!(
env.iter()
.any(|(k, v)| k == "SETUPTOOLS_SCM_PRETEND_VERSION" && v == "2.4.1"),
"expected env_vars values to expand package variables"
);
assert!(
env.iter()
.any(|(k, v)| k == "PYO3_CONFIG_FILE" && v == "/tmp/specs/demo/pyo3.toml"),
"expected env_vars values to expand specdir variables"
);
assert!(
env.iter()
.any(|(k, v)| k == "PKG_CONFIG_PATH" && v == "/usr/lib/pkgconfig"),
"expected env_vars values to expand install directory variables"
);
}
#[test]
fn test_standard_build_env_declared_env_vars_override_defaults_and_passthrough() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.cc = "spec-cc".to_string();
spec.build.flags.passthrough_env = vec!["CC".into()];
spec.build.flags.env_vars = vec!["CC=custom-cc".into()];
let mut env = TestEnv::new();
env.set_var("CC", "host-cc");
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CC" && v == "custom-cc"),
"expected explicit env_vars assignments to override default and passthrough values"
);
}
#[test]
fn test_effective_rustflags_applies_replace_rules() {
let flags = BuildFlags {
rustflags: vec!["-C".into(), "debuginfo=2".into()],
replace_rustflags: vec!["debuginfo=2=>opt-level=2".into()],
..BuildFlags::default()
};
assert_eq!(effective_rustflags(&flags), vec!["-C", "opt-level=2"]);
}
#[test]
fn test_effective_rustflags_appends_rustltoflags_when_enabled() {
let flags = BuildFlags {
rustflags: vec!["-C".into(), "opt-level=3".into()],
rustltoflags: vec!["-Clinker-plugin-lto".into(), "-Cembed-bitcode=yes".into()],
use_lto: true,
..BuildFlags::default()
};
assert_eq!(
effective_rustflags(&flags),
vec![
"-C",
"opt-level=3",
"-Clinker-plugin-lto",
"-Cembed-bitcode=yes"
]
);
}
#[test]
fn test_standard_build_env_passthrough_does_not_override_default_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.cc = "spec-cc".to_string();
spec.build.flags.passthrough_env = vec!["CC".into()];
let mut env = TestEnv::new();
env.set_var("CC", "host-cc");
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CC" && v == "spec-cc"),
"expected default CC to take precedence over passthrough CC"
);
}
#[test]
fn test_install_dirs_use_defaults_and_lib32_fallbacks() {
let default_dirs = install_dirs(&BuildFlags::default());
assert_eq!(default_dirs.bindir, "/usr/bin");
assert_eq!(default_dirs.sbindir, "/usr/bin");
assert_eq!(default_dirs.libdir, "/usr/lib");
assert_eq!(default_dirs.libexecdir, "/usr/lib");
assert_eq!(default_dirs.datarootdir, "/usr/share");
assert_eq!(default_dirs.datadir, "/usr/share");
let lib32_dirs = install_dirs(&BuildFlags {
lib32_variant: true,
..BuildFlags::default()
});
assert_eq!(lib32_dirs.libdir, "/usr/lib32");
assert_eq!(lib32_dirs.libexecdir, "/usr/lib32");
}
#[test]
fn test_build_helper_context_restores_runtime_build_flags() {
let mut spec = mk_spec(Vec::new(), Vec::new());
spec.build.flags.lib32_variant = true;
spec.build.flags.host_build_dir = Some("/tmp/build-host".into());
let restored = BuildHelperContext::from_spec(&spec).build_flags();
assert!(restored.lib32_variant);
assert_eq!(restored.host_build_dir.as_deref(), Some("/tmp/build-host"));
}
#[test]
fn test_install_dirs_respect_explicit_overrides_and_derived_defaults() {
let dirs = install_dirs(&BuildFlags {
bindir: "/opt/bin".into(),
libdir: "/opt/lib64".into(),
datarootdir: "/opt/share-root".into(),
..BuildFlags::default()
});
assert_eq!(dirs.bindir, "/opt/bin");
assert_eq!(dirs.libdir, "/opt/lib64");
assert_eq!(dirs.libexecdir, "/opt/lib64");
assert_eq!(dirs.datarootdir, "/opt/share-root");
assert_eq!(dirs.datadir, "/opt/share-root");
}
#[test]
fn test_install_destdir_path_uses_build_dir_for_lib32() {
let build_dir = Path::new("/tmp/build");
let destdir = Path::new("/tmp/pkg");
assert_eq!(install_destdir_path(build_dir, destdir, false), destdir);
assert_eq!(
install_destdir_path(build_dir, destdir, true),
build_dir.join("destdir")
);
}
#[test]
fn test_stage_lib32_install_tree_uses_usr_lib32_when_present() -> Result<()> {
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
let dest = temp.path().join("dest");
fs::create_dir_all(staging.join("usr/lib32"))?;
fs::create_dir_all(staging.join("usr/bin"))?;
fs::write(staging.join("usr/lib32/libfoo.so.1"), "lib32")?;
fs::write(staging.join("usr/bin/foo"), "bin")?;
stage_lib32_install_tree(&staging, &dest)?;
assert_eq!(
fs::read_to_string(dest.join("usr/lib32/libfoo.so.1"))?,
"lib32"
);
assert!(!dest.join("usr/bin/foo").exists());
Ok(())
}
#[test]
#[cfg(unix)]
fn test_stage_lib32_install_tree_relocates_usr_lib_when_needed() -> Result<()> {
use std::os::unix::fs as unix_fs;
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
let dest = temp.path().join("dest");
fs::create_dir_all(staging.join("usr/lib"))?;
fs::create_dir_all(staging.join("usr/share/man/man1"))?;
fs::write(staging.join("usr/lib/libfoo.so.1"), "relocated")?;
fs::write(staging.join("usr/share/man/man1/foo.1"), "manpage")?;
unix_fs::symlink("libfoo.so.1", staging.join("usr/lib/libfoo.so"))?;
stage_lib32_install_tree(&staging, &dest)?;
assert_eq!(
fs::read_to_string(dest.join("usr/lib32/libfoo.so.1"))?,
"relocated"
);
assert_eq!(
fs::read_link(dest.join("usr/lib32/libfoo.so"))?,
PathBuf::from("libfoo.so.1")
);
assert!(!dest.join("usr/share/man/man1/foo.1").exists());
assert!(!dest.join("usr/lib").exists());
Ok(())
}
#[test]
fn test_stage_lib32_install_tree_preserves_hardlinks() -> Result<()> {
use std::os::unix::fs::MetadataExt;
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
let dest = temp.path().join("dest");
fs::create_dir_all(staging.join("usr/lib32"))?;
fs::write(staging.join("usr/lib32/libfoo.so.1"), "lib32")?;
fs::hard_link(
staging.join("usr/lib32/libfoo.so.1"),
staging.join("usr/lib32/libfoo-current.so"),
)?;
stage_lib32_install_tree(&staging, &dest)?;
let first = dest.join("usr/lib32/libfoo.so.1").metadata()?;
let second = dest.join("usr/lib32/libfoo-current.so").metadata()?;
assert_eq!(first.ino(), second.ino());
assert_eq!(first.nlink(), 2);
assert_eq!(second.nlink(), 2);
Ok(())
}
+6 -2226
View File
File diff suppressed because it is too large Load Diff
+857
View File
@@ -0,0 +1,857 @@
use super::*;
pub(super) fn is_archive_install_request(spec_path: &Path) -> bool {
spec_path.exists()
&& spec_path
.to_string_lossy()
.to_ascii_lowercase()
.ends_with(".tar.zst")
}
pub(super) fn shared_local_sibling_root(spec_paths: &[PathBuf]) -> Option<PathBuf> {
let mut roots = spec_paths.iter().filter_map(|path| {
path.parent()
.and_then(|p| p.parent())
.map(Path::to_path_buf)
});
let first = roots.next()?;
if roots.all(|path| path == first) {
Some(first)
} else {
None
}
}
#[derive(Clone, Copy)]
pub(super) struct DirectInstallOptions<'a> {
pub(super) rootfs: &'a Path,
pub(super) no_deps: bool,
pub(super) no_flags: bool,
pub(super) cross_prefix: Option<&'a str>,
pub(super) clean: bool,
pub(super) dry_run: bool,
pub(super) lib32_only: bool,
pub(super) install_test_deps: bool,
}
pub(super) fn run_direct_archive_install_requests(
options: DirectInstallOptions<'_>,
config: &config::Config,
archive_paths: &[PathBuf],
confirm_installation: bool,
) -> Result<bool> {
if archive_paths.is_empty() {
return Ok(false);
}
let mut install_lock = locking::open_lock(config)?;
let install_lock_path = locking::lock_path(config);
let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "install")?;
let mut staged_dirs = Vec::with_capacity(archive_paths.len());
let mut pkg_specs = Vec::with_capacity(archive_paths.len());
let mut install_targets = Vec::with_capacity(archive_paths.len());
let suppress_output = suppress_nested_install_output();
for archive_path in archive_paths {
if !suppress_output {
ui::info(format!(
"Installing package from: {}",
archive_path.display()
));
}
let (pkg_spec, staging_dir) = load_package_archive_into_staging(config, archive_path)?;
if options.lib32_only {
anyhow::bail!("--lib32-only is only supported when installing from a package spec");
}
install_targets.push(format!(
"{} v{}-{}",
pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision
));
pkg_specs.push(pkg_spec);
staged_dirs.push(staging_dir);
}
let mut conflict_subjects = Vec::new();
for pkg_spec in &pkg_specs {
conflict_subjects.extend(install_conflict_subjects_for_spec(pkg_spec, true, false));
}
resolve_installed_conflicts_for_subjects(
&conflict_subjects,
options.rootfs,
config,
options.dry_run,
)?;
if options.dry_run {
ui::info("Dry run enabled, stopping before install/build work.");
return Ok(false);
}
if confirm_installation
&& !suppress_output
&& !ui::prompt_package_action("installation", &install_targets, true)?
{
anyhow::bail!("Aborted");
}
if !suppress_output {
ui::info(format!(
"Installing {} binary archive payload(s)",
archive_paths.len()
));
}
let mut transaction_plans = Vec::new();
for (pkg_spec, staging_dir) in pkg_specs.iter().zip(staged_dirs.iter()) {
let output_plans =
plan_package_outputs_for_install(pkg_spec, staging_dir.path(), options.rootfs, config)?;
transaction_plans.extend(output_plans);
}
install_direct_transaction(&transaction_plans, options.rootfs, config)?;
Ok(true)
}
pub(super) struct SourceBuildCleanupGuard<'a> {
pub(super) config: &'a config::Config,
pub(super) enabled: bool,
}
impl<'a> SourceBuildCleanupGuard<'a> {
fn new(config: &'a config::Config, enabled: bool) -> Self {
Self { config, enabled }
}
}
impl Drop for SourceBuildCleanupGuard<'_> {
fn drop(&mut self) {
if self.enabled
&& let Err(err) = clean_build_source_dirs(self.config)
{
crate::log_warn!("Failed to clean build source dirs: {}", err);
}
}
}
pub(super) fn prepare_direct_install_request<'a>(
options: DirectInstallOptions<'_>,
config: &'a config::Config,
spec_path: &Path,
preparation: DirectInstallPreparationOptions<'_>,
) -> Result<PreparedDirectInstall<'a>> {
let (mut pkg_spec, staging_dir): (package::PackageSpec, Option<tempfile::TempDir>) =
if spec_path.to_string_lossy().ends_with(".tar.zst") {
let (spec, tmp_dir) = load_package_archive_into_staging(config, spec_path)?;
(spec, Some(tmp_dir))
} else {
let mut pkg_spec = package::PackageSpec::from_file(spec_path)?;
pkg_spec.apply_config(config);
pkg_spec.build.flags.rootfs = build_cmd::build_env_rootfs(options.rootfs);
(pkg_spec, None)
};
let built_from_source = staging_dir.is_none();
let source_cleanup_guard = SourceBuildCleanupGuard::new(config, built_from_source);
if options.lib32_only && staging_dir.is_some() {
anyhow::bail!("--lib32-only is only supported when installing from a package spec");
}
let lib32_only = effective_lib32_only(&pkg_spec, options.lib32_only);
if staging_dir.is_none() && !preparation.suppress_output {
ui::info(format!(
"Package: {} v{}-{}",
pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision
));
}
let requested_outputs = requested_outputs(&pkg_spec, lib32_only);
let db_path = config.installed_db_path(options.rootfs);
if staging_dir.is_none() {
source::preflight_local_manual_sources(&pkg_spec)?;
if !pkg_spec.is_metapackage() {
ensure_requested_development_package_installed(&db_path)?;
}
}
let mut conflict_subjects = install_conflict_subjects_for_spec(
&pkg_spec,
!lib32_only,
staging_dir.is_none() && (lib32_only || pkg_spec.builds_lib32_output()),
);
if staging_dir.is_some() {
conflict_subjects = install_conflict_subjects_for_spec(&pkg_spec, true, false);
}
if preparation.resolve_installed_conflicts {
resolve_installed_conflicts_for_subjects(
&conflict_subjects,
options.rootfs,
config,
options.dry_run,
)?;
}
if options.dry_run {
ui::info("Dry run enabled, stopping before install/build work.");
return Ok(PreparedDirectInstall {
plans: Vec::new(),
resources: PreparedDirectInstallResources {
_staging_dir: staging_dir,
_source_cleanup_guard: source_cleanup_guard,
},
});
}
let install_targets = vec![format!(
"{} v{}-{}",
pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision
)];
if preparation.confirm_installation
&& !preparation.suppress_output
&& !ui::prompt_package_action("installation", &install_targets, true)?
{
anyhow::bail!("Aborted");
}
std::fs::create_dir_all(&config.db_dir).with_context(|| {
format!(
"Failed to create database directory: {}",
config.db_dir.display()
)
})?;
if staging_dir.is_none() {
if options.no_deps
&& should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs)
{
let missing_test =
deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if !missing_test.is_empty()
&& !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies are missing",
)?
{
anyhow::bail!("Missing test dependencies: {}", missing_test.join(", "));
}
} else if options.no_deps
|| !should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs)
{
maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path, requested_outputs)?;
}
}
if !options.no_deps {
let missing_required = merge_missing_dependencies(
deps::check_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?,
deps::check_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?,
);
if !missing_required.is_empty() {
let dep_chain = std::env::var("DEPOT_DEPCHAIN").unwrap_or_default();
let chain_set: std::collections::HashSet<&str> =
dep_chain.split(',').filter(|s| !s.is_empty()).collect();
if chain_set.contains(pkg_spec.package.name.as_str()) {
anyhow::bail!(
"Dependency cycle detected! {} is already in chain: {}",
pkg_spec.package.name,
dep_chain
);
}
ui::warn(format!(
"Missing dependencies: {}",
missing_required.join(", ")
));
let local_sibling_root = spec_path.parent().and_then(|path| path.parent());
let dep_plan = planner::build_dependency_install_plan(
config,
options.rootfs,
&missing_required,
planner::PlannerOptions {
assume_yes: ui::assume_yes_enabled(),
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root: local_sibling_root.map(Path::to_path_buf),
include_test_deps: options.install_test_deps,
lib32_only_requested_specs: false,
},
)?;
let dep_plan_packages = actionable_plan_packages(&dep_plan);
warn_source_build_plan(&dep_plan);
let dep_prompt_packages = if dep_plan_packages.is_empty() {
missing_required.clone()
} else {
dep_plan_packages
};
if ui::prompt_package_action("dependency installation", &dep_prompt_packages, true)? {
let pkg_index =
index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
let new_chain = if dep_chain.is_empty() {
pkg_spec.package.name.clone()
} else {
format!("{},{}", dep_chain, pkg_spec.package.name)
};
let mut dep_spec_paths = Vec::new();
for dep in missing_required {
let candidate = pkg_index.find(&dep);
if let Some(dep_spec_path) = candidate {
dep_spec_paths.push(dep_spec_path);
} else {
anyhow::bail!("Could not find package spec for dependency: {}", dep);
}
}
ui::info(format!(
"Installing dependencies: {}",
install_request_display(&dep_spec_paths)
));
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
run_install_command_with_program(
&exe,
&dep_spec_paths,
options.rootfs,
ChildInstallCommandOptions {
no_deps: options.no_deps,
assume_yes: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
lib32_only: false,
install_test_deps: options.install_test_deps,
install_context: Some(INSTALL_CONTEXT_PLANNED),
dep_chain: Some(&new_chain),
},
)?;
}
}
deps::require_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
deps::require_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) {
let missing_test =
deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if !missing_test.is_empty() {
let pkg_index =
index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
let mut dep_spec_paths = Vec::new();
let mut unavailable_test = Vec::new();
for dep in &missing_test {
if let Some(dep_spec_path) = pkg_index.find(dep) {
dep_spec_paths.push(dep_spec_path);
} else {
unavailable_test.push(dep.clone());
}
}
if !unavailable_test.is_empty()
&& !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&unavailable_test,
"Requested test dependencies could not be resolved",
)?
{
anyhow::bail!("Missing test dependencies: {}", unavailable_test.join(", "));
}
if !automatic_tests_disabled_for_outputs(&pkg_spec, requested_outputs)
&& !dep_spec_paths.is_empty()
{
ui::warn(format!(
"Missing test dependencies: {}",
missing_test.join(", ")
));
let local_sibling_root = spec_path.parent().and_then(|path| path.parent());
let dep_plan = planner::build_dependency_install_plan(
config,
options.rootfs,
&missing_test,
planner::PlannerOptions {
assume_yes: ui::assume_yes_enabled(),
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root: local_sibling_root.map(Path::to_path_buf),
include_test_deps: options.install_test_deps,
lib32_only_requested_specs: false,
},
)?;
let dep_plan_packages = actionable_plan_packages(&dep_plan);
warn_source_build_plan(&dep_plan);
let dep_prompt_packages = if dep_plan_packages.is_empty() {
missing_test.clone()
} else {
dep_plan_packages
};
if ui::prompt_package_action(
"dependency installation",
&dep_prompt_packages,
true,
)? {
ui::info(format!(
"Installing test dependencies: {}",
install_request_display(&dep_spec_paths)
));
let exe =
std::env::current_exe().context("Failed to locate depot executable")?;
run_install_command_with_program(
&exe,
&dep_spec_paths,
options.rootfs,
ChildInstallCommandOptions {
no_deps: options.no_deps,
assume_yes: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
lib32_only: false,
install_test_deps: options.install_test_deps,
install_context: Some(INSTALL_CONTEXT_PLANNED),
dep_chain: None,
},
)?;
} else if !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies were not installed",
)? {
anyhow::bail!("Aborted");
}
}
}
}
if should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) {
let missing_test =
deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if !missing_test.is_empty()
&& !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies are still missing",
)?
{
deps::require_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
}
}
}
let cross_config = options
.cross_prefix
.map(cross::CrossConfig::from_prefix)
.transpose()?;
let mut built_src_dir: Option<PathBuf> = None;
let destdir = if let Some(dir) = &staging_dir {
dir.path().to_path_buf()
} else {
if preparation.clean_sources_before_build {
clean_build_source_dirs(config)?;
}
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, preparation.build_dir)?;
built_src_dir = Some(src_dir.clone());
let host_build_dir = builder::ensure_host_build(
&pkg_spec,
&src_dir,
cross_config.as_ref(),
!options.no_flags,
builder::TargetBuildKind::Primary,
)?;
if let Some(host_dir) = host_build_dir.as_ref() {
pkg_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned());
}
let destdir = preparation
.build_dir
.join("destdir")
.join(&pkg_spec.package.name);
if destdir.exists() {
fs::remove_dir_all(&destdir)
.with_context(|| format!("Failed to clean destdir: {}", destdir.display()))?;
}
if !lib32_only {
builder::build(
&pkg_spec,
&src_dir,
&destdir,
cross_config.as_ref(),
!options.no_flags,
host_build_dir.as_deref(),
)?;
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?;
builder::stage_generated_lifecycle_scripts(&pkg_spec, &destdir)?;
}
destdir
};
let mut transaction_plans = Vec::new();
if !lib32_only {
if staging_dir.is_none() {
staging::process(&destdir, &pkg_spec)?;
if let Some(src_dir) = built_src_dir.as_deref() {
staging::stage_split_package_licenses(src_dir, &destdir, &pkg_spec)?;
}
} else if !preparation.suppress_output {
ui::info("Installing binary archive payload");
}
let output_plans =
plan_package_outputs_for_install(&pkg_spec, &destdir, options.rootfs, config)?;
transaction_plans.extend(output_plans);
}
if let Some(src_dir) = built_src_dir.as_deref()
&& let Some((lib32_spec, lib32_destdir)) = build_lib32_companion_package(
&pkg_spec,
src_dir,
config,
cross_config.as_ref(),
!options.no_flags,
lib32_only,
)?
{
let staged = plan_staged_install(&lib32_spec, &lib32_destdir, options.rootfs, config)?;
transaction_plans.push(PlannedPackageInstall {
spec: lib32_spec,
destdir: lib32_destdir,
staged,
});
}
Ok(PreparedDirectInstall {
plans: transaction_plans,
resources: PreparedDirectInstallResources {
_staging_dir: staging_dir,
_source_cleanup_guard: source_cleanup_guard,
},
})
}
pub(super) fn install_direct_transaction(
plans: &[PlannedPackageInstall],
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
let ordered_plans = preflight_file_ownership_and_order(plans, &HashSet::new(), rootfs, config)?;
run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Pre, &ordered_plans)?;
install_preflighted_planned_packages_to_rootfs_with_pre_removed(
&ordered_plans,
rootfs,
config,
&HashSet::new(),
true,
)?;
run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Post, &ordered_plans)?;
Ok(())
}
pub(super) fn install_requests_for_plan(
plan: &planner::ExecutionPlan,
config: &config::Config,
rootfs: &Path,
) -> Result<Vec<PathBuf>> {
let mut requests = Vec::new();
for step in plan.actionable_steps() {
match &step.origin {
planner::PlanOrigin::Source { path, .. } => {
requests.push(path.clone());
}
planner::PlanOrigin::Binary { repo_name, record } => {
let repo_cfg = config
.binary_repos
.get(repo_name)
.with_context(|| format!("Binary repo '{}' not found in config", repo_name))?;
let archive = db::repo::fetch_binary_package_archive(
repo_name,
repo_cfg,
rootfs,
record,
&config.package_cache_dir,
)
.with_context(|| {
format!(
"Failed to fetch binary package '{}' from repo '{}'",
record.filename, repo_name
)
})?;
requests.push(archive);
}
planner::PlanOrigin::Installed => {}
}
}
Ok(requests)
}
pub(super) fn planned_installed_removals(
rootfs: &Path,
config: &config::Config,
packages: impl IntoIterator<Item = String>,
) -> Result<Vec<PlannedInstalledRemoval>> {
let db_path = config.installed_db_path(rootfs);
let installed = db::get_installed_packages(&db_path)?;
let mut unique = BTreeSet::new();
for package in packages {
if installed.contains(&package) {
unique.insert(package);
}
}
unique
.into_iter()
.map(|package| {
let affected_paths = db::get_package_files(&db_path, &package)?;
Ok(PlannedInstalledRemoval {
package,
affected_paths,
})
})
.collect()
}
pub(super) fn transaction_contexts_for_update(
removals: &[PlannedInstalledRemoval],
plans: &[PlannedPackageInstall],
) -> Vec<install::hooks::HookExecutionContextOwned> {
let mut contexts = Vec::with_capacity(removals.len() + plans.len());
contexts.extend(
removals
.iter()
.map(|removal| install::hooks::HookExecutionContextOwned {
operation: install::hooks::HookOperation::Remove,
package: removal.package.clone(),
affected_paths: removal.affected_paths.clone(),
}),
);
contexts.extend(plans.iter().map(|plan| plan.staged.hook_context.clone()));
contexts
}
pub(super) fn install_update_transaction(
plans: &[PlannedPackageInstall],
removals: &[PlannedInstalledRemoval],
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
let pre_removed_packages: HashSet<String> = removals
.iter()
.map(|removal| removal.package.clone())
.collect();
let ordered_plans =
preflight_file_ownership_and_order(plans, &pre_removed_packages, rootfs, config)?;
let contexts = transaction_contexts_for_update(removals, &ordered_plans);
install::hooks::run_transaction_hooks_batch(rootfs, install::hooks::HookPhase::Pre, &contexts)?;
for removal in removals {
remove_installed_package_without_transaction_hooks(
&removal.package,
rootfs,
config,
&removal.affected_paths,
)?;
}
install_preflighted_planned_packages_to_rootfs_with_pre_removed(
&ordered_plans,
rootfs,
config,
&pre_removed_packages,
false,
)?;
install::hooks::run_transaction_hooks_batch(
rootfs,
install::hooks::HookPhase::Post,
&contexts,
)?;
Ok(())
}
pub(super) fn run_direct_install_request(
options: DirectInstallOptions<'_>,
config: &config::Config,
mut spec_path: PathBuf,
) -> Result<bool> {
let mut install_lock = locking::open_lock(config)?;
let install_lock_path = locking::lock_path(config);
let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "install")?;
// Repo clone dir is available via `config.repo_clone_dir` and
// is passed explicitly to index builders below.
// If the provided path doesn't exist, treat it as a package name and
// try to locate a spec under configured repo dir or local packages/.
if !spec_path.exists() {
let name = spec_path.to_string_lossy().to_string();
ui::info(format!("Looking up package '{}' in local indexes...", name));
let pkg_index =
index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
if let Some(found) = pkg_index.find(&name) {
spec_path = found;
} else {
let host_arch = std::env::consts::ARCH;
let mut binary_repos: Vec<_> = config
.binary_repos
.iter()
.filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch))
.collect();
binary_repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0)));
for (repo_name, repo_cfg) in binary_repos {
match db::repo::find_binary_repo_package(
repo_name,
repo_cfg,
options.rootfs,
&config.package_cache_dir,
&name,
) {
Ok(Some(rec)) => {
let archive = db::repo::fetch_binary_package_archive(
repo_name,
repo_cfg,
options.rootfs,
&rec,
&config.package_cache_dir,
)?;
ui::info(format!(
"Resolved '{}' from binary repo '{}' as {}-{} (package {}) ({} bytes){} -> {}",
name,
repo_name,
rec.version,
rec.revision,
rec.name,
rec.size,
rec.description
.as_ref()
.map(|d| format!(" [{}]", d))
.unwrap_or_default(),
archive.display()
));
spec_path = archive;
break;
}
Ok(None) => {}
Err(e) => {
crate::log_warn!("Binary repo '{}': {}", repo_name, e);
}
}
}
}
}
let suppress_output = suppress_nested_install_output();
if !suppress_output {
ui::info(format!("Installing package from: {}", spec_path.display()));
}
let _snapper_pre_install_snapshot_todo: fn() -> ! =
|| todo!("snapper: create pre-install snapshot before install work starts");
let _snapper_post_install_snapshot_todo: fn() -> ! =
|| todo!("snapper: create post-install snapshot after install commit succeeds");
let prepared = prepare_direct_install_request(
options,
config,
&spec_path,
DirectInstallPreparationOptions {
build_dir: &config.build_dir,
clean_sources_before_build: true,
suppress_output,
confirm_installation: true,
resolve_installed_conflicts: true,
},
)?;
if options.dry_run {
return Ok(false);
}
let _resources = prepared.resources;
install_direct_transaction(&prepared.plans, options.rootfs, config)?;
Ok(true)
}
pub(super) fn isolated_update_build_dir(config: &config::Config, idx: usize) -> PathBuf {
config
.build_dir
.join("update-tx")
.join(format!("{:04}", idx + 1))
}
pub(super) fn run_update_transaction_install_requests(
options: DirectInstallOptions<'_>,
config: &config::Config,
requests: &[PathBuf],
) -> Result<bool> {
if requests.is_empty() {
return Ok(false);
}
let mut install_lock = locking::open_lock(config)?;
let install_lock_path = locking::lock_path(config);
let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "update")?;
if requests
.iter()
.any(|request| !is_archive_install_request(request))
{
clean_build_source_dirs(config)?;
}
let mut transaction_plans = Vec::new();
let mut resources = Vec::with_capacity(requests.len());
for (idx, request) in requests.iter().enumerate() {
let build_dir = isolated_update_build_dir(config, idx);
let prepared = prepare_direct_install_request(
options,
config,
request,
DirectInstallPreparationOptions {
build_dir: &build_dir,
clean_sources_before_build: false,
suppress_output: true,
confirm_installation: false,
resolve_installed_conflicts: false,
},
)
.with_context(|| {
format!(
"Failed to prepare update payload from {}",
request.display()
)
})?;
transaction_plans.extend(prepared.plans);
resources.push(prepared.resources);
}
if options.dry_run {
return Ok(false);
}
let conflict_subjects: Vec<_> = transaction_plans
.iter()
.flat_map(|plan| install_conflict_subjects_for_output_spec(&plan.spec))
.collect();
validate_no_transaction_conflicts(&conflict_subjects)?;
let mut removal_packages = prompt_installed_conflict_removals_for_subjects(
&conflict_subjects,
options.rootfs,
config,
false,
)?;
for plan in &transaction_plans {
removal_packages.extend(plan.staged.replacement_removals.iter().cloned());
}
let removals = planned_installed_removals(options.rootfs, config, removal_packages)?;
install_update_transaction(&transaction_plans, &removals, options.rootfs, config)?;
drop(resources);
Ok(true)
}
+550
View File
@@ -0,0 +1,550 @@
use super::*;
pub(super) fn print_plan_summary(plan: &planner::ExecutionPlan) {
if std::env::var_os("DEPOT_VERBOSE_PLAN").is_none() {
return;
}
for step in &plan.steps {
let (action, origin) = match &step.action {
planner::PlanAction::SkipInstalled => ("skip", "installed".to_string()),
planner::PlanAction::BuildAndInstall => match &step.origin {
planner::PlanOrigin::Source {
path,
local_sibling,
} => (
"build+install",
if *local_sibling {
format!("source:local-sibling ({})", path.display())
} else {
format!("source ({})", path.display())
},
),
_ => ("build+install", "source".to_string()),
},
planner::PlanAction::InstallBinary => match &step.origin {
planner::PlanOrigin::Binary { repo_name, record } => (
"install",
format!(
"binary:{} {}-{} size={}",
repo_name,
record.version,
record.revision,
human_bytes(record.size)
),
),
_ => ("install", "binary".to_string()),
},
};
ui::info(format!(" {} [{}] {}", step.package, action, origin));
}
}
pub(super) fn actionable_plan_packages(plan: &planner::ExecutionPlan) -> Vec<String> {
plan.actionable_steps()
.map(|step| step.package.clone())
.collect()
}
pub(super) fn source_build_reason(reason: &str) -> String {
if let Some(dep) = reason.strip_prefix("dependency ") {
format!("requested dependency '{dep}'")
} else if let Some((requester, _)) = reason.split_once(" needs ") {
format!("needed by '{requester}'")
} else if reason == "requested spec" {
"requested spec".to_string()
} else if reason == "requested package" {
"requested package".to_string()
} else {
reason.to_string()
}
}
pub(super) fn source_build_warning_messages(plan: &planner::ExecutionPlan) -> Vec<String> {
let mut lines = Vec::new();
for step in plan.actionable_steps() {
if !matches!(step.action, planner::PlanAction::BuildAndInstall) {
continue;
}
let mut reasons = Vec::new();
for reason in &step.requested_by {
let label = source_build_reason(reason);
if !reasons.contains(&label) {
reasons.push(label);
}
}
if reasons.is_empty() {
lines.push(step.package.clone());
} else {
lines.push(format!("{} ({})", step.package, reasons.join(", ")));
}
}
lines
}
pub(super) fn warn_source_build_plan(plan: &planner::ExecutionPlan) {
let lines = source_build_warning_messages(plan);
if lines.is_empty() {
return;
}
ui::warn(format!(
"{} package(s) will be built from source before installation.",
lines.len()
));
for line in lines {
ui::warn(format!(" {line}"));
}
}
pub(super) fn validate_source_build_prereqs_for_plan(
plan: &planner::ExecutionPlan,
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
let db_path = config.installed_db_path(rootfs);
let mut checked_development_package = false;
for step in plan.actionable_steps() {
let planner::PlanOrigin::Source { path, .. } = &step.origin else {
continue;
};
if !matches!(step.action, planner::PlanAction::BuildAndInstall) {
continue;
}
let mut spec = package::PackageSpec::from_file(path)
.with_context(|| format!("Failed to parse spec {}", path.display()))?;
spec.apply_config(config);
source::preflight_local_manual_sources(&spec)?;
if !checked_development_package && !spec.is_metapackage() {
ensure_requested_development_package_installed(&db_path)?;
checked_development_package = true;
}
}
Ok(())
}
#[derive(Clone, Copy)]
pub(super) struct InstallPlanExecutionOptions<'a> {
pub(super) no_flags: bool,
pub(super) cross_prefix: Option<&'a str>,
pub(super) clean: bool,
pub(super) dry_run: bool,
pub(super) confirm_installation: bool,
pub(super) lib32_only_requested_specs: bool,
pub(super) install_test_deps: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ChildInstallBatch {
pub(super) requests: Vec<PathBuf>,
pub(super) lib32_only: bool,
}
pub(super) fn step_requests_only_lib32(
step: &planner::PlannedStep,
options: &InstallPlanExecutionOptions<'_>,
) -> bool {
options.lib32_only_requested_specs
&& step
.requested_by
.iter()
.any(|reason| reason.starts_with("requested "))
}
pub(super) fn build_live_rootfs_child_install_batches(
steps: &[&planner::PlannedStep],
options: &InstallPlanExecutionOptions<'_>,
binary_archives: &HashMap<(String, String), db::repo::BinaryRepoCachedArchive>,
) -> Result<Vec<ChildInstallBatch>> {
let mut batches = Vec::new();
let mut pending_binary_requests = Vec::new();
for step in steps {
match &step.origin {
planner::PlanOrigin::Source { path, .. } => {
if !pending_binary_requests.is_empty() {
batches.push(ChildInstallBatch {
requests: std::mem::take(&mut pending_binary_requests),
lib32_only: false,
});
}
batches.push(ChildInstallBatch {
requests: vec![path.clone()],
lib32_only: step_requests_only_lib32(step, options),
});
}
planner::PlanOrigin::Binary { repo_name, record } => {
let cached = binary_archives
.get(&(repo_name.clone(), record.filename.clone()))
.with_context(|| {
format!(
"Cached archive missing for planned binary step '{}' from repo '{}'",
record.filename, repo_name
)
})?;
pending_binary_requests.push(cached.package_path.clone());
}
planner::PlanOrigin::Installed => {}
}
}
if !pending_binary_requests.is_empty() {
batches.push(ChildInstallBatch {
requests: pending_binary_requests,
lib32_only: false,
});
}
Ok(batches)
}
pub(super) fn flush_binary_install_batch(
pending_plans: &mut Vec<PlannedPackageInstall>,
pending_staging_dirs: &mut Vec<tempfile::TempDir>,
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
if pending_plans.is_empty() {
return Ok(());
}
install_planned_packages_to_rootfs(pending_plans, rootfs, config)?;
pending_plans.clear();
pending_staging_dirs.clear();
Ok(())
}
pub(super) fn execute_install_plan_with_child_commands(
plan: &planner::ExecutionPlan,
rootfs: &Path,
config: &config::Config,
options: InstallPlanExecutionOptions<'_>,
) -> Result<()> {
#[derive(Clone)]
struct BinaryPhaseItem {
repo_name: String,
record: db::repo::BinaryRepoPackageRecord,
}
let actionable_steps: Vec<_> = plan.actionable_steps().collect();
if actionable_steps.is_empty() {
ui::info("Nothing to do.");
return Ok(());
}
validate_source_build_prereqs_for_plan(plan, rootfs, config)?;
warn_source_build_plan(plan);
let planned_packages = actionable_plan_packages(plan);
if options.confirm_installation
&& !ui::prompt_package_action("installation", &planned_packages, true)?
{
anyhow::bail!("Aborted");
}
let mut conflict_subjects = Vec::new();
for step in &actionable_steps {
match &step.origin {
planner::PlanOrigin::Source { path, .. } => {
let mut spec = package::PackageSpec::from_file(path)
.with_context(|| format!("Failed to parse spec {}", path.display()))?;
spec.apply_config(config);
let lib32_only =
effective_lib32_only(&spec, step_requests_only_lib32(step, &options));
conflict_subjects.extend(install_conflict_subjects_for_spec(
&spec,
!lib32_only,
spec.builds_lib32_output() || lib32_only,
));
}
planner::PlanOrigin::Binary { record, .. } => {
conflict_subjects.push(install_conflict_subject_for_binary_record(record));
}
planner::PlanOrigin::Installed => {}
}
}
resolve_installed_conflicts_for_subjects(&conflict_subjects, rootfs, config, options.dry_run)?;
if options.dry_run {
ui::info("Dry run enabled, no install/build actions executed.");
return Ok(());
}
let mut binary_archives: HashMap<(String, String), db::repo::BinaryRepoCachedArchive> =
HashMap::new();
let mut binary_phase_items = Vec::new();
let mut seen_binary_archives = HashSet::new();
for step in &actionable_steps {
if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin
&& seen_binary_archives.insert((repo_name.clone(), record.filename.clone()))
{
binary_phase_items.push(BinaryPhaseItem {
repo_name: repo_name.clone(),
record: (**record).clone(),
});
}
}
if !binary_phase_items.is_empty() {
ui::info(format!(
"Downloading {} binary package(s) and detached signatures...",
binary_phase_items.len()
));
let use_tty_progress = std::io::stderr().is_terminal();
let download_progress = MultiProgress::with_draw_target(if use_tty_progress {
ProgressDrawTarget::stderr()
} else {
ProgressDrawTarget::hidden()
});
let download_bars = binary_phase_items
.iter()
.map(|item| {
let label = format!(
"{}-{}-{}",
item.record.name,
item.record.version,
binary_arch_from_filename(&item.record.filename)
);
let pb = download_progress.add(ProgressBar::new(item.record.size.max(1)));
pb.set_style(
ProgressStyle::default_bar()
.template("{prefix:.bold} [{bar:40.cyan/blue}] {eta}")
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("#>-"),
);
pb.set_prefix(label);
pb
})
.collect::<Vec<_>>();
let download_client = db::repo::binary_package_http_client()?;
let download_results = run_parallel_tasks(
&binary_phase_items,
MAX_PARALLEL_DOWNLOADS,
|index, item| {
let pb = &download_bars[index];
let mut progress_cb = |downloaded: u64, total: Option<u64>| {
if let Some(t) = total
&& t > 0
{
pb.set_length(t);
}
pb.set_position(downloaded);
};
let result = (|| {
let repo_cfg = config.binary_repos.get(&item.repo_name).with_context(|| {
format!("Binary repo '{}' not found in config", item.repo_name)
})?;
db::repo::cache_binary_package_archive_with_client_and_progress(
&item.repo_name,
repo_cfg,
&item.record,
&config.package_cache_dir,
&download_client,
Some(&mut progress_cb),
)
.with_context(|| {
format!(
"Failed to cache binary package '{}' from repo '{}'",
item.record.filename, item.repo_name
)
})
})();
pb.finish_and_clear();
result
},
);
download_progress
.clear()
.context("Failed to clear binary download progress")?;
for (item, cached) in binary_phase_items.iter().zip(download_results?) {
binary_archives.insert(
(item.repo_name.clone(), item.record.filename.clone()),
cached,
);
}
ui::info(format!(
"Verifying checksums and detached signatures for {} binary package(s)...",
binary_phase_items.len()
));
let integrity_pb = ProgressBar::new(binary_phase_items.len() as u64);
integrity_pb.set_draw_target(if use_tty_progress {
ProgressDrawTarget::stderr()
} else {
ProgressDrawTarget::hidden()
});
integrity_pb.set_style(
ProgressStyle::default_bar()
.template("{prefix:.bold} [{bar:40.cyan/blue}] {pos}/{len} {eta}")
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("#>-"),
);
integrity_pb.set_prefix("integrity");
let has_detached_signatures = binary_phase_items.iter().any(|item| {
binary_archives
.get(&(item.repo_name.clone(), item.record.filename.clone()))
.is_some_and(|cached| cached.signature_path.exists())
});
let trusted_keys = if has_detached_signatures {
signing::load_trusted_public_keys(rootfs)
.context("Failed to load trusted Minisign public keys")?
} else {
Vec::new()
};
run_parallel_verification(&binary_phase_items, &integrity_pb, |item| {
let repo_cfg = config
.binary_repos
.get(&item.repo_name)
.with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?;
let cached = binary_archives
.get(&(item.repo_name.clone(), item.record.filename.clone()))
.with_context(|| {
format!(
"Cached archive missing for {} from repo '{}'",
item.record.filename, item.repo_name
)
})?;
db::repo::verify_binary_package_archive_integrity_with_trusted_keys(
&item.repo_name,
repo_cfg,
&item.record,
&cached.package_path,
&cached.signature_path,
&trusted_keys,
)
.with_context(|| {
format!(
"Integrity verification failed for {} from repo '{}'",
item.record.filename, item.repo_name
)
})
})?;
integrity_pb.finish_and_clear();
}
if should_delegate_live_rootfs_installs(rootfs) {
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
let batches =
build_live_rootfs_child_install_batches(&actionable_steps, &options, &binary_archives)?;
for batch in batches {
run_install_command_with_program(
&exe,
&batch.requests,
rootfs,
ChildInstallCommandOptions {
no_deps: true,
assume_yes: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
lib32_only: batch.lib32_only,
install_test_deps: options.install_test_deps,
install_context: Some(INSTALL_CONTEXT_PLANNED),
dep_chain: None,
},
)?;
}
return Ok(());
}
let mut binary_pre_hook_plans = Vec::new();
for step in &actionable_steps {
if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin {
let cached = binary_archives
.get(&(repo_name.clone(), record.filename.clone()))
.with_context(|| {
format!(
"Cached archive missing for planned binary step '{}' from repo '{}'",
record.filename, repo_name
)
})?;
let staged = extract_package_archive_to_staging(config, &cached.package_path)?;
let spec = load_package_spec_from_staging_or_repo_record(staged.path(), record)?;
let plans = plan_package_outputs_for_install(&spec, staged.path(), rootfs, config)?;
binary_pre_hook_plans.extend(plans);
}
}
run_transaction_hooks_for_plans(
rootfs,
install::hooks::HookPhase::Pre,
&binary_pre_hook_plans,
)?;
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
let total_steps = actionable_steps.len();
let mut binary_post_hook_plans = Vec::new();
let mut pending_binary_install_plans = Vec::new();
let mut pending_binary_install_staging_dirs = Vec::new();
for (idx, step) in actionable_steps.into_iter().enumerate() {
match &step.origin {
planner::PlanOrigin::Source { path, .. } => {
flush_binary_install_batch(
&mut pending_binary_install_plans,
&mut pending_binary_install_staging_dirs,
rootfs,
config,
)?;
ui::info(format!(
"[{}/{}] building+installing {} from source",
idx + 1,
total_steps,
step.package
));
run_install_command_with_program(
&exe,
std::slice::from_ref(path),
rootfs,
ChildInstallCommandOptions {
no_deps: true,
assume_yes: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
lib32_only: step_requests_only_lib32(step, &options),
install_test_deps: options.install_test_deps,
install_context: Some(INSTALL_CONTEXT_PLANNED),
dep_chain: None,
},
)
.with_context(|| {
format!("Failed to spawn planned install step '{}'", step.package)
})?;
}
planner::PlanOrigin::Binary { repo_name, record } => {
let cached = binary_archives
.get(&(repo_name.clone(), record.filename.clone()))
.with_context(|| {
format!(
"Cached archive missing for planned binary step '{}' from repo '{}'",
record.filename, repo_name
)
})?;
let staged = extract_package_archive_to_staging(config, &cached.package_path)?;
let spec = load_package_spec_from_staging_or_repo_record(staged.path(), record)?;
let plans = plan_package_outputs_for_install(&spec, staged.path(), rootfs, config)?;
binary_post_hook_plans.extend(plans.iter().cloned());
pending_binary_install_plans.extend(plans);
pending_binary_install_staging_dirs.push(staged);
}
planner::PlanOrigin::Installed => {}
}
}
flush_binary_install_batch(
&mut pending_binary_install_plans,
&mut pending_binary_install_staging_dirs,
rootfs,
config,
)?;
run_transaction_hooks_for_plans(
rootfs,
install::hooks::HookPhase::Post,
&binary_post_hook_plans,
)?;
install::scripts::run_deferred_hooks_if_possible(rootfs)?;
Ok(())
}
File diff suppressed because it is too large Load Diff
+16 -3116
View File
File diff suppressed because it is too large Load Diff
+764
View File
@@ -0,0 +1,764 @@
use super::*;
#[test]
fn build_env_rootfs_uses_selected_non_live_rootfs() {
let tmp = tempfile::tempdir().unwrap();
let expected = tmp.path().canonicalize().unwrap();
assert_eq!(
build_cmd::build_env_rootfs(tmp.path()),
expected.to_string_lossy()
);
assert_eq!(build_cmd::build_env_rootfs(Path::new("/")), "/");
}
#[test]
fn clean_build_workspace_removes_build_and_source_cache_dirs() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("tmp/build");
cfg.cache_dir = rootfs.path().join("tmp/sources");
fs::create_dir_all(&cfg.build_dir)
.with_context(|| format!("Failed to create {}", cfg.build_dir.display()))?;
fs::create_dir_all(&cfg.cache_dir)
.with_context(|| format!("Failed to create {}", cfg.cache_dir.display()))?;
let mut build_file = fs::File::create(cfg.build_dir.join("artifact.txt"))?;
build_file.write_all(b"build data")?;
build_file.flush()?;
let mut source_file = fs::File::create(cfg.cache_dir.join("source.tar.zst"))?;
source_file.write_all(b"source data")?;
source_file.flush()?;
clean_build_workspace(&cfg)?;
assert!(!cfg.build_dir.exists());
assert!(!cfg.cache_dir.exists());
Ok(())
}
#[test]
fn clean_build_workspace_noops_when_dirs_are_missing() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("tmp/build");
cfg.cache_dir = rootfs.path().join("tmp/sources");
clean_build_workspace(&cfg)?;
assert!(!cfg.build_dir.exists());
assert!(!cfg.cache_dir.exists());
Ok(())
}
#[test]
fn clean_build_source_dirs_removes_build_dir_only() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("tmp/build");
cfg.cache_dir = rootfs.path().join("tmp/sources");
fs::create_dir_all(&cfg.build_dir)
.with_context(|| format!("Failed to create {}", cfg.build_dir.display()))?;
fs::create_dir_all(&cfg.cache_dir)
.with_context(|| format!("Failed to create {}", cfg.cache_dir.display()))?;
clean_build_source_dirs(&cfg)?;
assert!(!cfg.build_dir.exists());
assert!(cfg.cache_dir.exists());
Ok(())
}
#[test]
fn clean_build_source_dirs_noops_when_build_dir_missing() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("tmp/build");
cfg.cache_dir = rootfs.path().join("tmp/sources");
fs::create_dir_all(&cfg.cache_dir)
.with_context(|| format!("Failed to create {}", cfg.cache_dir.display()))?;
clean_build_source_dirs(&cfg)?;
assert!(!cfg.build_dir.exists());
assert!(cfg.cache_dir.exists());
Ok(())
}
#[test]
fn binary_archive_staging_uses_config_build_dir_instead_of_process_tmpdir() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_path = pkg_dir.path().join("pkg-1.0-1-x86_64.depot.pkg.tar.zst");
let file = fs::File::create(&archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let payload = b"hello";
let mut header = tar::Header::new_gnu();
header.set_path("usr/bin/hello").unwrap();
header.set_size(payload.len() as u64);
header.set_mode(0o755);
header.set_cksum();
tar.append(&header, &payload[..]).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
let staged = extract_package_archive_to_staging(&cfg, &archive_path)?;
assert!(staged.path().starts_with(staging_temp_root(&cfg)));
assert!(staged.path().join("usr/bin/hello").exists());
Ok(())
}
#[test]
#[cfg(unix)]
fn child_install_command_includes_lib32_only_flag_when_requested() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let script_path = temp.path().join("capture-lib32-child-install.sh");
let args_path = temp.path().join("args.txt");
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"{}\"\n",
args_path.display()
);
fs::write(&script_path, script)
.with_context(|| format!("Failed to write {}", script_path.display()))?;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))
.with_context(|| format!("Failed to chmod {}", script_path.display()))?;
run_install_command_with_program(
&script_path,
&[PathBuf::from("/tmp/pkg.toml")],
Path::new("/"),
ChildInstallCommandOptions {
no_deps: true,
assume_yes: true,
no_flags: false,
cross_prefix: None,
clean: false,
lib32_only: true,
install_test_deps: false,
install_context: None,
dep_chain: None,
},
)?;
let captured_args = fs::read_to_string(&args_path)
.with_context(|| format!("Failed to read {}", args_path.display()))?;
assert!(captured_args.lines().any(|line| line == "--lib32-only"));
Ok(())
}
#[test]
fn direct_install_checks_manual_sources_before_dependency_resolution() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let spec_dir = tempfile::tempdir().context("Failed to create temp spec dir")?;
let spec_path = spec_dir.path().join("demo.toml");
fs::write(
&spec_path,
r#"[package]
name = "demo"
version = "1.0.0"
revision = 1
description = "demo"
homepage = "https://example.test/demo"
license = "MIT"
[build]
type = "custom"
[dependencies]
runtime = ["definitely-missing-dep"]
optional = []
[[manual_sources]]
file = "missing.patch"
"#,
)?;
let mut config = config::Config::for_rootfs(rootfs.path());
config.build_dir = rootfs.path().join("var/cache/depot/build");
config.cache_dir = rootfs.path().join("var/cache/depot/sources");
config.db_dir = rootfs.path().join("var/lib/depot");
ui::set_assume_yes(true);
let result = run_direct_install_request(
DirectInstallOptions {
rootfs: rootfs.path(),
no_deps: false,
no_flags: false,
cross_prefix: None,
clean: false,
dry_run: false,
lib32_only: false,
install_test_deps: false,
},
&config,
spec_path,
);
ui::set_assume_yes(false);
let err = result.expect_err("missing manual source should fail before dependency install");
assert!(
err.to_string()
.contains("Manual source not found: missing.patch")
);
assert!(
!err.to_string()
.contains("Could not find package spec for dependency")
);
Ok(())
}
#[test]
fn build_command_checks_manual_sources_before_dependency_resolution() -> Result<()> {
let _guard = assume_yes_test_lock();
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let rootfs = temp.path().join("rootfs");
let spec_dir = temp.path().join("packages").join("demo");
fs::create_dir_all(&rootfs)?;
fs::create_dir_all(&spec_dir)?;
let spec_path = spec_dir.join("demo.toml");
fs::write(
&spec_path,
r#"[package]
name = "demo"
version = "1.0.0"
revision = 1
description = "demo"
homepage = "https://example.test/demo"
license = "MIT"
[[source]]
url = "https://example.test/demo-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "demo-1.0.0"
[build]
type = "custom"
[dependencies]
build = ["definitely-missing-dep"]
runtime = []
optional = []
[[manual_sources]]
file = "missing.patch"
"#,
)?;
let result = run(Cli {
command: Commands::Build(BuildArgs {
rootfs_args: rootfs_args(rootfs),
prompt_args: prompt_args(true),
build_exec_args: build_exec_args(),
lib32_args: lib32_args(),
spec_pos: Some(spec_path),
spec: None,
install: false,
install_deps: true,
cleanup_deps: false,
}),
});
let err = result.expect_err("missing manual source should fail before dependency install");
assert!(
err.to_string()
.contains("Manual source not found: missing.patch")
);
assert!(
!err.to_string()
.contains("Failed to resolve required build tool package")
);
Ok(())
}
#[test]
fn source_build_warning_messages_include_dependency_context() {
let plan = planner::ExecutionPlan {
steps: vec![
planner::PlannedStep {
package: "dep-src".into(),
action: planner::PlanAction::BuildAndInstall,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("/tmp/dep-src.toml"),
local_sibling: false,
},
requested_by: vec!["dependency dep-src".into(), "app needs dep-src".into()],
},
planner::PlannedStep {
package: "dep-bin".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Binary {
repo_name: "core".into(),
record: Box::new(test_binary_repo_record(
"dep-bin",
"dep-bin-1.0-1-x86_64.tar.zst",
)),
},
requested_by: vec!["app needs dep-bin".into()],
},
],
};
assert_eq!(
source_build_warning_messages(&plan),
vec!["dep-src (requested dependency 'dep-src', needed by 'app')".to_string()]
);
}
#[test]
fn planned_source_build_prereqs_check_manual_sources_before_confirmation() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let spec_dir = tempfile::tempdir().context("Failed to create temp spec dir")?;
let spec_path = spec_dir.path().join("demo.toml");
fs::write(
&spec_path,
r#"[package]
name = "demo"
version = "1.0.0"
revision = 1
description = "demo"
homepage = "https://example.test/demo"
license = "MIT"
[[source]]
url = "https://example.test/demo-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "demo-1.0.0"
[build]
type = "custom"
[dependencies]
build = []
runtime = []
optional = []
[[manual_sources]]
file = "missing.patch"
"#,
)?;
let config = config::Config::for_rootfs(rootfs.path());
let plan = planner::ExecutionPlan {
steps: vec![planner::PlannedStep {
package: "demo".into(),
action: planner::PlanAction::BuildAndInstall,
origin: planner::PlanOrigin::Source {
path: spec_path,
local_sibling: true,
},
requested_by: vec!["requested spec".into()],
}],
};
let err = validate_source_build_prereqs_for_plan(&plan, rootfs.path(), &config)
.expect_err("missing local manual source should fail before confirmation");
assert!(
err.to_string()
.contains("Manual source not found: missing.patch")
);
Ok(())
}
#[test]
fn cleanup_targets_keep_runtime_dependencies_for_build_install() {
let plan = planner::ExecutionPlan {
steps: vec![
planner::PlannedStep {
package: "zlib".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/zlib/zlib.toml"),
local_sibling: false,
},
requested_by: vec!["cmake needs zlib".into(), "llvm-runtime needs zlib".into()],
},
planner::PlannedStep {
package: "cmake".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/cmake/cmake.toml"),
local_sibling: false,
},
requested_by: vec!["dependency cmake".into()],
},
planner::PlannedStep {
package: "llvm-runtime".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/llvm-runtime/llvm-runtime.toml"),
local_sibling: false,
},
requested_by: vec!["dependency llvm-runtime".into()],
},
],
};
let mut tracker = AutoInstalledDependencyTracker::default();
tracker.record_plan(&plan, &["cmake".into()], AutoInstalledDependencyKind::Build);
tracker.record_plan(
&plan,
&["llvm-runtime".into()],
AutoInstalledDependencyKind::Runtime,
);
assert_eq!(tracker.cleanup_targets(false), vec!["cmake".to_string()]);
assert_eq!(
tracker.cleanup_targets(true),
vec![
"llvm-runtime".to_string(),
"cmake".to_string(),
"zlib".to_string()
]
);
}
#[test]
fn build_type_runs_automatic_tests_matches_builder_behavior() {
assert!(build_type_runs_automatic_tests(&test_package_spec(
package::BuildType::Autotools,
None,
&[]
)));
assert!(build_type_runs_automatic_tests(&test_package_spec(
package::BuildType::Perl,
None,
&[]
)));
assert!(build_type_runs_automatic_tests(&test_package_spec(
package::BuildType::Meson,
None,
&[]
)));
assert!(build_type_runs_automatic_tests(&test_package_spec(
package::BuildType::CMake,
None,
&[]
)));
}
#[test]
fn requested_test_deps_prompt_can_disable_tests() -> Result<()> {
let _guard = assume_yes_test_lock();
let mut spec = test_package_spec(package::BuildType::Meson, None, &[]);
spec.dependencies.test = vec!["pytest".into()];
ui::set_assume_yes(true);
let prompted = maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut spec,
&["pytest".into()],
"Requested test dependencies are missing",
)?;
ui::set_assume_yes(false);
assert!(prompted);
assert!(spec.build.flags.skip_tests);
Ok(())
}
#[test]
fn requested_test_deps_prompt_is_ignored_for_non_automatic_test_builders() -> Result<()> {
let _guard = assume_yes_test_lock();
let mut spec = test_package_spec(package::BuildType::Custom, None, &[]);
spec.dependencies.test = vec!["pytest".into()];
ui::set_assume_yes(true);
let prompted = maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut spec,
&["pytest".into()],
"Requested test dependencies are missing",
)?;
ui::set_assume_yes(false);
assert!(!prompted);
assert!(!spec.build.flags.skip_tests);
Ok(())
}
#[test]
fn requested_test_deps_prompt_is_ignored_for_multilib_builds() -> Result<()> {
let _guard = assume_yes_test_lock();
let mut spec = test_package_spec(package::BuildType::Meson, None, &[]);
spec.build.flags.build_32 = true;
spec.dependencies.test = vec!["pytest".into()];
ui::set_assume_yes(true);
let prompted = maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut spec,
&["pytest".into()],
"Requested test dependencies are missing",
)?;
ui::set_assume_yes(false);
assert!(!prompted);
assert!(!spec.build.flags.skip_tests);
Ok(())
}
#[test]
fn should_not_install_test_deps_for_cli_lib32_only_builds() {
let mut spec = test_package_spec(package::BuildType::Meson, None, &[]);
spec.dependencies.lib32 = Some(package::DependencyGroup {
build: Vec::new(),
runtime: Vec::new(),
test: vec!["lib32-pytest".into()],
optional: Vec::new(),
groups: Vec::new(),
});
assert!(!should_install_test_deps(
&spec,
true,
deps::RequestedOutputs::Lib32Only
));
}
#[test]
fn build_command_requires_install_deps_flag_for_missing_dependencies() -> Result<()> {
let _guard = assume_yes_test_lock();
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let rootfs = temp.path().join("rootfs");
let repo_root = temp.path().join("packages");
let app_dir = repo_root.join("app");
let dep_dir = repo_root.join("dep");
fs::create_dir_all(&rootfs)?;
fs::create_dir_all(&app_dir)?;
fs::create_dir_all(&dep_dir)?;
let app_spec = app_dir.join("app.toml");
fs::write(
&app_spec,
r#"[package]
name = "app"
version = "1.0.0"
revision = 1
description = "app"
homepage = "https://example.test/app"
license = "MIT"
[[source]]
url = "https://example.test/app-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "app-1.0.0"
[build]
type = "custom"
[dependencies]
build = ["dep"]
runtime = []
optional = []
"#,
)
.with_context(|| format!("Failed to write {}", app_spec.display()))?;
let dep_spec = dep_dir.join("dep.toml");
fs::write(
&dep_spec,
r#"[package]
name = "dep"
version = "1.0.0"
revision = 1
description = "dep"
homepage = "https://example.test/dep"
license = "MIT"
[[source]]
url = "https://example.test/dep-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "dep-1.0.0"
[build]
type = "custom"
[dependencies]
build = []
runtime = []
optional = []
"#,
)
.with_context(|| format!("Failed to write {}", dep_spec.display()))?;
let config = config::Config::for_rootfs(&rootfs);
register_required_development_package_if_configured(&config, &rootfs)?;
let result = run(Cli {
command: Commands::Build(BuildArgs {
rootfs_args: rootfs_args(rootfs.clone()),
prompt_args: prompt_args(true),
build_exec_args: build_exec_args(),
lib32_args: lib32_args(),
spec_pos: Some(app_spec),
spec: None,
install: false,
install_deps: false,
cleanup_deps: false,
}),
});
ui::set_assume_yes(false);
let err = result.expect_err("build should require --install-deps when deps are missing");
assert!(err.to_string().contains("Re-run with --install-deps"));
Ok(())
}
#[test]
fn make_lib32_build_spec_uses_only_lib32_flag_rules() {
let mut base = test_package_spec(package::BuildType::Custom, None, &[]);
base.build.flags.cflags = vec!["-O2".into()];
base.build.flags.replace_cflags = vec!["-O2=>-O3".into()];
base.build.flags.cflags_lib32 = vec!["-m32".into()];
base.build.flags.replace_cflags_lib32 = vec!["-m32=>-mstackrealign".into()];
base.build.flags.cxxflags = vec!["-O2".into()];
base.build.flags.replace_cxxflags = vec!["-O2=>-O3".into()];
base.build.flags.cxxflags_lib32 = vec!["-fno-rtti".into()];
base.build.flags.replace_cxxflags_lib32 = vec!["-fno-rtti=>-fno-exceptions".into()];
let lib32 = make_lib32_build_spec(&base);
assert!(lib32.build.flags.lib32_variant);
assert_eq!(lib32.build.flags.cflags, vec!["-m32"]);
assert_eq!(
lib32.build.flags.replace_cflags,
vec!["-m32=>-mstackrealign"]
);
assert_eq!(lib32.build.flags.cxxflags, vec!["-fno-rtti"]);
assert_eq!(
lib32.build.flags.replace_cxxflags,
vec!["-fno-rtti=>-fno-exceptions"]
);
}
#[test]
fn make_lib32_package_spec_uses_lib32_dependency_override() {
let mut base = test_package_spec(package::BuildType::Custom, None, &[]);
base.dependencies.runtime = vec!["zlib".into()];
base.dependencies.lib32 = Some(package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: Vec::new(),
optional: vec!["lib32-gtk-doc".into()],
groups: Vec::new(),
});
let lib32 = make_lib32_package_spec(&base);
assert_eq!(lib32.package.name, "lib32-pkg");
assert_eq!(lib32.dependencies.build, vec!["gcc-multilib"]);
assert_eq!(lib32.dependencies.runtime, vec!["lib32-zlib", "pkg"]);
assert_eq!(lib32.dependencies.optional, vec!["lib32-gtk-doc"]);
}
#[test]
fn make_lib32_package_spec_does_not_inherit_primary_alternatives() {
let mut base = test_package_spec(package::BuildType::Custom, None, &[]);
base.alternatives.provides = vec!["editor".into()];
base.alternatives.conflicts = vec!["nano".into()];
base.alternatives.replaces = vec!["vi".into()];
let lib32 = make_lib32_package_spec(&base);
assert_eq!(lib32.package.name, "lib32-pkg");
assert!(lib32.alternatives.provides.is_empty());
assert!(lib32.alternatives.conflicts.is_empty());
assert!(lib32.alternatives.replaces.is_empty());
}
#[test]
fn requested_outputs_prefers_lib32_only_spec_flag() {
let mut spec = test_package_spec(package::BuildType::Custom, None, &[]);
spec.build.flags.lib32_only = true;
assert_eq!(
requested_outputs(&spec, false),
deps::RequestedOutputs::Lib32Only
);
}
#[test]
fn expand_install_requests_for_groups_uses_source_specs() -> Result<()> {
let temp = tempfile::tempdir()?;
let rootfs = temp.path().join("rootfs");
let repo_root = temp.path().join("repos");
let core = repo_root.join("core").join("foo");
let desktop = repo_root.join("desktop").join("bar");
fs::create_dir_all(&rootfs)?;
fs::create_dir_all(&core)?;
fs::create_dir_all(&desktop)?;
let foo_spec = core.join("foo.toml");
fs::write(
&foo_spec,
r#"[package]
name = "foo"
version = "1.0.0"
revision = 1
description = "foo"
homepage = "https://example.test/foo"
license = "MIT"
[[source]]
url = "https://example.test/foo-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "foo-1.0.0"
[build]
type = "custom"
[dependencies]
groups = ["base"]
runtime = []
optional = []
"#,
)?;
let bar_spec = desktop.join("bar.toml");
fs::write(
&bar_spec,
r#"[package]
name = "bar"
version = "1.0.0"
revision = 1
description = "bar"
homepage = "https://example.test/bar"
license = "MIT"
[[source]]
url = "https://example.test/bar-1.0.0.tar.gz"
sha256 = "skip"
extract_dir = "bar-1.0.0"
[build]
type = "custom"
[dependencies]
groups = ["base", "desktop"]
runtime = []
optional = []
"#,
)?;
let mut config = config::Config::for_rootfs(&rootfs);
config.repo_clone_dir = repo_root;
config.binary_repos.clear();
let (expanded, groups) =
expand_install_requests_for_groups(&config, &rootfs, &[PathBuf::from("base")])?;
assert_eq!(groups, vec!["base".to_string()]);
assert_eq!(expanded, vec![PathBuf::from("bar"), PathBuf::from("foo")]);
Ok(())
}
+502
View File
@@ -0,0 +1,502 @@
use super::*;
#[test]
fn install_post_extract_env_uses_selected_non_live_rootfs() -> Result<()> {
let _guard = assume_yes_test_lock();
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let rootfs = temp.path().join("rootfs");
let spec_dir = temp.path().join("packages").join("demo");
let source_dir = temp.path().join("source").join("demo-1.0.0");
let observed_env = temp.path().join("post-extract-rootfs.txt");
fs::create_dir_all(&rootfs)?;
fs::create_dir_all(&spec_dir)?;
fs::create_dir_all(&source_dir)?;
fs::write(source_dir.join("README"), "demo source")?;
fs::write(
spec_dir.join("build.sh"),
"mkdir -p \"$DESTDIR/usr/bin\"\nprintf demo > \"$DESTDIR/usr/bin/demo\"\n",
)?;
let spec_path = spec_dir.join("demo.toml");
fs::write(
&spec_path,
format!(
r#"[package]
name = "demo"
version = "1.0.0"
revision = 1
description = "demo"
homepage = "https://example.test/demo"
license = "MIT"
[[source]]
url = "file://{}"
sha256 = "skip"
extract_dir = "demo-1.0.0"
post_extract = ["printf '%s' \"$DEPOT_ROOTFS\" > '{}'"]
[build]
type = "custom"
[dependencies]
build = []
runtime = []
optional = []
"#,
source_dir.display(),
observed_env.display()
),
)?;
let config = config::Config::for_rootfs(&rootfs);
register_required_development_package_if_configured(&config, &rootfs)?;
run(Cli {
command: Commands::Install(InstallArgs {
rootfs_args: rootfs_args(rootfs.clone()),
prompt_args: prompt_args(true),
build_exec_args: BuildExecArgs {
no_deps: true,
..build_exec_args()
},
lib32_args: lib32_args(),
spec_or_archive: vec![spec_path],
spec: None,
}),
})?;
assert_eq!(
fs::read_to_string(&observed_env)?,
build_cmd::build_env_rootfs(&rootfs)
);
Ok(())
}
#[test]
fn run_internal_clone_checks_out_git_revision() {
let (_tmp, remote_url, tagged) = make_remote_git_repo();
let clone_root = tempfile::tempdir().unwrap();
let dest = clone_root.path().join("cloned-src");
run_internal_command(InternalCommands::Clone {
repo: format!("{remote_url}#v1.0.0"),
dest: Some(dest.clone()),
})
.unwrap();
let repo = Repository::open(&dest).unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), tagged);
assert_eq!(
std::fs::read_to_string(dest.join("README")).unwrap(),
"tagged\n"
);
}
#[test]
fn install_planned_packages_to_rootfs_runs_post_hooks_after_batch_install() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.db_dir = rootfs.path().join("var/lib/depot");
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
let old_spec = package::PackageSpec {
package: package::PackageInfo {
name: "findutils".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "findutils".into(),
homepage: "https://example.test/findutils".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: package::Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags::default(),
},
dependencies: package::Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let old_dest = rootfs.path().join("old-dest");
fs::create_dir_all(old_dest.join("usr/bin"))?;
fs::write(old_dest.join("usr/bin/find"), "old-find")?;
install_package_outputs_to_rootfs(&old_spec, &old_dest, rootfs.path(), &cfg)?;
let alpha_spec = package::PackageSpec {
package: package::PackageInfo {
name: "alpha".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "alpha".into(),
homepage: "https://example.test/alpha".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: package::Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags::default(),
},
dependencies: package::Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let alpha_dest = rootfs.path().join("alpha-dest");
fs::create_dir_all(alpha_dest.join("usr/bin"))?;
fs::create_dir_all(alpha_dest.join("scripts"))?;
fs::write(alpha_dest.join("usr/bin/alpha"), "alpha")?;
fs::write(
alpha_dest.join("scripts/post_install"),
"cat \"$DEPOT_ROOTFS/usr/bin/find\" > \"$DEPOT_ROOTFS/alpha-marker\"\n",
)?;
let replacement_spec = package::PackageSpec {
package: package::PackageInfo {
name: "busybox".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "busybox".into(),
homepage: "https://example.test/busybox".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["GPL-2.0-only".into()],
},
packages: Vec::new(),
alternatives: package::Alternatives {
provides: Vec::new(),
conflicts: Vec::new(),
replaces: vec!["findutils".into()],
lib32: None,
},
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags::default(),
},
dependencies: package::Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let replacement_dest = rootfs.path().join("replacement-dest");
fs::create_dir_all(replacement_dest.join("usr/bin"))?;
fs::write(replacement_dest.join("usr/bin/find"), "new-find")?;
let mut plans = Vec::new();
plans.extend(plan_package_outputs_for_install(
&alpha_spec,
&alpha_dest,
rootfs.path(),
&cfg,
)?);
plans.extend(plan_package_outputs_for_install(
&replacement_spec,
&replacement_dest,
rootfs.path(),
&cfg,
)?);
install_planned_packages_to_rootfs(&plans, rootfs.path(), &cfg)?;
assert_eq!(
fs::read_to_string(rootfs.path().join("alpha-marker"))?,
"new-find"
);
assert_eq!(
fs::read_to_string(rootfs.path().join("usr/bin/find"))?,
"new-find"
);
assert!(db::get_package_version(&cfg.installed_db_path(rootfs.path()), "findutils")?.is_none());
assert_eq!(
db::get_package_version(&cfg.installed_db_path(rootfs.path()), "busybox")?,
Some("1.0".into())
);
Ok(())
}
#[test]
#[cfg(unix)]
fn child_install_command_batches_multiple_requests_in_one_invocation() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let script_path = temp.path().join("capture-child-install.sh");
let args_path = temp.path().join("args.txt");
let env_path = temp.path().join("env.txt");
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"{}\"\nprintf '%s' \"${{DEPOT_DEPCHAIN:-}}\" > \"{}\"\n",
args_path.display(),
env_path.display()
);
fs::write(&script_path, script)
.with_context(|| format!("Failed to write {}", script_path.display()))?;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))
.with_context(|| format!("Failed to chmod {}", script_path.display()))?;
let requests = vec![
PathBuf::from("/tmp/pkg-a.toml"),
PathBuf::from("/tmp/pkg-b.toml"),
];
let rootfs = Path::new("/");
run_install_command_with_program(
&script_path,
&requests,
rootfs,
ChildInstallCommandOptions {
no_deps: false,
assume_yes: false,
no_flags: true,
cross_prefix: Some("x86_64-linux-musl"),
clean: true,
lib32_only: false,
install_test_deps: true,
install_context: None,
dep_chain: Some("parent"),
},
)?;
let captured_args = fs::read_to_string(&args_path)
.with_context(|| format!("Failed to read {}", args_path.display()))?;
assert_eq!(
captured_args.lines().collect::<Vec<_>>(),
vec![
"install",
"-r",
"/",
"--no-flags",
"--cross-prefix",
"x86_64-linux-musl",
"--clean",
"--test-deps",
"/tmp/pkg-a.toml",
"/tmp/pkg-b.toml",
]
);
assert_eq!(fs::read_to_string(&env_path)?, "parent");
Ok(())
}
#[test]
#[cfg(unix)]
fn child_install_command_propagates_install_context_env() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let script_path = temp.path().join("capture-child-install-context.sh");
let env_path = temp.path().join("context.txt");
let script = format!(
"#!/bin/sh\nprintf '%s' \"${{{}:-}}\" > \"{}\"\n",
DEPOT_INSTALL_CONTEXT_ENV,
env_path.display()
);
fs::write(&script_path, script)
.with_context(|| format!("Failed to write {}", script_path.display()))?;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))
.with_context(|| format!("Failed to chmod {}", script_path.display()))?;
run_install_command_with_program(
&script_path,
&[PathBuf::from("/tmp/pkg.toml")],
Path::new("/"),
ChildInstallCommandOptions {
no_deps: true,
assume_yes: true,
no_flags: false,
cross_prefix: None,
clean: false,
lib32_only: false,
install_test_deps: false,
install_context: Some(INSTALL_CONTEXT_UPDATE),
dep_chain: None,
},
)?;
assert_eq!(fs::read_to_string(&env_path)?, INSTALL_CONTEXT_UPDATE);
Ok(())
}
#[test]
fn sudo_preserve_env_arg_only_includes_present_depot_env_vars() {
let mut env = TestEnv::new();
assert_eq!(sudo_preserve_env_arg(), None);
env.set_var(DEPOT_INSTALL_CONTEXT_ENV, INSTALL_CONTEXT_PLANNED);
assert_eq!(
sudo_preserve_env_arg(),
Some(format!("--preserve-env={}", DEPOT_INSTALL_CONTEXT_ENV))
);
env.set_var("DEPOT_DEPCHAIN", "parent");
assert_eq!(
sudo_preserve_env_arg(),
Some(format!(
"--preserve-env={},DEPOT_DEPCHAIN",
DEPOT_INSTALL_CONTEXT_ENV
))
);
}
#[test]
fn rootfs_is_system_root_detects_live_rootfs() {
assert!(rootfs_is_system_root(Path::new("/")));
assert!(!rootfs_is_system_root(Path::new("/tmp/depot-test-rootfs")));
}
#[test]
fn should_delegate_live_rootfs_installs_only_for_live_root_when_non_root() {
assert_eq!(
should_delegate_live_rootfs_installs(Path::new("/")),
!crate::fakeroot::is_root()
);
assert!(!should_delegate_live_rootfs_installs(Path::new(
"/tmp/depot-test-rootfs"
)));
}
#[test]
fn live_rootfs_child_install_batches_group_consecutive_binary_steps() -> Result<()> {
let source_path = PathBuf::from("/tmp/requested.toml");
let expat_archive = PathBuf::from("/tmp/expat.pkg.tar.zst");
let python_archive = PathBuf::from("/tmp/python.pkg.tar.zst");
let compiler_rt_archive = PathBuf::from("/tmp/lib32-compiler-rt.pkg.tar.zst");
let expat_record = test_binary_repo_record("expat", "expat-1.0-1-x86_64.depot.pkg.tar.zst");
let python_record = test_binary_repo_record("python", "python-1.0-1-x86_64.depot.pkg.tar.zst");
let compiler_rt_record = test_binary_repo_record(
"lib32-compiler-rt",
"lib32-compiler-rt-1.0-1-x86_64.depot.pkg.tar.zst",
);
let steps = [
planner::PlannedStep {
package: "expat".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Binary {
repo_name: "core".into(),
record: Box::new(expat_record.clone()),
},
requested_by: vec!["pkg needs expat".into()],
},
planner::PlannedStep {
package: "python".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Binary {
repo_name: "core".into(),
record: Box::new(python_record.clone()),
},
requested_by: vec!["pkg needs python".into()],
},
planner::PlannedStep {
package: "pkg".into(),
action: planner::PlanAction::BuildAndInstall,
origin: planner::PlanOrigin::Source {
path: source_path.clone(),
local_sibling: false,
},
requested_by: vec!["requested spec".into()],
},
planner::PlannedStep {
package: "lib32-compiler-rt".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Binary {
repo_name: "core".into(),
record: Box::new(compiler_rt_record.clone()),
},
requested_by: vec!["pkg needs lib32-compiler-rt".into()],
},
];
let step_refs = steps.iter().collect::<Vec<_>>();
let mut binary_archives = HashMap::new();
binary_archives.insert(
("core".to_string(), expat_record.filename.clone()),
db::repo::BinaryRepoCachedArchive {
package_path: expat_archive.clone(),
signature_path: PathBuf::from("/tmp/expat.sig"),
},
);
binary_archives.insert(
("core".to_string(), python_record.filename.clone()),
db::repo::BinaryRepoCachedArchive {
package_path: python_archive.clone(),
signature_path: PathBuf::from("/tmp/python.sig"),
},
);
binary_archives.insert(
("core".to_string(), compiler_rt_record.filename.clone()),
db::repo::BinaryRepoCachedArchive {
package_path: compiler_rt_archive.clone(),
signature_path: PathBuf::from("/tmp/lib32-compiler-rt.sig"),
},
);
let options = InstallPlanExecutionOptions {
no_flags: false,
cross_prefix: None,
clean: false,
dry_run: false,
confirm_installation: false,
lib32_only_requested_specs: true,
install_test_deps: false,
};
let batches = build_live_rootfs_child_install_batches(&step_refs, &options, &binary_archives)?;
assert_eq!(
batches,
vec![
ChildInstallBatch {
requests: vec![expat_archive, python_archive],
lib32_only: false,
},
ChildInstallBatch {
requests: vec![source_path],
lib32_only: true,
},
ChildInstallBatch {
requests: vec![compiler_rt_archive],
lib32_only: false,
},
]
);
Ok(())
}
#[test]
fn expand_installed_group_targets_uses_installed_group_membership() -> Result<()> {
let temp = tempfile::tempdir()?;
let rootfs = temp.path().join("rootfs");
fs::create_dir_all(&rootfs)?;
let config = config::Config::for_rootfs(&rootfs);
let db_path = config.installed_db_path(&rootfs);
let dest = temp.path().join("dest");
fs::create_dir_all(dest.join("usr/bin"))?;
fs::write(dest.join("usr/bin/foo"), "foo")?;
let mut spec = test_package_spec(package::BuildType::Custom, None, &[]);
spec.package.name = "foo".into();
spec.dependencies.groups = vec!["base".into()];
db::register_package(&db_path, &spec, &dest)?;
db::record_installed_groups(&db_path, &[String::from("base")])?;
let (expanded, groups) = expand_installed_group_targets(&db_path, &[String::from("base")])?;
assert_eq!(groups, vec!["base".to_string()]);
assert_eq!(expanded, vec!["foo".to_string()]);
Ok(())
}
+637
View File
@@ -0,0 +1,637 @@
use super::*;
#[test]
fn parallel_verification_processes_every_item() -> Result<()> {
let items = vec![0_u8; 32];
let completed = AtomicUsize::new(0);
let progress = ProgressBar::hidden();
run_parallel_verification(&items, &progress, |_| {
completed.fetch_add(1, AtomicOrdering::Relaxed);
Ok(())
})?;
assert_eq!(completed.load(AtomicOrdering::Relaxed), items.len());
Ok(())
}
#[test]
fn parallel_tasks_run_concurrently_and_preserve_input_order() -> Result<()> {
let items = vec![3_u8, 1, 4, 2];
let barrier = Barrier::new(items.len());
let results = run_parallel_tasks(&items, items.len(), |_, item| {
barrier.wait();
Ok(item * 2)
})?;
assert_eq!(results, vec![6, 2, 8, 4]);
Ok(())
}
#[test]
fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_path = pkg_dir.path().join("pkg-1.0-1-x86_64.depot.pkg.tar.zst");
// Build an archive that intentionally does not contain .metadata.toml.
let file = fs::File::create(&archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let payload = b"hello";
let mut header = tar::Header::new_gnu();
header.set_path("usr/bin/hello").unwrap();
header.set_size(payload.len() as u64);
header.set_mode(0o755);
header.set_cksum();
tar.append(&header, &payload[..]).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
cfg.db_dir = rootfs.path().join("var/lib/depot");
let staged = extract_package_archive_to_staging(&cfg, &archive_path)?;
let record = db::repo::BinaryRepoPackageRecord {
repo_name: "core".into(),
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: archive_path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or_default()
.to_string(),
size: payload.len() as u64,
sha512: String::new(),
description: Some("test package".into()),
homepage: Some("https://example.test".into()),
license: Some("MIT".into()),
provides: vec!["pkg-virtual".into()],
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: vec!["glibc".into()],
optional_dependencies: vec!["manpages".into()],
groups: vec!["base".into()],
};
let spec = package_spec_from_repo_record(&record);
let installed = install_package_outputs_to_rootfs(&spec, staged.path(), rootfs.path(), &cfg)?;
assert_eq!(installed.len(), 1);
assert_eq!(installed[0].package.name, "pkg");
assert!(rootfs.path().join("usr/bin/hello").exists());
let db_path = cfg.installed_db_path(rootfs.path());
assert_eq!(
db::get_package_version(&db_path, "pkg")?,
Some("1.0".into())
);
Ok(())
}
#[test]
fn direct_archive_install_requests_batch_multiple_archives() -> Result<()> {
fn write_archive(
archive_path: &Path,
package_name: &str,
conflicts: &[&str],
payload_path: &str,
payload: &[u8],
) -> Result<()> {
let file = fs::File::create(archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let mut payload_header = tar::Header::new_gnu();
payload_header.set_path(payload_path)?;
payload_header.set_size(payload.len() as u64);
payload_header.set_mode(0o755);
payload_header.set_cksum();
tar.append(&payload_header, payload)?;
let conflicts_toml = if conflicts.is_empty() {
String::new()
} else {
format!(
"conflicts = [{}]\n",
conflicts
.iter()
.map(|conflict| format!("\"{conflict}\""))
.collect::<Vec<_>>()
.join(", ")
)
};
let metadata = format!(
"name = \"{package_name}\"\nversion = \"1.0\"\nrevision = 1\ndescription = \"test\"\nhomepage = \"https://example.test\"\nlicense = \"MIT\"\n{conflicts_toml}\n[dependencies]\nruntime = []\noptional = []\n"
);
let mut meta_header = tar::Header::new_gnu();
meta_header.set_path(".metadata.toml")?;
meta_header.set_size(metadata.len() as u64);
meta_header.set_mode(0o644);
meta_header.set_cksum();
tar.append(&meta_header, metadata.as_bytes())?;
let encoder = tar.into_inner()?;
encoder.finish()?;
Ok(())
}
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_a = pkg_dir.path().join("alpha-1.0-1-x86_64.depot.pkg.tar.zst");
let archive_b = pkg_dir.path().join("beta-1.0-1-x86_64.depot.pkg.tar.zst");
write_archive(&archive_a, "alpha", &[], "usr/bin/alpha", b"alpha")?;
write_archive(&archive_b, "beta", &[], "usr/bin/beta", b"beta")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
cfg.db_dir = rootfs.path().join("var/lib/depot");
let installed = run_direct_archive_install_requests(
DirectInstallOptions {
rootfs: rootfs.path(),
no_deps: true,
no_flags: false,
cross_prefix: None,
clean: false,
dry_run: false,
lib32_only: false,
install_test_deps: false,
},
&cfg,
&[archive_a, archive_b],
false,
)?;
assert!(installed);
assert!(rootfs.path().join("usr/bin/alpha").exists());
assert!(rootfs.path().join("usr/bin/beta").exists());
let db_path = cfg.installed_db_path(rootfs.path());
assert_eq!(
db::get_package_version(&db_path, "alpha")?,
Some("1.0".into())
);
assert_eq!(
db::get_package_version(&db_path, "beta")?,
Some("1.0".into())
);
Ok(())
}
#[test]
fn direct_archive_install_rejects_conflicting_archives_in_same_batch() -> Result<()> {
fn write_archive(archive_path: &Path, package_name: &str, conflicts: &[&str]) -> Result<()> {
let file = fs::File::create(archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let payload = package_name.as_bytes();
let mut payload_header = tar::Header::new_gnu();
payload_header.set_path(format!("usr/bin/{package_name}"))?;
payload_header.set_size(payload.len() as u64);
payload_header.set_mode(0o755);
payload_header.set_cksum();
tar.append(&payload_header, payload)?;
let conflicts_toml = if conflicts.is_empty() {
String::new()
} else {
format!(
"conflicts = [{}]\n",
conflicts
.iter()
.map(|conflict| format!("\"{conflict}\""))
.collect::<Vec<_>>()
.join(", ")
)
};
let metadata = format!(
"name = \"{package_name}\"\nversion = \"1.0\"\nrevision = 1\ndescription = \"test\"\nhomepage = \"https://example.test\"\nlicense = \"MIT\"\n{conflicts_toml}\n[dependencies]\nruntime = []\noptional = []\n"
);
let mut meta_header = tar::Header::new_gnu();
meta_header.set_path(".metadata.toml")?;
meta_header.set_size(metadata.len() as u64);
meta_header.set_mode(0o644);
meta_header.set_cksum();
tar.append(&meta_header, metadata.as_bytes())?;
let encoder = tar.into_inner()?;
encoder.finish()?;
Ok(())
}
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_a = pkg_dir.path().join("alpha-1.0-1-x86_64.depot.pkg.tar.zst");
let archive_b = pkg_dir.path().join("beta-1.0-1-x86_64.depot.pkg.tar.zst");
write_archive(&archive_a, "alpha", &["beta"])?;
write_archive(&archive_b, "beta", &[])?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
cfg.db_dir = rootfs.path().join("var/lib/depot");
let err = run_direct_archive_install_requests(
DirectInstallOptions {
rootfs: rootfs.path(),
no_deps: true,
no_flags: false,
cross_prefix: None,
clean: false,
dry_run: false,
lib32_only: false,
install_test_deps: false,
},
&cfg,
&[archive_a, archive_b],
false,
)
.expect_err("conflicting archives should be rejected");
assert!(
err.to_string()
.contains("Cannot install conflicting packages in the same transaction")
);
Ok(())
}
#[test]
fn collect_conflicting_installed_packages_matches_by_name_and_provide() -> Result<()> {
let removals = collect_conflicting_installed_packages(
&[InstallConflictSubject {
package: "beta".into(),
provides: Vec::new(),
conflicts: vec!["alpha".into(), "editor".into()],
}],
&[InstalledConflictPackage {
name: "alpha".into(),
provides: vec!["editor".into()],
}],
)?;
assert_eq!(
removals.get("alpha"),
Some(&BTreeSet::from(["beta".to_string()]))
);
Ok(())
}
#[test]
#[cfg(unix)]
fn binary_archive_install_preserves_setuid_permissions() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_path = pkg_dir.path().join("sudo-1.0-1-x86_64.depot.pkg.tar.zst");
let file = fs::File::create(&archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let payload = b"sudo";
let mut header = tar::Header::new_gnu();
header.set_path("bin/sudo").unwrap();
header.set_size(payload.len() as u64);
header.set_mode(0o4755);
header.set_cksum();
tar.append(&header, &payload[..]).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
cfg.db_dir = rootfs.path().join("var/lib/depot");
let staged = extract_package_archive_to_staging(&cfg, &archive_path)?;
let staged_mode = fs::metadata(staged.path().join("bin/sudo"))?
.permissions()
.mode()
& 0o7777;
assert_eq!(staged_mode, 0o4755);
let record = db::repo::BinaryRepoPackageRecord {
repo_name: "core".into(),
name: "sudo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: archive_path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or_default()
.to_string(),
size: payload.len() as u64,
sha512: String::new(),
description: Some("sudo".into()),
homepage: Some("https://example.test".into()),
license: Some("ISC".into()),
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
};
let spec = package_spec_from_repo_record(&record);
let installed = install_package_outputs_to_rootfs(&spec, staged.path(), rootfs.path(), &cfg)?;
assert_eq!(installed.len(), 1);
let root_mode = fs::metadata(rootfs.path().join("bin/sudo"))?
.permissions()
.mode()
& 0o7777;
assert_eq!(root_mode, 0o4755);
Ok(())
}
#[test]
fn binary_archive_install_honors_keep_paths_from_metadata() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_path = pkg_dir
.path()
.join("filesystem-1.0-3-x86_64.depot.pkg.tar.zst");
let file = fs::File::create(&archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let payload = b"package-fstab";
let mut fstab_header = tar::Header::new_gnu();
fstab_header.set_path("etc/fstab").unwrap();
fstab_header.set_size(payload.len() as u64);
fstab_header.set_mode(0o644);
fstab_header.set_cksum();
tar.append(&fstab_header, &payload[..]).unwrap();
let metadata = br#"name = "filesystem"
version = "1.0.1"
revision = 3
description = "Base filesystem"
homepage = "https://example.test"
license = "Unlicense"
keep = ["etc/fstab"]
[dependencies]
runtime = []
optional = []
"#;
let mut meta_header = tar::Header::new_gnu();
meta_header.set_path(".metadata.toml").unwrap();
meta_header.set_size(metadata.len() as u64);
meta_header.set_mode(0o644);
meta_header.set_cksum();
tar.append(&meta_header, &metadata[..]).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
cfg.db_dir = rootfs.path().join("var/lib/depot");
fs::create_dir_all(rootfs.path().join("etc"))?;
fs::write(rootfs.path().join("etc/fstab"), "existing-fstab")?;
let (spec, staged) = load_package_archive_into_staging(&cfg, &archive_path)?;
assert_eq!(spec.build.flags.keep, vec!["etc/fstab".to_string()]);
let installed = install_package_outputs_to_rootfs(&spec, staged.path(), rootfs.path(), &cfg)?;
assert_eq!(installed.len(), 1);
assert_eq!(
fs::read_to_string(rootfs.path().join("etc/fstab"))?,
"existing-fstab"
);
assert_eq!(
fs::read_to_string(rootfs.path().join("etc/fstab.depotnew"))?,
"package-fstab"
);
Ok(())
}
#[test]
fn binary_archive_install_honors_replaces_from_metadata() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
let archive_path = pkg_dir.path().join("vx-0.1.0-1-x86_64.depot.pkg.tar.zst");
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
cfg.db_dir = rootfs.path().join("var/lib/depot");
let old_spec = package::PackageSpec {
package: package::PackageInfo {
name: "diffutils".into(),
real_name: None,
version: "3.12".into(),
revision: 1,
description: "diffutils".into(),
homepage: "https://example.test/diffutils".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["GPL-3.0-or-later".into()],
},
packages: Vec::new(),
alternatives: package::Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags::default(),
},
dependencies: package::Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let old_dest = rootfs.path().join("old-dest");
fs::create_dir_all(old_dest.join("usr/bin"))?;
fs::write(old_dest.join("usr/bin/diff"), "old-diff")?;
install_package_outputs_to_rootfs(&old_spec, &old_dest, rootfs.path(), &cfg)?;
let file = fs::File::create(&archive_path)
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
let encoder =
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
let mut tar = tar::Builder::new(encoder);
let payload = b"vx-diff";
let mut payload_header = tar::Header::new_gnu();
payload_header.set_path("usr/bin/diff")?;
payload_header.set_size(payload.len() as u64);
payload_header.set_mode(0o755);
payload_header.set_cksum();
tar.append(&payload_header, &payload[..])?;
let metadata = br#"name = "vx"
version = "0.1.0"
revision = 1
description = "vertex utils"
homepage = "https://example.test/vx"
license = "MIT"
replaces = ["diffutils"]
[dependencies]
runtime = []
optional = []
"#;
let mut meta_header = tar::Header::new_gnu();
meta_header.set_path(".metadata.toml")?;
meta_header.set_size(metadata.len() as u64);
meta_header.set_mode(0o644);
meta_header.set_cksum();
tar.append(&meta_header, &metadata[..])?;
let encoder = tar.into_inner()?;
encoder.finish()?;
let (spec, staged) = load_package_archive_into_staging(&cfg, &archive_path)?;
assert_eq!(spec.alternatives.replaces, vec!["diffutils".to_string()]);
let installed = install_package_outputs_to_rootfs(&spec, staged.path(), rootfs.path(), &cfg)?;
assert_eq!(installed.len(), 1);
assert!(installed[0].is_update);
assert_eq!(installed[0].package.name, "vx");
assert_eq!(
fs::read_to_string(rootfs.path().join("usr/bin/diff"))?,
"vx-diff"
);
let db_path = cfg.installed_db_path(rootfs.path());
assert_eq!(db::get_package_version(&db_path, "diffutils")?, None);
assert_eq!(
db::get_package_version(&db_path, "vx")?,
Some("0.1.0".into())
);
Ok(())
}
#[test]
fn merge_missing_dependencies_preserves_order_and_uniqueness() {
let merged = merge_missing_dependencies(
vec!["make".into(), "pkgconf".into(), "glibc".into()],
vec![
"glibc".into(),
"openssl".into(),
"pkgconf".into(),
"zlib".into(),
],
);
assert_eq!(merged, vec!["make", "pkgconf", "glibc", "openssl", "zlib"]);
}
#[test]
fn install_planned_packages_sets_sole_tool_provider_before_post_hooks() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut config = config::Config::for_rootfs(rootfs.path());
config.db_dir = rootfs.path().join("var/lib/depot");
config.build_dir = rootfs.path().join("var/cache/depot/build");
let mut dash_spec = test_package_spec(package::BuildType::Bin, None, &[]);
dash_spec.package.name = "dash".into();
let dash_dest = rootfs.path().join("dash-dest");
fs::create_dir_all(dash_dest.join("usr/bin"))?;
fs::create_dir_all(dash_dest.join("scripts"))?;
fs::write(dash_dest.join("usr/bin/dash"), "dash")?;
fs::write(
dash_dest.join("scripts/post_install"),
"[ -L \"$DEPOT_ROOTFS/usr/bin/sh\" ] && [ \"$(readlink \"$DEPOT_ROOTFS/usr/bin/sh\")\" = dash ]\n",
)?;
let plans = plan_package_outputs_for_install(&dash_spec, &dash_dest, rootfs.path(), &config)?;
install_planned_packages_to_rootfs(&plans, rootfs.path(), &config)?;
assert_eq!(
fs::read_link(rootfs.path().join("usr/bin/sh"))?,
PathBuf::from("dash")
);
Ok(())
}
#[test]
fn suppress_nested_install_output_for_planned_context() {
let mut env = TestEnv::new();
env.set_var(DEPOT_INSTALL_CONTEXT_ENV, INSTALL_CONTEXT_PLANNED);
assert!(suppress_nested_install_output());
assert_eq!(
current_install_invocation_context(),
InstallInvocationContext::Planned
);
}
#[test]
fn plan_dependency_closure_tracks_requested_dependency_roots() {
let plan = planner::ExecutionPlan {
steps: vec![
planner::PlannedStep {
package: "zlib".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/zlib/zlib.toml"),
local_sibling: false,
},
requested_by: vec!["cmake needs zlib".into()],
},
planner::PlannedStep {
package: "cmake".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/cmake/cmake.toml"),
local_sibling: false,
},
requested_by: vec!["dependency cmake".into()],
},
planner::PlannedStep {
package: "libffi".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/libffi/libffi.toml"),
local_sibling: false,
},
requested_by: vec!["python needs libffi".into()],
},
planner::PlannedStep {
package: "python".into(),
action: planner::PlanAction::InstallBinary,
origin: planner::PlanOrigin::Source {
path: PathBuf::from("packages/core/python/python.toml"),
local_sibling: false,
},
requested_by: vec!["dependency python".into()],
},
],
};
let cmake_closure = plan_dependency_closure_for_requested_deps(&plan, &["cmake".into()]);
assert_eq!(
cmake_closure,
HashSet::from(["cmake".to_string(), "zlib".to_string()])
);
let python_closure = plan_dependency_closure_for_requested_deps(&plan, &["python".into()]);
assert_eq!(
python_closure,
HashSet::from(["python".to_string(), "libffi".to_string()])
);
}
File diff suppressed because it is too large Load Diff
+161
View File
@@ -0,0 +1,161 @@
use super::*;
#[test]
fn extract_version_patterns_handles_git_and_release_urls() {
let git_patterns = extract_version_patterns("https://codeberg.org/Limine/limine.git#v$version");
assert!(git_patterns.contains(&VersionPattern {
prefix: "v".into(),
suffix: String::new(),
}));
let release_patterns = extract_version_patterns(
"https://github.com/Mic92/iana-etc/releases/download/$version/iana-etc-$version.tar.gz",
);
assert!(release_patterns.contains(&VersionPattern {
prefix: String::new(),
suffix: String::new(),
}));
}
#[test]
fn candidate_versions_from_refs_matches_version_patterns() {
let refs = vec![
"refs/tags/v10.8.3".to_string(),
"refs/tags/v10.8.4".to_string(),
"refs/heads/main".to_string(),
];
let patterns = extract_version_patterns("https://codeberg.org/Limine/limine.git#v$version");
let candidates = candidate_versions_from_refs(&refs, &patterns);
assert_eq!(candidates, vec!["10.8.3".to_string(), "10.8.4".to_string()]);
assert_eq!(
best_newer_version("10.8.3", candidates.iter().map(String::as_str)),
Some("10.8.4".to_string())
);
}
#[test]
fn best_newer_version_skips_branches_and_prereleases() {
let candidates = ["2", "1.10.0rc1", "1.10.0", "release-0.13"];
assert_eq!(
best_newer_version("1.9.5", candidates.into_iter()),
Some("1.10.0".to_string())
);
}
#[test]
fn best_newer_version_normalizes_date_style_tags() {
let candidates = ["lts_2026_01_07", "lts_2027_02_03"];
assert_eq!(
best_newer_version("20260107.1", candidates.into_iter()),
Some("20270203".to_string())
);
}
#[test]
fn remote_git_repository_from_github_release_url_maps_to_repo_git_url() {
let repo_url = remote_git_repository_from_source_url(
"https://github.com/Mic92/iana-etc/releases/download/20260202/iana-etc-20260202.tar.gz",
);
assert_eq!(
repo_url,
Some("https://github.com/Mic92/iana-etc.git".to_string())
);
}
#[test]
fn remote_git_repository_from_gitlab_archive_url_maps_to_repo_git_url() {
let repo_url = remote_git_repository_from_source_url(
"https://gitlab.com/graphviz/graphviz/-/archive/14.1.4/graphviz-14.1.4.tar.gz",
);
assert_eq!(
repo_url,
Some("https://gitlab.com/graphviz/graphviz.git".to_string())
);
}
#[test]
fn archive_listing_probe_uses_parent_of_first_version_segment() {
let probe = archive_listing_probe(
"https://downloads.example.test/dav1d/$version/dav1d-$version.tar.xz",
"https://downloads.example.test/dav1d/1.5.3/dav1d-1.5.3.tar.xz",
)
.expect("archive probe");
assert_eq!(probe.listing_url, "https://downloads.example.test/dav1d/");
assert_eq!(
probe.patterns,
vec![VersionPattern {
prefix: String::new(),
suffix: String::new(),
}]
);
}
#[test]
fn candidate_versions_from_listing_matches_archive_entries() {
let patterns = vec![VersionPattern {
prefix: "alsa-lib-".into(),
suffix: ".tar.bz2".into(),
}];
let html = r#"
<a href="alsa-lib-1.2.15.3.tar.bz2">alsa-lib-1.2.15.3.tar.bz2</a>
<a href="alsa-lib-1.2.16.tar.bz2">alsa-lib-1.2.16.tar.bz2</a>
"#;
assert_eq!(
candidate_versions_from_listing(html, &patterns),
vec!["1.2.15.3".to_string(), "1.2.16".to_string()]
);
}
#[test]
fn list_archive_versions_reads_simple_http_index() -> Result<()> {
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::thread;
let listener = TcpListener::bind("127.0.0.1:0").context("bind test listener")?;
let addr = listener.local_addr().context("listener addr")?;
let server = thread::spawn(move || -> Result<()> {
let (mut stream, _) = listener.accept().context("accept request")?;
let mut reader = BufReader::new(stream.try_clone().context("clone stream")?);
let mut request_line = String::new();
reader
.read_line(&mut request_line)
.context("read request line")?;
loop {
let mut line = String::new();
reader.read_line(&mut line).context("read header line")?;
if line == "\r\n" || line.is_empty() {
break;
}
}
assert!(request_line.starts_with("GET /pub/lib/ HTTP/1.1"));
let body = r#"
<html>
<a href="alsa-lib-1.2.15.3.tar.bz2">alsa-lib-1.2.15.3.tar.bz2</a>
<a href="alsa-lib-1.2.16.tar.bz2">alsa-lib-1.2.16.tar.bz2</a>
</html>
"#;
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
)
.context("write response")?;
stream.flush().context("flush response")?;
Ok(())
});
let probe = ArchiveListingProbe {
listing_url: format!("http://{addr}/pub/lib/"),
patterns: vec![VersionPattern {
prefix: "alsa-lib-".into(),
suffix: ".tar.bz2".into(),
}],
};
let versions = list_archive_versions(&probe)?;
server.join().expect("join server")?;
assert_eq!(versions, vec!["1.2.15.3".to_string(), "1.2.16".to_string()]);
Ok(())
}
+309 -120
View File
@@ -747,26 +747,131 @@ pub(crate) fn collect_missing_update_dependencies(
Ok(missing)
}
fn confirm_update_replacements(candidates: &[UpdateCandidate]) -> Result<()> {
for candidate in candidates {
if !candidate.replaces_installed {
continue;
}
if !ui::prompt_yes_no(
&format!(
"replace {} with {}?",
candidate.installed_package, candidate.candidate_package
),
true,
)? {
anyhow::bail!(
"Replacement declined: {} -> {}",
candidate.installed_package,
candidate.candidate_package
);
fn update_release(version: &str, revision: u32) -> String {
format!("{version}-{revision}")
}
fn package_word(count: usize) -> &'static str {
if count == 1 { "package" } else { "packages" }
}
fn update_package_label(candidate: &UpdateCandidate) -> String {
if candidate.replaces_installed {
format!(
"{} → {}",
candidate.installed_package, candidate.candidate_package
)
} else {
candidate.candidate_package.clone()
}
}
fn format_update_summary(
updates: &[UpdateCandidate],
missing_dependencies: &[String],
download_size: Option<u64>,
) -> String {
let labels: Vec<_> = updates.iter().map(update_package_label).collect();
let label_width = labels
.iter()
.map(|label| label.chars().count())
.max()
.unwrap_or(0);
let mut output = format!("Updates ({})\n", updates.len());
for (candidate, label) in updates.iter().zip(labels) {
let installed = update_release(&candidate.installed_version, candidate.installed_revision);
let available = update_release(&candidate.candidate_version, candidate.candidate_revision);
let newer_build = candidate.installed_version == candidate.candidate_version
&& candidate.installed_revision == candidate.candidate_revision
&& compare_completed_at(
candidate.candidate_completed_at,
candidate.installed_completed_at,
) == Ordering::Greater;
let build_note = if newer_build { " (newer build)" } else { "" };
output.push_str(&format!(
" {label:<label_width$} {installed} → {available}{build_note}\n"
));
}
if let Some(bytes) = download_size {
output.push_str(&format!("\nDownload: {}\n", human_bytes(bytes)));
}
if !missing_dependencies.is_empty() {
output.push_str(&format!(
"Dependencies: {}\n",
missing_dependencies.join(", ")
));
}
output.push('\n');
output
}
fn planned_update_download_size(
updates: &[UpdateCandidate],
dependency_plan: Option<&planner::ExecutionPlan>,
) -> Option<u64> {
let mut total = 0u64;
for candidate in updates {
let UpdateOrigin::Binary { record, .. } = &candidate.origin else {
return None;
};
total = total.checked_add(record.size)?;
}
if let Some(plan) = dependency_plan {
for step in plan.actionable_steps() {
match &step.origin {
planner::PlanOrigin::Binary { record, .. } => {
total = total.checked_add(record.size)?;
}
planner::PlanOrigin::Source { .. } => return None,
planner::PlanOrigin::Installed => {}
}
}
}
Ok(())
Some(total)
}
fn start_update_status(package_count: usize) -> (ProgressBar, bool) {
let interactive = std::io::stdout().is_terminal() && std::io::stderr().is_terminal();
if !interactive {
println!(
"Updating {package_count} {}...",
package_word(package_count)
);
return (ProgressBar::hidden(), false);
}
let status = ProgressBar::new_spinner();
status.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner()),
);
status.enable_steady_tick(std::time::Duration::from_millis(80));
status.set_message(format!(
"Updating {package_count} {}",
package_word(package_count)
));
(status, true)
}
fn print_update_complete(updates: &[UpdateCandidate], interactive: bool) {
if interactive {
println!("Updated {} {}", updates.len(), package_word(updates.len()));
return;
}
for candidate in updates {
println!(
"Updated {} {} → {}",
update_package_label(candidate),
update_release(&candidate.installed_version, candidate.installed_revision),
update_release(&candidate.candidate_version, candidate.candidate_revision)
);
}
}
pub(crate) fn run_update_command(
@@ -776,36 +881,10 @@ pub(crate) fn run_update_command(
) -> Result<()> {
let updates = collect_update_candidates(config, options.rootfs, packages)?;
if updates.is_empty() {
ui::info("All installed packages are up to date.");
println!("All packages are up to date.");
return Ok(());
}
let targets: Vec<String> = updates
.iter()
.map(|candidate| {
let summary = format!(
"{} v{}-{} -> {} v{}-{}",
candidate.installed_package,
candidate.installed_version,
candidate.installed_revision,
candidate.candidate_package,
candidate.candidate_version,
candidate.candidate_revision
);
if candidate.installed_version == candidate.candidate_version
&& candidate.installed_revision == candidate.candidate_revision
&& compare_completed_at(
candidate.candidate_completed_at,
candidate.installed_completed_at,
) == Ordering::Greater
{
format!("{summary} (newer UTC build timestamp)")
} else {
summary
}
})
.collect();
let conflict_subjects: Vec<_> = updates
.iter()
.map(|candidate| super::super::InstallConflictSubject {
@@ -816,32 +895,39 @@ pub(crate) fn run_update_command(
.collect();
super::super::validate_no_transaction_conflicts(&conflict_subjects)?;
ui::info(format!("{} package(s) can be updated:", updates.len()));
for target in &targets {
ui::info(format!(" {}", target));
}
let db_path = config.installed_db_path(options.rootfs);
let missing_deps = if options.no_deps {
Vec::new()
} else {
collect_missing_update_dependencies(&updates, &db_path)?
};
if !missing_deps.is_empty() {
ui::info(format!(
"New dependencies required by updates: {}",
missing_deps.join(", ")
));
}
if !options.dry_run && !ui::prompt_package_action("update", &targets, true)? {
let dependency_plan = if missing_deps.is_empty() {
None
} else {
Some(planner::build_dependency_install_plan(
config,
options.rootfs,
&missing_deps,
planner::PlannerOptions {
assume_yes: options.assume_yes,
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root: None,
include_test_deps: options.install_test_deps,
lib32_only_requested_specs: false,
},
)?)
};
let download_size = planned_update_download_size(&updates, dependency_plan.as_ref());
print!(
"{}",
format_update_summary(&updates, &missing_deps, download_size)
);
if !options.dry_run && !options.assume_yes && !ui::prompt_yes_no("Proceed?", true)? {
anyhow::bail!("Aborted");
}
if !options.dry_run {
confirm_update_replacements(&updates)?;
}
if options.dry_run {
super::super::resolve_installed_conflicts_for_subjects(
&conflict_subjects,
@@ -851,70 +937,173 @@ pub(crate) fn run_update_command(
)?;
}
let mut transaction_requests = Vec::new();
if !missing_deps.is_empty() {
let dep_plan = planner::build_dependency_install_plan(
config,
options.rootfs,
&missing_deps,
planner::PlannerOptions {
assume_yes: options.assume_yes,
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root: None,
include_test_deps: options.install_test_deps,
lib32_only_requested_specs: false,
},
)?;
crate::commands::print_plan_summary(&dep_plan);
if options.dry_run {
ui::info("Dry run enabled, stopping before dependency installation/update.");
return Ok(());
if options.dry_run {
if let Some(plan) = dependency_plan.as_ref() {
crate::commands::print_plan_summary(plan);
}
transaction_requests.extend(super::super::install_requests_for_plan(
&dep_plan,
config,
options.rootfs,
)?);
} else if options.dry_run {
ui::info("Dry run enabled, stopping before update.");
println!("Dry run: no changes made.");
return Ok(());
}
for (idx, candidate) in updates.iter().enumerate() {
ui::info(format!(
"[{}/{}] staging update {} v{}-{} -> {} v{}-{}",
idx + 1,
updates.len(),
candidate.installed_package,
candidate.installed_version,
candidate.installed_revision,
candidate.candidate_package,
candidate.candidate_version,
candidate.candidate_revision
));
let request = candidate_request_path(candidate, config, options.rootfs)?;
transaction_requests.push(request);
}
super::super::run_update_transaction_install_requests(
super::super::DirectInstallOptions {
rootfs: options.rootfs,
no_deps: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
dry_run: false,
lib32_only: false,
install_test_deps: options.install_test_deps,
},
config,
&transaction_requests,
)?;
let (status, interactive) = start_update_status(updates.len());
let update_result = (|| -> Result<()> {
let mut transaction_requests = Vec::new();
if let Some(plan) = dependency_plan.as_ref() {
crate::commands::print_plan_summary(plan);
transaction_requests.extend(super::super::install_requests_for_plan(
plan,
config,
options.rootfs,
)?);
}
if options.clean {
crate::commands::clean_build_workspace(config)?;
}
for candidate in &updates {
status.set_message(format!(
"Updating {} {} · preparing {}",
updates.len(),
package_word(updates.len()),
candidate.candidate_package
));
let request = candidate_request_path(candidate, config, options.rootfs)?;
transaction_requests.push(request);
}
status.set_message(format!(
"Updating {} {} · installing",
updates.len(),
package_word(updates.len())
));
super::super::run_update_transaction_install_requests(
super::super::DirectInstallOptions {
rootfs: options.rootfs,
no_deps: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
dry_run: false,
lib32_only: false,
install_test_deps: options.install_test_deps,
},
config,
&transaction_requests,
)?;
if options.clean {
crate::commands::clean_build_workspace(config)?;
}
Ok(())
})();
status.finish_and_clear();
update_result?;
print_update_complete(&updates, interactive);
Ok(())
}
#[cfg(test)]
mod ux_tests {
use super::*;
fn binary_record(name: &str, version: &str, size: u64) -> db::repo::BinaryRepoPackageRecord {
db::repo::BinaryRepoPackageRecord {
repo_name: "core".into(),
name: name.into(),
real_name: None,
version: version.into(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: format!("{name}-{version}-1-x86_64.depot.pkg.tar.zst"),
size,
sha512: String::new(),
description: None,
homepage: None,
license: None,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
}
}
fn binary_update(
name: &str,
installed_version: &str,
candidate_version: &str,
size: u64,
) -> UpdateCandidate {
UpdateCandidate {
installed_package: name.into(),
candidate_package: name.into(),
replaces_installed: false,
installed_version: installed_version.into(),
installed_revision: 1,
installed_completed_at: None,
candidate_version: candidate_version.into(),
candidate_revision: 1,
candidate_completed_at: None,
runtime_dependencies: Vec::new(),
provides: Vec::new(),
conflicts: Vec::new(),
repo_priority: 0,
origin: UpdateOrigin::Binary {
repo_name: "core".into(),
record: Box::new(binary_record(name, candidate_version, size)),
},
}
}
#[test]
fn update_summary_is_compact_and_aligned() {
let updates = [
binary_update("curl", "8.11.0", "8.12.0", 3 * 1024),
binary_update("openssl", "3.4.0", "3.4.1", 5 * 1024),
];
assert_eq!(
format_update_summary(&updates, &["ca-certificates".into()], Some(8 * 1024)),
concat!(
"Updates (2)\n",
" curl 8.11.0-1 → 8.12.0-1\n",
" openssl 3.4.0-1 → 3.4.1-1\n",
"\n",
"Download: 8.0 KiB\n",
"Dependencies: ca-certificates\n",
"\n",
)
);
}
#[test]
fn update_summary_marks_replacements_and_newer_builds() {
let mut update = binary_update("icu78", "78.1", "79.1", 1024);
update.candidate_package = "icu79".into();
update.replaces_installed = true;
update.installed_version = "79.1".into();
update.installed_completed_at = Some(1);
update.candidate_completed_at = Some(2);
let summary = format_update_summary(&[update], &[], Some(1024));
assert!(summary.contains("icu78 → icu79"));
assert!(summary.contains("79.1-1 → 79.1-1 (newer build)"));
}
#[test]
fn planned_download_size_requires_all_binary_inputs() {
let binary = binary_update("curl", "8.11.0", "8.12.0", 3 * 1024);
assert_eq!(
planned_update_download_size(std::slice::from_ref(&binary), None),
Some(3 * 1024)
);
let mut source = binary;
source.origin = UpdateOrigin::Source {
repo_name: "core".into(),
path: PathBuf::from("/repo/curl.toml"),
};
assert_eq!(planned_update_download_size(&[source], None), None);
}
}
Executable → Regular
+36 -492
View File
@@ -6,6 +6,7 @@ use crate::package::PackageSpec;
use crate::staging;
use anyhow::{Context, Result};
use rusqlite::{Connection, OptionalExtension, params};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
@@ -24,10 +25,43 @@ fn should_ignore_sbase_conflicts() -> bool {
std::env::var_os(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS).is_some()
}
fn should_auto_clear_conflict(owner: &str, path: &str) -> bool {
pub(crate) fn should_auto_clear_conflict(owner: &str, path: &str) -> bool {
(owner == "sbase" && should_ignore_sbase_conflicts()) || is_auto_removable_path(path)
}
/// Return every installed file path and its owning package.
pub(crate) fn get_file_ownership(db_path: &Path) -> Result<BTreeMap<String, String>> {
if !db_path.exists() {
return Ok(BTreeMap::new());
}
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open package database at {}", db_path.display()))?;
init_db(&conn).with_context(|| {
format!(
"Failed to initialize package database at {}",
db_path.display()
)
})?;
let mut stmt = conn
.prepare(
"SELECT f.path, p.name
FROM files f
JOIN packages p ON p.id = f.package_id
ORDER BY f.path, p.name",
)
.context("Failed to prepare installed file ownership query")?;
let rows = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.context("Failed to query installed file ownership")?;
let mut ownership = BTreeMap::new();
for row in rows {
let (path, owner) = row.context("Failed to read installed file ownership row")?;
ownership.insert(path, owner);
}
Ok(ownership)
}
/// Installed package row from the local package database.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledPackageRecord {
@@ -1204,494 +1238,4 @@ pub fn owns_path(db_path: &Path, path: &Path) -> Result<Option<String>> {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
};
use crate::test_support::TestEnv;
use std::path::PathBuf;
fn mk_spec(name: &str, version: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: name.into(),
real_name: None,
version: version.into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives {
provides: vec![format!("{}-virtual", name)],
conflicts: Vec::new(),
replaces: Vec::new(),
lib32: None,
},
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
#[test]
fn register_package_updates_in_place_and_replaces_file_list() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let spec_v1 = mk_spec("foo", "1.0");
let dest1 = tmp.path().join("dest1");
std::fs::create_dir_all(dest1.join("usr/bin")).unwrap();
std::fs::write(dest1.join("usr/bin/foo"), "v1").unwrap();
register_package(&db_path, &spec_v1, &dest1).unwrap();
// Capture package id
let conn = Connection::open(&db_path).unwrap();
let id1: i64 = conn
.query_row(
"SELECT id FROM packages WHERE name = ?1",
params!["foo"],
|r| r.get(0),
)
.unwrap();
// Update with different file set
let spec_v2 = mk_spec("foo", "2.0");
let dest2 = tmp.path().join("dest2");
std::fs::create_dir_all(dest2.join("usr/bin")).unwrap();
std::fs::write(dest2.join("usr/bin/foo"), "v2").unwrap();
std::fs::write(dest2.join("usr/bin/new_only"), "x").unwrap();
register_package(&db_path, &spec_v2, &dest2).unwrap();
let id2: i64 = conn
.query_row(
"SELECT id FROM packages WHERE name = ?1",
params!["foo"],
|r| r.get(0),
)
.unwrap();
assert_eq!(id1, id2);
let files = get_package_files(&db_path, "foo").unwrap();
assert!(files.contains(&"usr/bin/foo".to_string()));
assert!(files.contains(&"usr/bin/new_only".to_string()));
let version = get_package_version(&db_path, "foo").unwrap();
assert_eq!(version.as_deref(), Some("2.0"));
}
#[test]
fn installed_dependency_names_include_real_name_aliases() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&destdir).unwrap();
let mut spec = mk_spec("libressl43", "4.3.2");
spec.package.real_name = Some("libressl".into());
register_package(&db_path, &spec, &destdir).unwrap();
let names = get_installed_dependency_names(&db_path).unwrap();
assert!(names.contains("libressl43"));
assert!(names.contains("libressl"));
assert_eq!(
get_dependency_version(&db_path, "libressl")
.unwrap()
.as_deref(),
Some("4.3.2")
);
}
#[test]
fn register_package_uses_metadata_completed_at_when_present() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let spec = mk_spec("foo", "1.0");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "bin").unwrap();
std::fs::write(
dest.join(".metadata.toml"),
"completed_at = \"2026-03-10T12:34:56Z\"\n",
)
.unwrap();
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].completed_at, Some(1_773_146_096));
}
#[test]
fn register_package_falls_back_to_destdir_mtime_when_metadata_missing() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let spec = mk_spec("foo", "1.0");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
let file = dest.join("usr/bin/foo");
std::fs::write(&file, "bin").unwrap();
let ts = filetime::FileTime::from_unix_time(1_700_000_000, 0);
filetime::set_file_mtime(&file, ts).unwrap();
filetime::set_file_mtime(dest.join("usr"), ts).unwrap();
filetime::set_file_mtime(dest.join("usr/bin"), ts).unwrap();
filetime::set_file_mtime(&dest, ts).unwrap();
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].completed_at, Some(1_700_000_000));
}
#[test]
fn register_package_detects_conflicting_files() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
// Install package 'alpha' owning usr/bin/shared
let spec_a = mk_spec("alpha", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
std::fs::write(dest_a.join("usr/bin/shared"), "a").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
// Try to install package 'beta' that also includes the same path -> should fail
let spec_b = mk_spec("beta", "1.0");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
std::fs::write(dest_b.join("usr/bin/shared"), "b").unwrap();
let res = register_package(&db_path, &spec_b, &dest_b);
assert!(res.is_err());
let err = format!("{}", res.err().unwrap());
assert!(err.contains("File ownership conflict detected"));
assert!(err.contains("usr/bin/shared"));
assert!(err.contains("alpha"));
}
#[test]
fn register_package_auto_clears_safe_conflicts() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
// Install package 'alpha' owning a known shared Perl path
let spec_a = mk_spec("alpha", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/share/perl5")).unwrap();
std::fs::write(dest_a.join("usr/share/perl5/shared.pm"), "package A;").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
// Now install package 'beta' that also provides the same shared path -> should auto-clear
let spec_b = mk_spec("beta", "1.0");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/share/perl5")).unwrap();
std::fs::write(dest_b.join("usr/share/perl5/shared.pm"), "package B;").unwrap();
// This should succeed and transfer ownership of the shared path to beta
register_package(&db_path, &spec_b, &dest_b).unwrap();
// Verify DB: alpha should no longer own the path, beta should
let files_a = get_package_files(&db_path, "alpha").unwrap();
assert!(!files_a.contains(&"usr/share/perl5/shared.pm".to_string()));
let files_b = get_package_files(&db_path, "beta").unwrap();
assert!(files_b.contains(&"usr/share/perl5/shared.pm".to_string()));
}
#[test]
fn register_package_auto_clears_sbase_conflicts_when_requested() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("rootfs");
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
std::fs::write(rootfs.join("usr/bin/find"), "sbase find").unwrap();
let spec_a = mk_spec("sbase", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
std::fs::write(dest_a.join("usr/bin/find"), "sbase find").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
let spec_b = mk_spec("bfs", "4.1");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
std::fs::write(dest_b.join("usr/bin/find"), "bfs find").unwrap();
let mut env = TestEnv::new();
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
register_package(&db_path, &spec_b, &dest_b).unwrap();
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
assert!(!files_sbase.contains(&"usr/bin/find".to_string()));
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
assert!(files_bfs.contains(&"usr/bin/find".to_string()));
}
#[test]
fn register_package_auto_clear_preserves_new_payload_on_disk() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("rootfs");
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
std::fs::write(rootfs.join("usr/bin/find"), "sbase find").unwrap();
let spec_a = mk_spec("sbase", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
std::fs::write(dest_a.join("usr/bin/find"), "sbase find").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
std::fs::write(rootfs.join("usr/bin/find"), "bfs find").unwrap();
let spec_b = mk_spec("bfs", "4.1");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
std::fs::write(dest_b.join("usr/bin/find"), "bfs find").unwrap();
let mut env = TestEnv::new();
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
register_package(&db_path, &spec_b, &dest_b).unwrap();
assert_eq!(
std::fs::read_to_string(rootfs.join("usr/bin/find")).unwrap(),
"bfs find"
);
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
assert!(!files_sbase.contains(&"usr/bin/find".to_string()));
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
assert!(files_bfs.contains(&"usr/bin/find".to_string()));
}
#[test]
fn get_package_files_missing_package_returns_empty() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
// Create an empty database file with schema but no packages
let conn = Connection::open(&db_path).unwrap();
init_db(&conn).unwrap();
drop(conn);
// Querying files for a package that doesn't exist should return an empty list
let files = get_package_files(&db_path, "nonexistent").unwrap();
assert!(files.is_empty());
}
#[test]
fn get_package_version_missing_db_returns_none_without_creating_db() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let version = get_package_version(&db_path, "nonexistent").unwrap();
assert!(version.is_none());
assert!(!db_path.exists());
}
#[test]
fn calculate_upgrade_paths_handles_existing_db_file_without_schema() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
std::fs::File::create(&db_path).unwrap();
let manifest = staging::Manifest {
files: vec!["usr/bin/foo".to_string()],
directories: Vec::new(),
};
let remove_paths = calculate_upgrade_paths(&db_path, "nonexistent", &manifest).unwrap();
assert!(remove_paths.is_empty());
}
#[test]
fn remove_package_tolerates_missing_files_and_cleans_db() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&rootfs).unwrap();
let spec = mk_spec("foo", "1.0");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "bin").unwrap();
register_package(&db_path, &spec, &dest).unwrap();
// Create the installed file in rootfs (one real)
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
std::fs::write(rootfs.join("usr/bin/foo"), "bin").unwrap();
// Inject an extra missing file into DB to ensure we tolerate it.
let conn = Connection::open(&db_path).unwrap();
let pkg_id: i64 = conn
.query_row(
"SELECT id FROM packages WHERE name = ?1",
params!["foo"],
|r| r.get(0),
)
.unwrap();
conn.execute(
"INSERT OR IGNORE INTO files (package_id, path) VALUES (?1, ?2)",
params![pkg_id, "usr/bin/does_not_exist"],
)
.unwrap();
remove_package(&db_path, "foo", &rootfs).unwrap();
assert!(get_package_version(&db_path, "foo").unwrap().is_none());
}
#[test]
fn test_package_upgrade_removes_orphaned_files() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let rootfs = tmp.path().join("root");
let tx_base = tmp.path().join("tx");
std::fs::create_dir_all(&rootfs).unwrap();
// 1. Install v1: usr/bin/foo, usr/bin/shared_dir/old_file
let spec_v1 = mk_spec("foo", "1.0");
let dest1 = tmp.path().join("dest1");
std::fs::create_dir_all(dest1.join("usr/bin/shared_dir")).unwrap();
std::fs::write(dest1.join("usr/bin/foo"), "v1").unwrap();
std::fs::write(dest1.join("usr/bin/shared_dir/old_file"), "old").unwrap();
register_package(&db_path, &spec_v1, &dest1).unwrap();
let _ = crate::staging::install_atomic(&dest1, &rootfs, &tx_base, &[], &[]).unwrap();
assert!(rootfs.join("usr/bin/foo").exists());
assert!(rootfs.join("usr/bin/shared_dir/old_file").exists());
// 2. Prepare v2: usr/bin/foo (updated), usr/bin/new_file
// (shared_dir/old_file is removed from spec)
let spec_v2 = mk_spec("foo", "2.0");
let dest2 = tmp.path().join("dest2");
std::fs::create_dir_all(dest2.join("usr/bin")).unwrap();
std::fs::write(dest2.join("usr/bin/foo"), "v2").unwrap();
std::fs::write(dest2.join("usr/bin/new_file"), "new").unwrap();
let manifest2 = crate::staging::generate_manifest_with_dirs(&dest2).unwrap();
let remove_paths = calculate_upgrade_paths(&db_path, "foo", &manifest2).unwrap();
assert_eq!(
remove_paths,
vec![
"usr/bin/shared_dir/old_file".to_string(),
"usr/bin/shared_dir".to_string()
]
);
let tx =
crate::staging::install_atomic(&dest2, &rootfs, &tx_base, &remove_paths, &[]).unwrap();
register_package(&db_path, &spec_v2, &dest2).unwrap();
tx.commit().unwrap();
// 3. Verify filesystem
assert_eq!(
std::fs::read_to_string(rootfs.join("usr/bin/foo")).unwrap(),
"v2"
);
assert!(rootfs.join("usr/bin/new_file").exists());
assert!(!rootfs.join("usr/bin/shared_dir/old_file").exists());
// Check DB
let files = get_package_files(&db_path, "foo").unwrap();
assert!(files.contains(&"usr/bin/foo".to_string()));
assert!(files.contains(&"usr/bin/new_file".to_string()));
assert!(!files.contains(&"usr/bin/shared_dir/old_file".to_string()));
}
#[test]
fn register_package_persists_replacements() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/vx"), "vx").unwrap();
let mut spec = mk_spec("vx", "1.0");
spec.alternatives.replaces = vec!["grep".into(), "patch".into()];
register_package(&db_path, &spec, &dest).unwrap();
let replaces = get_all_replaces(&db_path).unwrap();
assert!(replaces.contains("grep"));
assert!(replaces.contains("patch"));
}
#[test]
fn register_package_persists_groups() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "foo").unwrap();
let mut spec = mk_spec("foo", "1.0");
spec.dependencies.groups = vec!["base".into(), "desktop".into()];
register_package(&db_path, &spec, &dest).unwrap();
assert_eq!(
get_package_groups(&db_path, "foo").unwrap(),
vec!["base".to_string(), "desktop".to_string()]
);
}
#[test]
fn register_package_records_built_against_metadata() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(&dest).unwrap();
std::fs::write(dest.join("payload"), "x").unwrap();
let mut spec = mk_spec("app", "1.0");
spec.package.built_against = vec!["icu78".into()];
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].built_against, vec!["icu78".to_string()]);
}
#[test]
fn installed_group_helpers_round_trip_membership() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "foo").unwrap();
let mut spec = mk_spec("foo", "1.0");
spec.dependencies.groups = vec!["base".into()];
register_package(&db_path, &spec, &dest).unwrap();
record_installed_groups(&db_path, &[String::from("base")]).unwrap();
assert!(is_installed_group(&db_path, "base").unwrap());
assert_eq!(
get_packages_in_installed_group(&db_path, "base").unwrap(),
vec!["foo".to_string()]
);
remove_installed_group(&db_path, "base").unwrap();
assert!(!is_installed_group(&db_path, "base").unwrap());
}
}
mod tests;
+11 -3465
View File
File diff suppressed because it is too large Load Diff
+412
View File
@@ -0,0 +1,412 @@
use super::*;
pub(super) fn verify_binary_package_record_checksums(
path: &Path,
rec: &BinaryRepoPackageRecord,
) -> Result<()> {
let expected = expected_binary_package_sha512(path, rec)?;
let mut file =
fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mut hasher = Sha512::new();
let mut buf = [0u8; 64 * 1024];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
verify_binary_package_sha512_digest(
path,
rec,
&expected,
&crate::hex::encode_lower(hasher.finalize()),
)
}
pub(super) fn expected_binary_package_sha512(
path: &Path,
rec: &BinaryRepoPackageRecord,
) -> Result<String> {
let expected = rec.sha512.trim().to_ascii_lowercase();
if expected.is_empty() {
anyhow::bail!(
"Missing SHA-512 checksum for {} from repo '{}'",
path.display(),
rec.repo_name
);
}
Ok(expected)
}
pub(super) fn verify_binary_package_sha512_digest(
path: &Path,
rec: &BinaryRepoPackageRecord,
expected: &str,
actual: &str,
) -> Result<()> {
if actual != expected {
anyhow::bail!(
"SHA-512 mismatch for {} from repo '{}'",
path.display(),
rec.repo_name
);
}
Ok(())
}
pub(super) struct Sha512Reader<R> {
inner: R,
hasher: Sha512,
}
impl<R> Sha512Reader<R> {
fn new(inner: R) -> Self {
Self {
inner,
hasher: Sha512::new(),
}
}
fn finalize_hex(self) -> String {
crate::hex::encode_lower(self.hasher.finalize())
}
}
impl<R: Read> Read for Sha512Reader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read = self.inner.read(buf)?;
if read > 0 {
self.hasher.update(&buf[..read]);
}
Ok(read)
}
}
impl<R: Seek> Seek for Sha512Reader<R> {
fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
if position != SeekFrom::Start(0) {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"checksum reader only supports rewinding to the start",
));
}
let position = self.inner.seek(position)?;
self.hasher = Sha512::new();
Ok(position)
}
}
pub(super) fn download_binary_package_archive(
client: &reqwest::blocking::Client,
pkg_url: &str,
tmp_path: &Path,
progress_cb: &mut Option<&mut dyn FnMut(u64, Option<u64>)>,
) -> Result<()> {
match copy_file_url_to_path(pkg_url, tmp_path)? {
FileUrlCopyOutcome::Copied => {
if let Some(cb) = progress_cb.as_mut() {
let total = fs::metadata(tmp_path).map(|m| m.len()).unwrap_or(0);
cb(total, Some(total));
}
}
FileUrlCopyOutcome::Missing => {
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
}
FileUrlCopyOutcome::NotFileUrl => {
let mut resp = client
.get(pkg_url)
.send()
.with_context(|| format!("Failed to fetch {}", pkg_url))?;
if !resp.status().is_success() {
anyhow::bail!("Failed to fetch {}: HTTP {}", pkg_url, resp.status());
}
let total = resp.content_length();
if let Some(cb) = progress_cb.as_mut() {
cb(0, total);
}
let mut out = fs::File::create(tmp_path)
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
let mut downloaded = 0u64;
let mut buf = [0u8; 64 * 1024];
loop {
let n = resp
.read(&mut buf)
.with_context(|| format!("Failed to read {}", pkg_url))?;
if n == 0 {
break;
}
out.write_all(&buf[..n])
.with_context(|| format!("Failed to save {}", tmp_path.display()))?;
downloaded = downloaded.saturating_add(n as u64);
if let Some(cb) = progress_cb.as_mut() {
cb(downloaded, total);
}
}
out.flush()
.with_context(|| format!("Failed to flush {}", tmp_path.display()))?;
}
}
Ok(())
}
pub(super) fn fetch_binary_package_signature(
repo_name: &str,
repo: &crate::config::BinaryRepo,
client: &reqwest::blocking::Client,
sig_url: &str,
sig_path: &Path,
) -> Result<bool> {
let found = fetch_url_to_path(client, sig_url, sig_path)?;
if !found {
if !repo.allow_unsigned {
anyhow::bail!(
"Failed to fetch detached signature for binary package in repo '{}' at {}",
repo_name,
sig_url
);
}
crate::log_warn!(
"Detached package signature missing for binary repo '{}' at {}; allow_unsigned=true so continuing",
repo_name,
sig_url
);
}
Ok(found)
}
pub(super) fn verify_binary_package_signature_with_trusted_keys(
repo_name: &str,
repo: &crate::config::BinaryRepo,
pkg_path: &Path,
sig_path: &Path,
trusted_keys: &[crate::signing::TrustedPublicKey],
) -> Result<()> {
if !sig_path.exists() {
if repo.allow_unsigned {
return Ok(());
}
anyhow::bail!(
"Detached package signature required but missing for {}",
pkg_path.display()
);
}
if trusted_keys.is_empty() {
if repo.allow_unsigned {
crate::log_warn!(
"No trusted minisign public key found; skipping package signature verification for binary repo '{}' because allow_unsigned=true",
repo_name
);
return Ok(());
}
anyhow::bail!(
"No trusted minisign public key found for detached package signature verification in binary repo '{}'",
repo_name
);
}
let _verified_key = crate::signing::verify_zst_file_detached_with_trusted_keys(
pkg_path,
sig_path,
trusted_keys,
)
.with_context(|| {
format!(
"Failed to verify detached package signature for {}",
pkg_path.display()
)
})?;
Ok(())
}
/// Ensure a binary package archive and detached signature are present in cache
/// without performing checksum/signature verification.
pub fn cache_binary_package_archive(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rec: &BinaryRepoPackageRecord,
package_cache_dir: &Path,
) -> Result<BinaryRepoCachedArchive> {
cache_binary_package_archive_with_progress(repo_name, repo, rec, package_cache_dir, None)
}
/// Ensure a binary package archive and detached signature are present in cache
/// without performing checksum/signature verification, optionally reporting
/// download progress.
pub fn cache_binary_package_archive_with_progress(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rec: &BinaryRepoPackageRecord,
package_cache_dir: &Path,
progress_cb: Option<&mut dyn FnMut(u64, Option<u64>)>,
) -> Result<BinaryRepoCachedArchive> {
let client = binary_package_http_client()?;
cache_binary_package_archive_with_client_and_progress(
repo_name,
repo,
rec,
package_cache_dir,
&client,
progress_cb,
)
}
pub(crate) fn binary_package_http_client() -> Result<reqwest::blocking::Client> {
reqwest::blocking::Client::builder()
.build()
.context("Failed to build HTTP client for binary package fetch")
}
pub(crate) fn cache_binary_package_archive_with_client_and_progress(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rec: &BinaryRepoPackageRecord,
package_cache_dir: &Path,
client: &reqwest::blocking::Client,
mut progress_cb: Option<&mut dyn FnMut(u64, Option<u64>)>,
) -> Result<BinaryRepoCachedArchive> {
let machine_arch = std::env::consts::ARCH;
let base_url = repo.effective_url_for_arch(machine_arch).with_context(|| {
format!(
"Binary repo '{}' is not configured for machine arch '{}'",
repo_name, machine_arch
)
})?;
let cache_dir = binary_repo_packages_cache_dir(package_cache_dir, repo_name);
fs::create_dir_all(&cache_dir)
.with_context(|| format!("Failed to create {}", cache_dir.display()))?;
let package_path = cache_dir.join(&rec.filename);
let signature_path = cache_dir.join(format!("{}.sig", rec.filename));
let tmp_path = cache_dir.join(format!("{}.tmp", rec.filename));
let tmp_sig_path = cache_dir.join(format!("{}.sig.tmp", rec.filename));
let pkg_url = join_repo_url(base_url, &rec.filename)?;
let sig_url = join_repo_url(base_url, &format!("{}.sig", rec.filename))?;
let package_downloaded = if !package_path.exists() {
download_binary_package_archive(client, &pkg_url, &tmp_path, &mut progress_cb)?;
fs::rename(&tmp_path, &package_path).with_context(|| {
format!(
"Failed to move {} to {}",
tmp_path.display(),
package_path.display()
)
})?;
true
} else {
if let Some(cb) = progress_cb.as_mut() {
let total = fs::metadata(&package_path)
.with_context(|| format!("Failed to stat {}", package_path.display()))?
.len();
cb(total, Some(total));
}
false
};
if package_downloaded || !signature_path.exists() {
let sig_downloaded =
fetch_binary_package_signature(repo_name, repo, client, &sig_url, &tmp_sig_path)?;
if sig_downloaded {
fs::rename(&tmp_sig_path, &signature_path).with_context(|| {
format!(
"Failed to move {} to {}",
tmp_sig_path.display(),
signature_path.display()
)
})?;
} else {
let _ = fs::remove_file(&signature_path);
}
}
Ok(BinaryRepoCachedArchive {
package_path,
signature_path,
})
}
/// Verify a cached/downloaded package archive against checksums from signed
/// repository metadata.
pub fn verify_binary_package_archive_checksums(
archive_path: &Path,
rec: &BinaryRepoPackageRecord,
) -> Result<()> {
verify_binary_package_record_checksums(archive_path, rec)
}
pub(crate) fn verify_binary_package_archive_integrity_with_trusted_keys(
repo_name: &str,
repo: &crate::config::BinaryRepo,
record: &BinaryRepoPackageRecord,
package_path: &Path,
signature_path: &Path,
trusted_keys: &[crate::signing::TrustedPublicKey],
) -> Result<()> {
if signature_path.exists() && !trusted_keys.is_empty() {
let expected = expected_binary_package_sha512(package_path, record)?;
let file = fs::File::open(package_path)
.with_context(|| format!("Failed to open {}", package_path.display()))?;
let mut reader = Sha512Reader::new(file);
let _verified_key = crate::signing::verify_reader_detached_with_trusted_keys(
&mut reader,
package_path,
signature_path,
trusted_keys,
)
.with_context(|| {
format!(
"Failed to verify detached package signature for {}",
package_path.display()
)
})?;
let actual = reader.finalize_hex();
verify_binary_package_sha512_digest(package_path, record, &expected, &actual)?;
return Ok(());
}
verify_binary_package_archive_checksums(package_path, record)?;
verify_binary_package_signature_with_trusted_keys(
repo_name,
repo,
package_path,
signature_path,
trusted_keys,
)
}
/// Download a binary package archive and verify it against detached signatures
/// and checksums from signed repository metadata.
pub fn fetch_binary_package_archive(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
rec: &BinaryRepoPackageRecord,
package_cache_dir: &Path,
) -> Result<PathBuf> {
let cached = cache_binary_package_archive(repo_name, repo, rec, package_cache_dir)?;
let trusted_keys = if cached.signature_path.exists() {
crate::signing::load_trusted_public_keys(rootfs)?
} else {
Vec::new()
};
verify_binary_package_archive_integrity_with_trusted_keys(
repo_name,
repo,
rec,
&cached.package_path,
&cached.signature_path,
&trusted_keys,
)
.with_context(|| {
format!(
"Binary package failed integrity verification: {}",
cached.package_path.display()
)
})?;
Ok(cached.package_path)
}
+700
View File
@@ -0,0 +1,700 @@
use super::*;
pub(super) fn binary_repo_cache_dir(package_cache_dir: &Path, repo_name: &str) -> PathBuf {
package_cache_dir.join("repos").join(repo_name)
}
pub(super) fn binary_repo_packages_cache_dir(package_cache_dir: &Path, repo_name: &str) -> PathBuf {
binary_repo_cache_dir(package_cache_dir, repo_name).join("packages")
}
pub(super) fn join_repo_url(base: &str, rel: &str) -> Result<String> {
let base = if base.ends_with('/') {
base.to_string()
} else {
format!("{base}/")
};
let url = url::Url::parse(&base).with_context(|| format!("Invalid repo URL: {base}"))?;
Ok(url
.join(rel)
.with_context(|| format!("Invalid repo db path '{}'", rel))?
.to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum FileUrlCopyOutcome {
NotFileUrl,
Copied,
Missing,
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub(super) struct RepoDbFetchCacheKey {
pub(super) repo_name: String,
pub(super) base_url: String,
pub(super) repo_db_rel: String,
pub(super) rootfs: PathBuf,
pub(super) package_cache_dir: PathBuf,
}
pub(super) fn repo_db_fetch_cache() -> &'static Mutex<HashMap<RepoDbFetchCacheKey, PathBuf>> {
static CACHE: OnceLock<Mutex<HashMap<RepoDbFetchCacheKey, PathBuf>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(super) fn get_cached_repo_db_path(cache_key: &RepoDbFetchCacheKey) -> Option<PathBuf> {
let mut cache = repo_db_fetch_cache()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let cached = cache.get(cache_key).cloned()?;
if cached.exists() {
return Some(cached);
}
cache.remove(cache_key);
None
}
pub(super) fn remember_repo_db_path(cache_key: RepoDbFetchCacheKey, db_path: PathBuf) {
let mut cache = repo_db_fetch_cache()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
cache.insert(cache_key, db_path);
}
pub(super) fn copy_file_url_to_path(url: &str, dest: &Path) -> Result<FileUrlCopyOutcome> {
let parsed = match url::Url::parse(url) {
Ok(parsed) => parsed,
Err(_) => return Ok(FileUrlCopyOutcome::NotFileUrl),
};
if parsed.scheme() != "file" {
return Ok(FileUrlCopyOutcome::NotFileUrl);
}
let src = parsed
.to_file_path()
.map_err(|_| anyhow::anyhow!("Invalid file:// URL: {}", url))?;
if !src.exists() {
return Ok(FileUrlCopyOutcome::Missing);
}
if !src.is_file() {
anyhow::bail!("file:// URL is not a file: {}", src.display());
}
fs::copy(&src, dest)
.with_context(|| format!("Failed to copy {} to {}", src.display(), dest.display()))?;
Ok(FileUrlCopyOutcome::Copied)
}
pub(super) fn fetch_url_to_path(
client: &reqwest::blocking::Client,
url: &str,
dest: &Path,
) -> Result<bool> {
match copy_file_url_to_path(url, dest)? {
FileUrlCopyOutcome::Copied => return Ok(true),
FileUrlCopyOutcome::Missing => return Ok(false),
FileUrlCopyOutcome::NotFileUrl => {}
}
let mut resp = client
.get(url)
.send()
.with_context(|| format!("Failed to fetch {}", url))?;
if resp.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(false);
}
if !resp.status().is_success() {
anyhow::bail!("Failed to fetch {}: HTTP {}", url, resp.status());
}
let mut out =
fs::File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
std::io::copy(&mut resp, &mut out)
.with_context(|| format!("Failed to save {}", dest.display()))?;
out.flush()
.with_context(|| format!("Failed to flush {}", dest.display()))?;
Ok(true)
}
pub(super) fn extract_html_href_targets(html: &str) -> Vec<String> {
let lower = html.to_ascii_lowercase();
let lower_bytes = lower.as_bytes();
let html_bytes = html.as_bytes();
let mut out = Vec::new();
let mut i = 0usize;
while i < lower_bytes.len() {
let Some(rel) = lower[i..].find("href") else {
break;
};
let mut j = i + rel + 4;
while j < lower_bytes.len() && lower_bytes[j].is_ascii_whitespace() {
j += 1;
}
if j >= lower_bytes.len() || lower_bytes[j] != b'=' {
i = j;
continue;
}
j += 1;
while j < lower_bytes.len() && lower_bytes[j].is_ascii_whitespace() {
j += 1;
}
if j >= lower_bytes.len() {
break;
}
let (start, end) = if lower_bytes[j] == b'"' || lower_bytes[j] == b'\'' {
let quote = lower_bytes[j];
let start = j + 1;
let mut k = start;
while k < lower_bytes.len() && lower_bytes[k] != quote {
k += 1;
}
(start, k)
} else {
let start = j;
let mut k = start;
while k < lower_bytes.len()
&& !lower_bytes[k].is_ascii_whitespace()
&& lower_bytes[k] != b'>'
{
k += 1;
}
(start, k)
};
if start < end && end <= html_bytes.len() {
out.push(String::from_utf8_lossy(&html_bytes[start..end]).to_string());
}
i = end.saturating_add(1);
}
out
}
pub(super) fn default_repo_public_key_candidate_names(base_url: &str) -> Result<Vec<String>> {
let mut names = vec![
"vertex.pub".to_string(),
"depot.pub".to_string(),
"depot.minisign.pub".to_string(),
"minisign.pub".to_string(),
"repo.pub".to_string(),
];
if let Ok(parsed) = url::Url::parse(base_url)
&& let Some(last_segment) = parsed
.path_segments()
.and_then(|mut segments| segments.rfind(|s| !s.is_empty()))
{
names.push(format!("{}.pub", last_segment));
}
names.sort();
names.dedup();
Ok(names)
}
pub(super) fn probe_repo_public_key_urls(
base_url: &str,
client: &reqwest::blocking::Client,
) -> Result<Vec<(String, String)>> {
let mut out = Vec::new();
for key_name in default_repo_public_key_candidate_names(base_url)? {
let key_url = join_repo_url(base_url, &format!("keys/{}", key_name))?;
let resp = client
.get(&key_url)
.send()
.with_context(|| format!("Failed to fetch {}", key_url))?;
if resp.status().is_success() {
out.push((key_name, key_url));
}
}
out.sort();
out.dedup();
Ok(out)
}
pub(super) fn list_repo_public_key_urls(
base_url: &str,
client: &reqwest::blocking::Client,
) -> Result<Vec<(String, String)>> {
let parsed =
url::Url::parse(base_url).with_context(|| format!("Invalid repo URL: {base_url}"))?;
if parsed.scheme() == "file" {
let repo_dir = parsed
.to_file_path()
.map_err(|_| anyhow::anyhow!("Invalid file:// URL: {}", base_url))?;
let keys_dir = repo_dir.join("keys");
if !keys_dir.exists() {
return Ok(Vec::new());
}
if !keys_dir.is_dir() {
anyhow::bail!(
"Binary repo keys path is not a directory: {}",
keys_dir.display()
);
}
let mut out = Vec::new();
for entry in fs::read_dir(&keys_dir)
.with_context(|| format!("Failed to read {}", keys_dir.display()))?
{
let path = entry?.path();
if !path.is_file() {
continue;
}
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.to_ascii_lowercase().ends_with(".pub") {
continue;
}
let key_url = url::Url::from_file_path(&path)
.map_err(|_| anyhow::anyhow!("Failed to build file:// URL for {}", path.display()))?
.to_string();
out.push((name.to_string(), key_url));
}
out.sort();
out.dedup();
return Ok(out);
}
let mut out = Vec::new();
let keys_url = join_repo_url(base_url, "keys/")?;
let resp = client
.get(&keys_url)
.send()
.with_context(|| format!("Failed to fetch {}", keys_url))?;
if resp.status().is_success() {
let body = resp
.text()
.with_context(|| format!("Failed to read {}", keys_url))?;
let keys_base = url::Url::parse(&keys_url)
.with_context(|| format!("Invalid repo keys URL: {}", keys_url))?;
for href in extract_html_href_targets(&body) {
if href.is_empty() || href.starts_with('#') || href.starts_with('?') {
continue;
}
let Ok(url) = keys_base.join(&href) else {
continue;
};
let Some(name) = url.path_segments().and_then(|mut s| s.next_back()) else {
continue;
};
if name.is_empty() || !name.to_ascii_lowercase().ends_with(".pub") {
continue;
}
out.push((name.to_string(), url.to_string()));
}
}
if out.is_empty() {
out = probe_repo_public_key_urls(base_url, client)?;
}
out.sort();
out.dedup();
Ok(out)
}
pub(super) fn verify_with_any_trusted_public_key(
rootfs: &Path,
input: &Path,
sig_path: &Path,
) -> Result<PathBuf> {
let keys = crate::signing::load_trusted_public_keys(rootfs)?;
if keys.is_empty() {
anyhow::bail!("No trusted minisign public keys found in rootfs or host");
}
crate::signing::verify_zst_file_detached_with_trusted_keys(input, sig_path, &keys)
}
pub(super) fn sanitize_filename_component(input: &str) -> String {
input
.chars()
.map(|ch| match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '_' | '-' => ch,
_ => '_',
})
.collect()
}
pub(super) fn install_trusted_repo_public_key(
rootfs: &Path,
repo_name: &str,
source_key_path: &Path,
source_name: &str,
) -> Result<PathBuf> {
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs);
fs::create_dir_all(&trusted_dir)
.with_context(|| format!("Failed to create {}", trusted_dir.display()))?;
let base_name = source_name
.split('/')
.next_back()
.filter(|name| !name.is_empty())
.unwrap_or("repo.pub");
let base_name = sanitize_filename_component(base_name);
let repo_prefix = sanitize_filename_component(repo_name);
let source_bytes = fs::read(source_key_path)
.with_context(|| format!("Failed to read {}", source_key_path.display()))?;
let mut candidates = Vec::new();
candidates.push(trusted_dir.join(&base_name));
if !repo_prefix.is_empty() {
candidates.push(trusted_dir.join(format!("{}-{}", repo_prefix, base_name)));
}
for candidate in &candidates {
if candidate.exists() {
let existing = fs::read(candidate)
.with_context(|| format!("Failed to read {}", candidate.display()))?;
if existing == source_bytes {
return Ok(candidate.clone());
}
} else {
fs::write(candidate, &source_bytes)
.with_context(|| format!("Failed to write {}", candidate.display()))?;
return Ok(candidate.clone());
}
}
for idx in 1usize.. {
let candidate = trusted_dir.join(format!("{}-{}.{}", repo_prefix, base_name, idx));
if candidate.exists() {
let existing = fs::read(&candidate)
.with_context(|| format!("Failed to read {}", candidate.display()))?;
if existing == source_bytes {
return Ok(candidate);
}
continue;
}
fs::write(&candidate, &source_bytes)
.with_context(|| format!("Failed to write {}", candidate.display()))?;
return Ok(candidate);
}
unreachable!("infinite loop returns on first available candidate")
}
pub(super) fn try_trust_repo_public_key_for_repo_db(
repo_name: &str,
base_url: &str,
rootfs: &Path,
cache_dir: &Path,
client: &reqwest::blocking::Client,
repo_db_zst_path: &Path,
repo_db_sig_path: &Path,
) -> Result<Option<PathBuf>> {
let repo_keys = list_repo_public_key_urls(base_url, client)?;
if repo_keys.is_empty() {
return Ok(None);
}
let repo_keys_cache_dir = cache_dir.join("repo_keys");
fs::create_dir_all(&repo_keys_cache_dir)
.with_context(|| format!("Failed to create {}", repo_keys_cache_dir.display()))?;
for (key_name, key_url) in repo_keys {
let cache_name = sanitize_filename_component(&key_name);
let key_tmp_path = repo_keys_cache_dir.join(&cache_name);
if !fetch_url_to_path(client, &key_url, &key_tmp_path)? {
continue;
}
if crate::signing::verify_zst_file_detached_with_public_key(
repo_db_zst_path,
repo_db_sig_path,
&key_tmp_path,
)
.is_err()
{
continue;
}
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs);
let prompt = format!(
"Trust repo key '{}' from binary repo '{}' and copy it to {}?",
key_name,
repo_name,
trusted_dir.display()
);
if !crate::ui::prompt_yes_no(&prompt, true)? {
crate::log_warn!(
"Skipped trusting repo key '{}' for binary repo '{}'",
key_name,
repo_name
);
continue;
}
let installed =
install_trusted_repo_public_key(rootfs, repo_name, &key_tmp_path, &key_name)?;
return Ok(Some(installed));
}
Ok(None)
}
pub(super) fn normalize_git_mirror_url(url: &str) -> Result<String> {
let parsed = match url::Url::parse(url) {
Ok(parsed) => parsed,
Err(_) => return Ok(url.to_string()),
};
if parsed.scheme() != "file" {
return Ok(url.to_string());
}
let path = parsed
.to_file_path()
.map_err(|_| anyhow::anyhow!("Invalid file:// mirror URL: {}", url))?;
Ok(path.to_string_lossy().into_owned())
}
pub(super) fn decompress_zstd_file(src: &Path, dst: &Path) -> Result<()> {
let mut input =
fs::File::open(src).with_context(|| format!("Failed to open {}", src.display()))?;
let mut decoder = zstd::stream::read::Decoder::new(&mut input)
.with_context(|| format!("Failed to open zstd decoder for {}", src.display()))?;
let tmp = dst.with_extension("tmp");
let mut output =
fs::File::create(&tmp).with_context(|| format!("Failed to create {}", tmp.display()))?;
std::io::copy(&mut decoder, &mut output)
.with_context(|| format!("Failed to decompress {}", src.display()))?;
output
.flush()
.with_context(|| format!("Failed to flush {}", tmp.display()))?;
fs::rename(&tmp, dst)
.with_context(|| format!("Failed to move {} to {}", tmp.display(), dst.display()))?;
Ok(())
}
/// Fetch (or refresh) a binary repo `repo.db.zst` into the configured package cache.
///
/// Returns the path to the decompressed SQLite database file.
pub fn fetch_binary_repo_db(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
) -> Result<PathBuf> {
let machine_arch = std::env::consts::ARCH;
let base_url = repo.effective_url_for_arch(machine_arch).with_context(|| {
format!(
"Binary repo '{}' is not configured for machine arch '{}'",
repo_name, machine_arch
)
})?;
let repo_db_rel = repo
.effective_repo_db_for_arch(machine_arch)
.with_context(|| {
format!(
"Binary repo '{}' is not configured for machine arch '{}'",
repo_name, machine_arch
)
})?;
let cache_key = RepoDbFetchCacheKey {
repo_name: repo_name.to_string(),
base_url: base_url.to_string(),
repo_db_rel: repo_db_rel.to_string(),
rootfs: rootfs.to_path_buf(),
package_cache_dir: package_cache_dir.to_path_buf(),
};
if let Some(cached_db_path) = get_cached_repo_db_path(&cache_key) {
return Ok(cached_db_path);
}
let cache_dir = binary_repo_cache_dir(package_cache_dir, repo_name);
fs::create_dir_all(&cache_dir)
.with_context(|| format!("Failed to create {}", cache_dir.display()))?;
let repo_db_zst = cache_dir.join("repo.db.zst");
let repo_db_sig = cache_dir.join("repo.db.zst.sig");
let repo_db_sqlite = cache_dir.join("repo.db");
let tmp_zst = cache_dir.join("repo.db.zst.tmp");
let tmp_sig = cache_dir.join("repo.db.zst.sig.tmp");
let repo_db_url = join_repo_url(base_url, repo_db_rel)?;
let repo_sig_url = join_repo_url(base_url, &format!("{}.sig", repo_db_rel))?;
crate::log_info!("Fetching binary repo DB for '{}'", repo_name);
let client = reqwest::blocking::Client::builder()
.build()
.context("Failed to build HTTP client for binary repo fetch")?;
match copy_file_url_to_path(&repo_db_url, &tmp_zst)? {
FileUrlCopyOutcome::Copied => {}
FileUrlCopyOutcome::Missing => {
if repo_db_sqlite.exists() {
crate::log_warn!(
"Failed to refresh binary repo '{}' (missing local file), using cached DB: {}",
repo_name,
repo_db_url
);
remember_repo_db_path(cache_key.clone(), repo_db_sqlite.clone());
return Ok(repo_db_sqlite);
}
anyhow::bail!("Failed to fetch {}: local file not found", repo_db_url);
}
FileUrlCopyOutcome::NotFileUrl => {
let resp = client
.get(&repo_db_url)
.send()
.with_context(|| format!("Failed to fetch {}", repo_db_url))?;
if !resp.status().is_success() {
if repo_db_sqlite.exists() {
crate::log_warn!(
"Failed to refresh binary repo '{}' (HTTP {}), using cached DB",
repo_name,
resp.status()
);
remember_repo_db_path(cache_key.clone(), repo_db_sqlite.clone());
return Ok(repo_db_sqlite);
}
anyhow::bail!("Failed to fetch {}: HTTP {}", repo_db_url, resp.status());
}
let mut resp = resp;
let mut out = fs::File::create(&tmp_zst)
.with_context(|| format!("Failed to create {}", tmp_zst.display()))?;
std::io::copy(&mut resp, &mut out)
.with_context(|| format!("Failed to save {}", tmp_zst.display()))?;
out.flush()
.with_context(|| format!("Failed to flush {}", tmp_zst.display()))?;
}
}
let sig_downloaded = match copy_file_url_to_path(&repo_sig_url, &tmp_sig)? {
FileUrlCopyOutcome::Copied => true,
FileUrlCopyOutcome::Missing => {
if !repo.allow_unsigned {
anyhow::bail!(
"Failed to fetch detached signature for binary repo '{}' (local file not found): {}",
repo_name,
repo_sig_url
);
}
crate::log_warn!(
"Binary repo '{}' has no detached signature (missing local file) for {}; allow_unsigned=true so continuing",
repo_name,
repo_db_url
);
false
}
FileUrlCopyOutcome::NotFileUrl => {
let sig_resp = client
.get(&repo_sig_url)
.send()
.with_context(|| format!("Failed to fetch {}", repo_sig_url))?;
if sig_resp.status().is_success() {
let mut sig_resp = sig_resp;
let mut sig_out = fs::File::create(&tmp_sig)
.with_context(|| format!("Failed to create {}", tmp_sig.display()))?;
std::io::copy(&mut sig_resp, &mut sig_out)
.with_context(|| format!("Failed to save {}", tmp_sig.display()))?;
sig_out
.flush()
.with_context(|| format!("Failed to flush {}", tmp_sig.display()))?;
true
} else {
if !repo.allow_unsigned {
anyhow::bail!(
"Failed to fetch detached signature for binary repo '{}' (HTTP {}): {}",
repo_name,
sig_resp.status(),
repo_sig_url
);
}
crate::log_warn!(
"Binary repo '{}' has no detached signature (HTTP {}) for {}; allow_unsigned=true so continuing",
repo_name,
sig_resp.status(),
repo_db_url
);
false
}
}
};
if sig_downloaded {
let mut trusted_keys = crate::signing::list_trusted_public_keys(rootfs)?;
if trusted_keys.is_empty() {
if try_trust_repo_public_key_for_repo_db(
repo_name, base_url, rootfs, &cache_dir, &client, &tmp_zst, &tmp_sig,
)?
.is_some()
{
crate::log_info!("Trusted repo key for '{}' installed", repo_name);
} else if !repo.allow_unsigned {
anyhow::bail!(
"No trusted minisign public key found for binary repo '{}' and no trusted key was accepted from {}/keys/",
repo_name,
base_url.trim_end_matches('/')
);
} else {
crate::log_warn!(
"No trusted minisign public key found; skipping verification for binary repo '{}' because allow_unsigned=true",
repo_name
);
}
trusted_keys = crate::signing::list_trusted_public_keys(rootfs)?;
}
if trusted_keys.is_empty() {
// No key was trusted/installed, and allow_unsigned=true already handled above.
} else {
if let Err(initial_err) = verify_with_any_trusted_public_key(rootfs, &tmp_zst, &tmp_sig)
{
if try_trust_repo_public_key_for_repo_db(
repo_name, base_url, rootfs, &cache_dir, &client, &tmp_zst, &tmp_sig,
)?
.is_some()
{
crate::log_info!("Trusted repo key for '{}' installed", repo_name);
verify_with_any_trusted_public_key(rootfs, &tmp_zst, &tmp_sig).with_context(
|| {
format!(
"Failed to verify detached signature for binary repo '{}'",
repo_name
)
},
)?;
} else {
return Err(initial_err).with_context(|| {
format!(
"Failed to verify detached signature for binary repo '{}'",
repo_name
)
});
}
}
crate::log_info!(
"Verified detached signature for binary repo '{}'",
repo_name
);
}
}
fs::rename(&tmp_zst, &repo_db_zst).with_context(|| {
format!(
"Failed to move {} to {}",
tmp_zst.display(),
repo_db_zst.display()
)
})?;
if sig_downloaded {
fs::rename(&tmp_sig, &repo_db_sig).with_context(|| {
format!(
"Failed to move {} to {}",
tmp_sig.display(),
repo_db_sig.display()
)
})?;
} else if repo_db_sig.exists() {
let _ = fs::remove_file(&repo_db_sig);
}
decompress_zstd_file(&repo_db_zst, &repo_db_sqlite)?;
remember_repo_db_path(cache_key, repo_db_sqlite.clone());
Ok(repo_db_sqlite)
}
+511
View File
@@ -0,0 +1,511 @@
use super::*;
impl RepoManager {
pub fn new(repo_dir: PathBuf) -> Self {
Self { repo_dir }
}
/// Create a compressed SQLite repository database from a directory of packages
pub fn create_repo_db(&self) -> Result<PathBuf> {
let db_path = self.repo_dir.join("repo.db");
let compressed_db_path = self.repo_dir.join("repo.db.zst");
// Remove existing DB if it exists
if db_path.exists() {
fs::remove_file(&db_path)?;
}
let mut conn = Connection::open(&db_path)
.with_context(|| format!("Failed to create repo database at {}", db_path.display()))?;
self.configure_repo_build_pragmas(&mut conn)?;
self.init_repo_schema(&mut conn)?;
let package_paths = self.collect_repo_package_paths()?;
let indexed_packages = self.collect_indexed_packages_parallel(&package_paths)?;
conn.execute_batch("BEGIN IMMEDIATE TRANSACTION;")
.context("Failed to begin repo DB write transaction")?;
let insert_result: Result<()> = (|| {
for indexed in indexed_packages {
self.insert_indexed_package(&mut conn, indexed)?;
}
Ok(())
})();
match insert_result {
Ok(()) => {
conn.execute_batch("COMMIT;")
.context("Failed to commit repo DB write transaction")?;
}
Err(err) => {
let _ = conn.execute_batch("ROLLBACK;");
return Err(err);
}
}
self.create_repo_indexes(&mut conn)?;
conn.close().map_err(|(_, e)| e)?;
// Compress the database
self.compress_db(&db_path, &compressed_db_path)?;
// Remove the uncompressed DB
fs::remove_file(&db_path)?;
Ok(compressed_db_path)
}
pub(super) fn configure_repo_build_pragmas(&self, conn: &mut Connection) -> Result<()> {
// Speed-focused settings are scoped to this temporary repo DB build process.
conn.execute_batch(
"PRAGMA synchronous = OFF;
PRAGMA journal_mode = MEMORY;
PRAGMA temp_store = MEMORY;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA cache_size = -200000;",
)
.context("Failed to apply SQLite build PRAGMAs for repo DB creation")?;
Ok(())
}
pub(super) fn collect_repo_package_paths(&self) -> Result<Vec<PathBuf>> {
let mut package_paths = Vec::new();
for entry in fs::read_dir(&self.repo_dir)
.with_context(|| format!("Failed to read {}", self.repo_dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_file() && path.to_string_lossy().ends_with(".depot.pkg.tar.zst") {
package_paths.push(path);
}
}
package_paths.sort();
Ok(package_paths)
}
pub(super) fn collect_indexed_packages_parallel(
&self,
package_paths: &[PathBuf],
) -> Result<Vec<IndexedPackage>> {
if package_paths.is_empty() {
return Ok(Vec::new());
}
let worker_count = num_cpus().min(package_paths.len());
crate::log_info!(
"Using {} thread(s) to index {} package(s)...",
worker_count,
package_paths.len()
);
let next_index = AtomicUsize::new(0);
let mut indexed = std::thread::scope(|scope| -> Result<Vec<(usize, IndexedPackage)>> {
let (tx, rx) = mpsc::channel::<(usize, Result<IndexedPackage>)>();
for _ in 0..worker_count {
let tx = tx.clone();
let next_index = &next_index;
scope.spawn(move || {
loop {
let idx = next_index.fetch_add(1, Ordering::Relaxed);
if idx >= package_paths.len() {
break;
}
let result = self.read_indexed_package(&package_paths[idx]);
if tx.send((idx, result)).is_err() {
break;
}
}
});
}
drop(tx);
let mut indexed = Vec::with_capacity(package_paths.len());
for _ in 0..package_paths.len() {
let (idx, result) = rx
.recv()
.context("Failed to receive package indexing result from worker")?;
indexed.push((idx, result?));
}
Ok(indexed)
})?;
indexed.sort_by_key(|(idx, _)| *idx);
Ok(indexed.into_iter().map(|(_, pkg)| pkg).collect())
}
pub(super) fn init_repo_schema(&self, conn: &mut Connection) -> Result<()> {
conn.execute_batch(
"CREATE TABLE packages (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
real_name TEXT,
version TEXT NOT NULL,
revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER,
description TEXT,
homepage TEXT,
license TEXT,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
sha512 TEXT NOT NULL
);
CREATE TABLE provides (
package_id INTEGER,
name TEXT NOT NULL,
FOREIGN KEY(package_id) REFERENCES packages(id)
);
CREATE TABLE conflicts (
package_id INTEGER,
name TEXT NOT NULL,
FOREIGN KEY(package_id) REFERENCES packages(id)
);
CREATE TABLE replaces (
package_id INTEGER,
name TEXT NOT NULL,
FOREIGN KEY(package_id) REFERENCES packages(id)
);
CREATE TABLE dependencies (
package_id INTEGER,
kind TEXT NOT NULL,
name TEXT NOT NULL,
FOREIGN KEY(package_id) REFERENCES packages(id)
);
CREATE TABLE groups (
package_id INTEGER,
name TEXT NOT NULL,
FOREIGN KEY(package_id) REFERENCES packages(id)
);
CREATE TABLE files (
package_id INTEGER,
path TEXT NOT NULL,
FOREIGN KEY(package_id) REFERENCES packages(id)
);",
)
.context("Failed to initialize repo schema")?;
Ok(())
}
pub(super) fn create_repo_indexes(&self, conn: &mut Connection) -> Result<()> {
conn.execute_batch(
"CREATE INDEX idx_packages_name ON packages(name);
CREATE INDEX idx_provides_name ON provides(name);
CREATE INDEX idx_conflicts_name ON conflicts(name);
CREATE INDEX idx_replaces_name ON replaces(name);
CREATE INDEX idx_dependencies_name ON dependencies(name);
CREATE INDEX idx_dependencies_kind ON dependencies(kind);
CREATE INDEX idx_groups_name ON groups(name);
CREATE INDEX idx_repo_files_path ON files(path);",
)
.context("Failed to create repo DB indexes")?;
Ok(())
}
pub(super) fn read_indexed_package(&self, pkg_path: &Path) -> Result<IndexedPackage> {
crate::log_info!("Indexing package {}...", pkg_path.display());
let filename = pkg_path
.file_name()
.and_then(|name| name.to_str())
.with_context(|| format!("Invalid package filename: {}", pkg_path.display()))?
.to_string();
let file = fs::File::open(pkg_path)?;
let size = file.metadata()?.len();
let mut hashing_reader = HashingReader::new(file);
let mut name = String::new();
let mut real_name = None;
let mut version = String::new();
let mut revision = 1;
let mut abi_breaking = false;
let mut built_against = Vec::new();
let mut completed_at = path_modified_unix_timestamp(pkg_path)?;
let mut description = None;
let mut homepage = None;
let mut license = None;
let mut provides = Vec::new();
let mut conflicts = Vec::new();
let mut replaces = Vec::new();
let mut runtime_dependencies = Vec::new();
let mut optional_dependencies = Vec::new();
let mut groups = Vec::new();
let mut archive_files = Vec::new();
{
let zstd_decoder = zstd::stream::read::Decoder::new(&mut hashing_reader)?;
let mut archive = tar::Archive::new(zstd_decoder);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
let path_str = path.to_string_lossy().to_string();
if path_str == ".metadata.toml" {
let mut content = String::new();
use std::io::Read;
entry.read_to_string(&mut content)?;
let metadata: toml::Value = toml::from_str(&content).with_context(|| {
format!("Failed to parse .metadata.toml in {}", pkg_path.display())
})?;
name = metadata
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
real_name = metadata
.get("real_name")
.and_then(|v| v.as_str())
.map(String::from);
version = metadata
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
revision = metadata
.get("revision")
.and_then(|v| v.as_integer())
.unwrap_or(1) as u32;
abi_breaking = metadata
.get("abi_breaking")
.and_then(|v| v.as_bool())
.unwrap_or(false);
built_against = parse_string_array_metadata(&metadata, "built_against");
completed_at =
metadata_time::parse_completed_at_value(&metadata).or(completed_at);
description = metadata
.get("description")
.and_then(|v| v.as_str())
.map(String::from);
homepage = metadata
.get("homepage")
.and_then(|v| v.as_str())
.map(String::from);
license = parse_license_text(&metadata);
if let Some(provides_arr) = metadata.get("provides").and_then(|v| v.as_array())
{
provides = provides_arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(conflicts_arr) =
metadata.get("conflicts").and_then(|v| v.as_array())
{
conflicts = conflicts_arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(replaces_arr) = metadata.get("replaces").and_then(|v| v.as_array())
{
replaces = replaces_arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(runtime_arr) = metadata
.get("dependencies")
.and_then(|v| v.get("runtime"))
.and_then(|v| v.as_array())
{
runtime_dependencies = runtime_arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(optional_arr) = metadata
.get("dependencies")
.and_then(|v| v.get("optional"))
.and_then(|v| v.as_array())
{
optional_dependencies = optional_arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(groups_arr) = metadata
.get("dependencies")
.and_then(|v| v.get("groups"))
.and_then(|v| v.as_array())
{
groups = groups_arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
continue;
}
let entry_type = entry.header().entry_type();
if entry_type.is_file() || entry_type.is_symlink() || entry_type.is_hard_link() {
let normalized = path_str.trim_start_matches("./").to_string();
if normalized == ".metadata.toml" {
continue;
}
archive_files.push(normalized);
}
}
}
let (sha256, sha512) = hashing_reader.finalize_hex();
if name.is_empty() {
// Fallback for packages WITHOUT metadata (e.g. legacy or during transition)
let name_parts: Vec<&str> = filename.split('-').collect();
if name_parts.len() < 4 {
anyhow::bail!(
"Invalid package filename and no .metadata.toml: {}",
filename
);
}
name = name_parts[0].to_string();
version = name_parts[1].to_string();
revision = name_parts[2].parse().unwrap_or(1);
}
Ok(IndexedPackage {
name,
real_name,
version,
revision,
abi_breaking,
built_against,
completed_at,
description,
homepage,
license,
filename,
size,
sha256,
sha512,
provides,
conflicts,
replaces,
runtime_dependencies,
optional_dependencies,
groups,
archive_files,
})
}
pub(super) fn insert_indexed_package(
&self,
conn: &mut Connection,
indexed: IndexedPackage,
) -> Result<()> {
let IndexedPackage {
name,
real_name,
version,
revision,
abi_breaking,
built_against,
completed_at,
description,
homepage,
license,
filename,
size,
sha256,
sha512,
provides,
conflicts,
replaces,
runtime_dependencies,
optional_dependencies,
groups,
archive_files,
} = indexed;
// Insert into database
conn.execute(
"INSERT INTO packages (name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
params![
name,
real_name,
version,
revision as i64,
abi_breaking,
format_built_against(&built_against),
completed_at,
description,
homepage,
license,
filename,
size as i64,
sha256,
sha512
],
)?;
let package_id = conn.last_insert_rowid();
// Insert into provides
for provide in provides {
conn.execute(
"INSERT INTO provides (package_id, name) VALUES (?1, ?2)",
params![package_id, provide],
)?;
}
for conflict in conflicts {
conn.execute(
"INSERT INTO conflicts (package_id, name) VALUES (?1, ?2)",
params![package_id, conflict],
)?;
}
for replacement in replaces {
conn.execute(
"INSERT INTO replaces (package_id, name) VALUES (?1, ?2)",
params![package_id, replacement],
)?;
}
for dep in runtime_dependencies {
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (?1, 'runtime', ?2)",
params![package_id, dep],
)?;
}
for dep in optional_dependencies {
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (?1, 'optional', ?2)",
params![package_id, dep],
)?;
}
for group in groups {
conn.execute(
"INSERT INTO groups (package_id, name) VALUES (?1, ?2)",
params![package_id, group],
)?;
}
for file_path in archive_files {
conn.execute(
"INSERT INTO files (package_id, path) VALUES (?1, ?2)",
params![package_id, file_path],
)?;
}
Ok(())
}
pub(super) fn compress_db(&self, source: &Path, dest: &Path) -> Result<()> {
let mut input = fs::File::open(source)?;
let output = fs::File::create(dest)?;
let mut encoder = Encoder::new(output, 19)?; // High compression for repo DB
encoder.multithread(num_cpus() as u32)?;
std::io::copy(&mut input, &mut encoder)?;
encoder.finish()?;
Ok(())
}
}
+149
View File
@@ -0,0 +1,149 @@
use super::*;
/// Synchronize git mirrors into /usr/src/depot/<reponame>
pub fn sync_mirrors(
repo_dir: &std::path::Path,
mirrors: &std::collections::HashMap<String, String>,
) -> Result<()> {
use git2::{FetchOptions, Repository, ResetType, build::RepoBuilder};
use std::os::unix::fs::PermissionsExt;
let base = repo_dir.to_path_buf();
if !base.exists() {
std::fs::create_dir_all(&base)?;
}
for (name, url) in mirrors {
let target = base.join(name);
let git_url = normalize_git_mirror_url(url)?;
if !target.exists() {
crate::log_info!("Cloning mirror '{}' -> {}", name, target.display());
let mut fo = FetchOptions::new();
fo.remote_callbacks(crate::source::authenticated_remote_callbacks(
None, &git_url,
));
let mut builder = RepoBuilder::new();
builder.fetch_options(fo);
builder
.clone(&git_url, &target)
.with_context(|| format!("Failed to clone {}", url))?;
} else {
crate::log_info!("Updating mirror '{}' in {}", name, target.display());
// Open repository and fetch updates
let repo = Repository::open(&target)
.with_context(|| format!("Failed to open repository at {}", target.display()))?;
let mut fo = FetchOptions::new();
fo.remote_callbacks(crate::source::authenticated_remote_callbacks(
None, &git_url,
));
// Fetch from origin
let mut remote = repo
.find_remote("origin")
.or_else(|_| repo.remote_anonymous(&git_url))?;
remote
.fetch(&["refs/heads/*:refs/remotes/origin/*"], Some(&mut fo), None)
.with_context(|| format!("Failed to fetch updates for {}", url))?;
// Try to fast-forward HEAD to origin/HEAD by resetting to FETCH_HEAD if present
if let Ok(fetch_head) = repo.find_reference("FETCH_HEAD")
&& let Some(oid) = fetch_head.target()
{
let obj = repo.find_object(oid, None)?;
repo.reset(&obj, ResetType::Hard, None)?;
}
}
// Make the tree readable and writable by everyone
for entry in walkdir::WalkDir::new(&target) {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o777))?;
} else if path.is_file() {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666))?;
}
}
}
Ok(())
}
/// Show status for each mirror repository: path, exists, branch/HEAD, latest commit, dirty
pub fn mirrors_status(
repo_dir: &std::path::Path,
mirrors: &std::collections::HashMap<String, String>,
) -> Result<()> {
use git2::Repository;
let base = repo_dir.to_path_buf();
if !base.exists() {
crate::log_info!("Repo base directory does not exist: {}", base.display());
return Ok(());
}
for name in mirrors.keys() {
let target = base.join(name);
crate::log_info!("--- {} ---", name);
if !target.exists() {
crate::log_info!("Not cloned: {}", target.display());
continue;
}
match Repository::open(&target) {
Ok(repo) => {
// Branch / HEAD
let head = repo.head().ok();
let branch = head
.as_ref()
.and_then(|h| h.shorthand().ok().map(|s| s.to_string()))
.unwrap_or_else(|| "(no branch)".to_string());
// Latest commit OID
let oid = repo.refname_to_id("HEAD").ok();
let short = oid
.map(|o| format!("{}", o))
.unwrap_or_else(|| "(unknown)".to_string());
// Commit time (seconds since epoch) if available
let mut commit_time = String::new();
if let Some(oid) = oid
&& let Ok(commit) = repo.find_commit(oid)
{
let t = commit.time().seconds();
commit_time = format!("{}", t);
}
// Dirty status
let statuses = match repo.statuses(None) {
Ok(s) => s,
Err(_) => {
crate::log_warn!("Failed to read status for {}", target.display());
continue;
}
};
let dirty = statuses.iter().any(|s| {
s.status().intersects(
git2::Status::WT_MODIFIED | git2::Status::WT_NEW | git2::Status::WT_DELETED,
)
});
crate::log_info!("Path: {}", target.display());
crate::log_info!("Branch/HEAD: {}", branch);
crate::log_info!("HEAD OID: {}", short);
if !commit_time.is_empty() {
crate::log_info!("Latest commit time (epoch): {}", commit_time);
}
crate::log_info!("Dirty: {}", if dirty { "yes" } else { "no" });
}
Err(e) => {
crate::log_info!("Failed to open repo at {}: {}", target.display(), e);
}
}
}
Ok(())
}
+806
View File
@@ -0,0 +1,806 @@
use super::*;
/// Search a cached binary repository SQLite DB by package name or provided feature.
pub fn search_cached_binary_repo_db(
repo_name: &str,
db_path: &Path,
query: &str,
) -> Result<Vec<BinaryRepoSearchHit>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let like = format!("%{}%", query.to_ascii_lowercase());
let mut stmt = conn.prepare(
"SELECT
p.name,
p.version,
p.revision,
p.description,
p.filename,
p.size,
GROUP_CONCAT(DISTINCT pr_all.name)
FROM packages p
LEFT JOIN provides pr_all ON pr_all.package_id = p.id
WHERE lower(p.name) LIKE ?1
OR EXISTS (
SELECT 1 FROM provides pr
WHERE pr.package_id = p.id
AND lower(pr.name) LIKE ?1
)
GROUP BY p.id
ORDER BY
CASE
WHEN lower(p.name) = lower(?2) THEN 0
WHEN lower(p.name) LIKE lower(?3) THEN 1
ELSE 2
END,
p.name ASC",
)?;
let starts = format!("{}%", query.to_ascii_lowercase());
let rows = stmt.query_map(params![like, query, starts], |row| {
let provides_csv: Option<String> = row.get(6)?;
Ok(BinaryRepoSearchHit {
repo_name: repo_name.to_string(),
name: row.get(0)?,
version: row.get(1)?,
revision: row.get::<_, i64>(2)? as u32,
description: row.get(3)?,
filename: row.get(4)?,
size: row.get::<_, i64>(5)? as u64,
provides: provides_csv
.map(|s| {
s.split(',')
.filter(|v| !v.is_empty())
.map(|v| v.to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default(),
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
/// Fetch and search a binary repo by name or provide.
pub fn search_binary_repo(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
query: &str,
) -> Result<Vec<BinaryRepoSearchHit>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
search_cached_binary_repo_db(repo_name, &db_path, query)
}
/// Search a cached binary repo DB by file path substring.
pub fn search_cached_binary_repo_files(
repo_name: &str,
db_path: &Path,
query: &str,
) -> Result<Vec<BinaryRepoFileSearchHit>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let has_files_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='files'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_files_table {
return Ok(Vec::new());
}
let like = format!("%{}%", query.to_ascii_lowercase());
let mut stmt = conn.prepare(
"SELECT p.name, p.version, p.revision, f.path, p.size
FROM files f
JOIN packages p ON p.id = f.package_id
WHERE lower(f.path) LIKE ?1
ORDER BY p.name ASC, f.path ASC",
)?;
let rows = stmt.query_map(params![like], |row| {
Ok(BinaryRepoFileSearchHit {
repo_name: repo_name.to_string(),
package_name: row.get(0)?,
version: row.get(1)?,
revision: row.get::<_, i64>(2)? as u32,
path: row.get(3)?,
size: row.get::<_, i64>(4)? as u64,
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
/// Fetch and search a binary repo by file path substring.
pub fn search_binary_repo_files(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
query: &str,
) -> Result<Vec<BinaryRepoFileSearchHit>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
search_cached_binary_repo_files(repo_name, &db_path, query)
}
/// Find the package(s) that own a file path in a cached binary repo DB.
pub fn cached_binary_repo_owns_path(
repo_name: &str,
db_path: &Path,
path: &str,
) -> Result<Vec<BinaryRepoFileSearchHit>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let has_files_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='files'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_files_table {
return Ok(Vec::new());
}
let normalized = path.trim_start_matches('/').trim_start_matches("./");
let mut stmt = conn.prepare(
"SELECT p.name, p.version, p.revision, f.path, p.size
FROM files f
JOIN packages p ON p.id = f.package_id
WHERE f.path = ?1
ORDER BY p.name ASC",
)?;
let rows = stmt.query_map(params![normalized], |row| {
Ok(BinaryRepoFileSearchHit {
repo_name: repo_name.to_string(),
package_name: row.get(0)?,
version: row.get(1)?,
revision: row.get::<_, i64>(2)? as u32,
path: row.get(3)?,
size: row.get::<_, i64>(4)? as u64,
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn repo_owns_query_candidates(rootfs: &Path, path: &str) -> Vec<String> {
let normalized = path.trim_start_matches('/').trim_start_matches("./");
let mut candidates = BTreeSet::new();
if !normalized.is_empty() {
candidates.insert(normalized.to_string());
}
let query_path = Path::new(path);
let fs_path = if query_path.is_absolute() {
rootfs.join(query_path.strip_prefix("/").unwrap_or(query_path))
} else {
rootfs.join(query_path)
};
if let Ok(resolved) = fs::canonicalize(&fs_path)
&& let Some(rel) = resolved_repo_owns_path(rootfs, &resolved)
&& !rel.is_empty()
{
candidates.insert(rel);
}
candidates.into_iter().collect()
}
pub(super) fn resolved_repo_owns_path(rootfs: &Path, resolved: &Path) -> Option<String> {
if rootfs == Path::new("/") {
return Some(
resolved
.to_string_lossy()
.trim_start_matches('/')
.to_string(),
);
}
resolved
.strip_prefix(rootfs)
.ok()
.map(|rel| rel.to_string_lossy().trim_start_matches('/').to_string())
}
/// Fetch repo metadata and resolve file ownership in a binary repo.
pub fn binary_repo_owns_path(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
path: &str,
) -> Result<Vec<BinaryRepoFileSearchHit>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
let mut hits = Vec::new();
let mut seen = BTreeSet::new();
for candidate in repo_owns_query_candidates(rootfs, path) {
for hit in cached_binary_repo_owns_path(repo_name, &db_path, &candidate)? {
let key = format!(
"{}\u{0}{}\u{0}{}\u{0}{}\u{0}{}",
hit.repo_name, hit.package_name, hit.version, hit.revision, hit.path
);
if seen.insert(key) {
hits.push(hit);
}
}
}
Ok(hits)
}
pub(super) fn query_package_provides(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
let mut stmt = conn.prepare("SELECT name FROM provides WHERE package_id = ?1 ORDER BY name")?;
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn query_package_conflicts(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
let has_conflicts_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='conflicts'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_conflicts_table {
return Ok(Vec::new());
}
let mut stmt =
conn.prepare("SELECT name FROM conflicts WHERE package_id = ?1 ORDER BY name")?;
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn query_package_replaces(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
let has_replaces_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='replaces'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_replaces_table {
return Ok(Vec::new());
}
let mut stmt = conn.prepare("SELECT name FROM replaces WHERE package_id = ?1 ORDER BY name")?;
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn query_package_runtime_deps(
conn: &Connection,
package_id: i64,
) -> Result<Vec<String>> {
let has_dependencies_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='dependencies'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_dependencies_table {
return Ok(Vec::new());
}
let mut stmt = conn.prepare(
"SELECT name FROM dependencies WHERE package_id = ?1 AND kind = 'runtime' ORDER BY name",
)?;
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn query_package_built_against(
conn: &Connection,
package_id: i64,
) -> Result<Vec<String>> {
if !repo_packages_have_built_against(conn)? {
return Ok(Vec::new());
}
let raw: String = conn.query_row(
"SELECT built_against FROM packages WHERE id = ?1",
params![package_id],
|row| row.get(0),
)?;
Ok(parse_built_against(&raw))
}
pub(super) fn query_package_optional_deps(
conn: &Connection,
package_id: i64,
) -> Result<Vec<String>> {
let has_dependencies_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='dependencies'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_dependencies_table {
return Ok(Vec::new());
}
let mut stmt = conn.prepare(
"SELECT name FROM dependencies WHERE package_id = ?1 AND kind = 'optional' ORDER BY name",
)?;
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn query_package_groups(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
let has_groups_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='groups'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_groups_table {
return Ok(Vec::new());
}
let mut stmt = conn.prepare("SELECT name FROM groups WHERE package_id = ?1 ORDER BY name")?;
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub(super) fn find_cached_binary_repo_packages(
repo_name: &str,
db_path: &Path,
query: &str,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
"p.completed_at"
} else {
"NULL"
};
let real_name_expr = if repo_packages_have_real_name(&conn)? {
"p.real_name"
} else {
"NULL"
};
let abi_breaking_expr = if repo_packages_have_abi_breaking(&conn)? {
"p.abi_breaking"
} else {
"0"
};
let built_against_expr = if repo_packages_have_built_against(&conn)? {
"p.built_against"
} else {
"''"
};
let sql = format!(
"SELECT
p.id,
p.name,
{real_name_expr},
p.version,
p.revision,
{abi_breaking_expr},
{built_against_expr},
{completed_at_expr},
p.filename,
p.size,
p.sha512,
p.description,
p.homepage,
p.license
FROM packages p
WHERE lower(p.name) = lower(?1)
OR lower({real_name_expr}) = lower(?1)
OR EXISTS (
SELECT 1 FROM replaces rp
WHERE rp.package_id = p.id
AND lower(rp.name) = lower(?1)
)
OR EXISTS (
SELECT 1 FROM provides pr
WHERE pr.package_id = p.id
AND lower(pr.name) = lower(?1)
)
ORDER BY
CASE
WHEN EXISTS (
SELECT 1 FROM replaces rp
WHERE rp.package_id = p.id
AND lower(rp.name) = lower(?1)
) THEN 0
WHEN lower(p.name) = lower(?1) THEN 1
WHEN lower({real_name_expr}) = lower(?1) THEN 2
ELSE 2
END,
p.name ASC"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params![query], |row| {
let package_id = row.get::<_, i64>(0)?;
Ok((
package_id,
BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: row.get(1)?,
real_name: row.get(2)?,
version: row.get(3)?,
revision: row.get::<_, i64>(4)? as u32,
abi_breaking: row.get(5)?,
built_against: parse_built_against(&row.get::<_, String>(6)?),
completed_at: row.get(7)?,
filename: row.get(8)?,
size: row.get::<_, i64>(9)? as u64,
sha512: row.get(10)?,
description: row.get(11)?,
homepage: row.get(12)?,
license: row.get(13)?,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
},
))
})?;
let mut out = Vec::new();
for row in rows {
let (package_id, mut rec) = row?;
rec.provides = query_package_provides(&conn, package_id)?;
rec.conflicts = query_package_conflicts(&conn, package_id)?;
rec.replaces = query_package_replaces(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.built_against = query_package_built_against(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
rec.groups = query_package_groups(&conn, package_id)?;
out.push(rec);
}
Ok(out)
}
pub(super) fn find_cached_binary_repo_packages_by_group(
repo_name: &str,
db_path: &Path,
group: &str,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let has_groups_table: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='groups'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap_or(false);
if !has_groups_table {
return Ok(Vec::new());
}
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
"p.completed_at"
} else {
"NULL"
};
let real_name_expr = if repo_packages_have_real_name(&conn)? {
"p.real_name"
} else {
"NULL"
};
let abi_breaking_expr = if repo_packages_have_abi_breaking(&conn)? {
"p.abi_breaking"
} else {
"0"
};
let built_against_expr = if repo_packages_have_built_against(&conn)? {
"p.built_against"
} else {
"''"
};
let sql = format!(
"SELECT
p.id,
p.name,
{real_name_expr},
p.version,
p.revision,
{abi_breaking_expr},
{built_against_expr},
{completed_at_expr},
p.filename,
p.size,
p.sha512,
p.description,
p.homepage,
p.license
FROM packages p
WHERE EXISTS (
SELECT 1 FROM groups g
WHERE g.package_id = p.id
AND lower(g.name) = lower(?1)
)
ORDER BY p.name ASC"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params![group], |row| {
let package_id = row.get::<_, i64>(0)?;
Ok((
package_id,
BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: row.get(1)?,
real_name: row.get(2)?,
version: row.get(3)?,
revision: row.get::<_, i64>(4)? as u32,
abi_breaking: row.get(5)?,
built_against: parse_built_against(&row.get::<_, String>(6)?),
completed_at: row.get(7)?,
filename: row.get(8)?,
size: row.get::<_, i64>(9)? as u64,
sha512: row.get(10)?,
description: row.get(11)?,
homepage: row.get(12)?,
license: row.get(13)?,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
},
))
})?;
let mut out = Vec::new();
for row in rows {
let (package_id, mut rec) = row?;
rec.provides = query_package_provides(&conn, package_id)?;
rec.conflicts = query_package_conflicts(&conn, package_id)?;
rec.replaces = query_package_replaces(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.built_against = query_package_built_against(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
rec.groups = query_package_groups(&conn, package_id)?;
out.push(rec);
}
Ok(out)
}
pub(super) fn list_cached_binary_repo_packages(
repo_name: &str,
db_path: &Path,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
"p.completed_at"
} else {
"NULL"
};
let real_name_expr = if repo_packages_have_real_name(&conn)? {
"p.real_name"
} else {
"NULL"
};
let abi_breaking_expr = if repo_packages_have_abi_breaking(&conn)? {
"p.abi_breaking"
} else {
"0"
};
let built_against_expr = if repo_packages_have_built_against(&conn)? {
"p.built_against"
} else {
"''"
};
let sql = format!(
"SELECT
p.id,
p.name,
{real_name_expr},
p.version,
p.revision,
{abi_breaking_expr},
{built_against_expr},
{completed_at_expr},
p.filename,
p.size,
p.sha512,
p.description,
p.homepage,
p.license
FROM packages p
ORDER BY p.name ASC, p.version ASC, p.revision ASC"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([], |row| {
let package_id = row.get::<_, i64>(0)?;
Ok((
package_id,
BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: row.get(1)?,
real_name: row.get(2)?,
version: row.get(3)?,
revision: row.get::<_, i64>(4)? as u32,
abi_breaking: row.get(5)?,
built_against: parse_built_against(&row.get::<_, String>(6)?),
completed_at: row.get(7)?,
filename: row.get(8)?,
size: row.get::<_, i64>(9)? as u64,
sha512: row.get(10)?,
description: row.get(11)?,
homepage: row.get(12)?,
license: row.get(13)?,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
},
))
})?;
let mut out = Vec::new();
for row in rows {
let (package_id, mut rec) = row?;
rec.provides = query_package_provides(&conn, package_id)?;
rec.conflicts = query_package_conflicts(&conn, package_id)?;
rec.replaces = query_package_replaces(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.built_against = query_package_built_against(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
rec.groups = query_package_groups(&conn, package_id)?;
out.push(rec);
}
Ok(out)
}
pub(super) fn repo_packages_have_completed_at(conn: &Connection) -> Result<bool> {
conn.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
[],
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)
.context("Failed to inspect binary repo DB schema")
}
pub(super) fn repo_packages_have_real_name(conn: &Connection) -> Result<bool> {
conn.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'real_name'",
[],
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)
.context("Failed to inspect binary repo DB schema")
}
pub(super) fn repo_packages_have_abi_breaking(conn: &Connection) -> Result<bool> {
conn.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'abi_breaking'",
[],
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)
.context("Failed to inspect binary repo DB schema")
}
pub(super) fn repo_packages_have_built_against(conn: &Connection) -> Result<bool> {
conn.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'built_against'",
[],
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)
.context("Failed to inspect binary repo DB schema")
}
pub(super) fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to read metadata for {}", path.display()))?;
let modified = metadata
.modified()
.with_context(|| format!("Failed to read modification time for {}", path.display()))?;
Ok(Some(metadata_time::system_time_to_unix(modified)?))
}
/// Resolve an exact package name/provide match from a binary repo after verifying
/// and caching its signed `repo.db.zst`.
pub fn find_binary_repo_package(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
query: &str,
) -> Result<Option<BinaryRepoPackageRecord>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
let mut matches = find_cached_binary_repo_packages(repo_name, &db_path, query)?;
if matches.len() > 1 {
crate::log_warn!(
"Multiple binary packages matched '{}' in repo '{}'; using the first match",
query,
repo_name
);
}
Ok(matches.drain(..).next())
}
/// Resolve exact package name/provide matches from a binary repo.
pub fn find_binary_repo_packages(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
query: &str,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
find_cached_binary_repo_packages(repo_name, &db_path, query)
}
/// Resolve package records that belong to the named group from a binary repo.
pub fn find_binary_repo_packages_by_group(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
group: &str,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
find_cached_binary_repo_packages_by_group(repo_name, &db_path, group)
}
/// List all binary packages from a cached, verified repository database.
pub fn list_binary_repo_packages(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
list_cached_binary_repo_packages(repo_name, &db_path)
}
+911
View File
@@ -0,0 +1,911 @@
use super::*;
#[test]
fn test_init_repo_schema() {
let mut conn = Connection::open_in_memory().unwrap();
let manager = RepoManager::new(PathBuf::from("."));
manager.init_repo_schema(&mut conn).unwrap();
// Check if table exists
let exists: bool = conn
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='packages'",
[],
|r| r.get(0),
)
.unwrap();
assert!(exists);
let has_sha512: bool = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'sha512'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap();
assert!(has_sha512);
let has_completed_at: bool = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap();
assert!(has_completed_at);
}
#[test]
fn test_index_package() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path();
let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst");
// Create a valid .tar.zst with .metadata.toml
let file = fs::File::create(&pkg_path).unwrap();
let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap();
let mut tar = tar::Builder::new(encoder);
let metadata = r#"
name = "test"
real_name = "icu"
version = "1.0"
revision = 1
abi_breaking = true
built_against = ["icu78"]
description = "test description"
homepage = "https://example.com"
license = "MIT"
completed_at = "2026-03-10T12:34:56Z"
provides = ["test-feature"]
[dependencies]
runtime = []
optional = []
"#;
let mut header = tar::Header::new_gnu();
header.set_path(".metadata.toml").unwrap();
header.set_size(metadata.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append(&header, metadata.as_bytes()).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
filetime::set_file_mtime(
&pkg_path,
filetime::FileTime::from_unix_time(1_700_000_000, 0),
)
.unwrap();
let mut conn = Connection::open_in_memory().unwrap();
let manager = RepoManager::new(repo_dir.to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
let indexed = manager.read_indexed_package(&pkg_path).unwrap();
manager.insert_indexed_package(&mut conn, indexed).unwrap();
type PackageRow = (
String,
Option<String>,
String,
i64,
i64,
String,
Option<String>,
Option<String>,
Option<String>,
String,
String,
);
let (
name,
real_name,
version,
revision,
abi_breaking,
built_against,
desc,
home,
lic,
sha256,
sha512,
): PackageRow = conn
.query_row(
"SELECT name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, sha256, sha512 FROM packages",
[],
|r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
r.get(4)?,
r.get(5)?,
r.get(6)?,
r.get(7)?,
r.get(8)?,
r.get(9)?,
r.get(10)?,
))
},
)
.unwrap();
assert_eq!(name, "test");
assert_eq!(real_name, Some("icu".to_string()));
assert_eq!(version, "1.0");
assert_eq!(revision, 1);
assert_eq!(abi_breaking, 1);
assert_eq!(built_against, "icu78");
assert_eq!(desc, Some("test description".to_string()));
assert_eq!(home, Some("https://example.com".to_string()));
assert_eq!(lic, Some("MIT".to_string()));
assert_eq!(sha256.len(), 64);
assert_eq!(sha512.len(), 128);
let completed_at: Option<i64> = conn
.query_row("SELECT completed_at FROM packages", [], |r| r.get(0))
.unwrap();
assert_eq!(completed_at, Some(1_773_146_096));
let provides_count: i64 = conn
.query_row("SELECT count(*) FROM provides", [], |r| r.get(0))
.unwrap();
assert_eq!(provides_count, 1);
}
#[test]
fn test_index_package_with_multiple_licenses() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path();
let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst");
let file = fs::File::create(&pkg_path).unwrap();
let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap();
let mut tar = tar::Builder::new(encoder);
let metadata = r#"
name = "test"
version = "1.0"
revision = 1
license = ["MIT", "Apache-2.0"]
"#;
let mut header = tar::Header::new_gnu();
header.set_path(".metadata.toml").unwrap();
header.set_size(metadata.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append(&header, metadata.as_bytes()).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut conn = Connection::open_in_memory().unwrap();
let manager = RepoManager::new(repo_dir.to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
let indexed = manager.read_indexed_package(&pkg_path).unwrap();
manager.insert_indexed_package(&mut conn, indexed).unwrap();
let lic: Option<String> = conn
.query_row("SELECT license FROM packages", [], |r| r.get(0))
.unwrap();
assert_eq!(lic, Some("MIT, Apache-2.0".to_string()));
}
#[test]
fn test_index_package_records_symlink_paths_for_repo_owns() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path();
let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst");
let file = fs::File::create(&pkg_path).unwrap();
let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap();
let mut tar = tar::Builder::new(encoder);
let metadata = r#"
name = "test"
version = "1.0"
revision = 1
"#;
let mut header = tar::Header::new_gnu();
header.set_path(".metadata.toml").unwrap();
header.set_size(metadata.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append(&header, metadata.as_bytes()).unwrap();
let mut file_header = tar::Header::new_gnu();
file_header.set_path("usr/bin/coreutils").unwrap();
file_header.set_size(4);
file_header.set_mode(0o755);
file_header.set_cksum();
tar.append(&file_header, &b"test"[..]).unwrap();
let mut link_header = tar::Header::new_gnu();
link_header.set_entry_type(tar::EntryType::Symlink);
link_header.set_path("usr/bin/ls").unwrap();
link_header.set_link_name("coreutils").unwrap();
link_header.set_size(0);
link_header.set_mode(0o777);
link_header.set_cksum();
tar.append(&link_header, std::io::empty()).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut conn = Connection::open_in_memory().unwrap();
let manager = RepoManager::new(repo_dir.to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
let indexed = manager.read_indexed_package(&pkg_path).unwrap();
manager.insert_indexed_package(&mut conn, indexed).unwrap();
let db_path = repo_dir.join("repo.db");
let mut file_conn = Connection::open(&db_path).unwrap();
manager.init_repo_schema(&mut file_conn).unwrap();
let indexed = manager.read_indexed_package(&pkg_path).unwrap();
manager
.insert_indexed_package(&mut file_conn, indexed)
.unwrap();
drop(file_conn);
let hits = cached_binary_repo_owns_path("repo", &db_path, "usr/bin/ls").unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].package_name, "test");
assert_eq!(hits[0].path, "usr/bin/ls");
}
#[test]
fn test_search_cached_binary_repo_db_matches_name_and_provides() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("repo.db");
let mut conn = Connection::open(&db_path).unwrap();
let manager = RepoManager::new(tmp.path().to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
conn.execute(
"INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'foo', '1.2.3', 1, 'Foo package', 'https://example.test', 'MIT', 'foo-1.2.3-1-x86_64.depot.pkg.tar.zst', 1234, 'a', 'b')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO provides (package_id, name) VALUES (1, 'libfoo.so')",
[],
)
.unwrap();
drop(conn);
let name_hits = search_cached_binary_repo_db("testrepo", &db_path, "foo").unwrap();
assert_eq!(name_hits.len(), 1);
assert_eq!(name_hits[0].name, "foo");
assert_eq!(name_hits[0].repo_name, "testrepo");
assert!(name_hits[0].provides.iter().any(|p| p == "libfoo.so"));
let provide_hits = search_cached_binary_repo_db("testrepo", &db_path, "libfoo").unwrap();
assert_eq!(provide_hits.len(), 1);
assert_eq!(provide_hits[0].name, "foo");
}
#[test]
fn test_find_cached_binary_repo_packages_by_group() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("repo.db");
let mut conn = Connection::open(&db_path).unwrap();
let manager = RepoManager::new(tmp.path().to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
conn.execute(
"INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'foo', '1.2.3', 1, 'Foo package', 'https://example.test', 'MIT', 'foo-1.2.3-1-x86_64.depot.pkg.tar.zst', 1234, 'a', 'b')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO groups (package_id, name) VALUES (1, 'base')",
[],
)
.unwrap();
drop(conn);
let hits = find_cached_binary_repo_packages_by_group("testrepo", &db_path, "base").unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].name, "foo");
assert_eq!(hits[0].groups, vec!["base".to_string()]);
}
#[test]
#[cfg(unix)]
fn test_repo_owns_query_candidates_follow_rootfs_symlink_targets() {
let rootfs = tempfile::tempdir().unwrap();
let usr_bin = rootfs.path().join("usr/bin");
fs::create_dir_all(&usr_bin).unwrap();
fs::write(usr_bin.join("coreutils"), b"payload").unwrap();
std::os::unix::fs::symlink("coreutils", usr_bin.join("ls")).unwrap();
std::os::unix::fs::symlink("usr/bin", rootfs.path().join("bin")).unwrap();
let ls_candidates = repo_owns_query_candidates(rootfs.path(), "/usr/bin/ls");
assert!(
ls_candidates
.iter()
.any(|candidate| candidate == "usr/bin/ls")
);
assert!(
ls_candidates
.iter()
.any(|candidate| candidate == "usr/bin/coreutils")
);
let bin_candidates = repo_owns_query_candidates(rootfs.path(), "/bin/ls");
assert!(bin_candidates.iter().any(|candidate| candidate == "bin/ls"));
assert!(
bin_candidates
.iter()
.any(|candidate| candidate == "usr/bin/coreutils")
);
}
#[test]
fn test_find_cached_binary_repo_package_prefers_exact_name() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("repo.db");
let mut conn = Connection::open(&db_path).unwrap();
let manager = RepoManager::new(tmp.path().to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
conn.execute(
"INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'foo', '1.0', 1, NULL, NULL, NULL, 'foo-1.0-1.depot.pkg.tar.zst', 10, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, version, revision, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'bar', '1.0', 1, NULL, NULL, NULL, 'bar-1.0-1.depot.pkg.tar.zst', 10, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO provides (package_id, name) VALUES (2, 'foo')",
[],
)
.unwrap();
drop(conn);
let recs = find_cached_binary_repo_packages("repo", &db_path, "foo").unwrap();
let rec = recs.first().expect("expected a match");
assert_eq!(rec.name, "foo");
assert_eq!(rec.filename, "foo-1.0-1.depot.pkg.tar.zst");
}
#[test]
fn test_find_cached_binary_repo_packages_matches_real_name_and_built_against() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("repo.db");
let mut conn = Connection::open(&db_path).unwrap();
let manager = RepoManager::new(tmp.path().to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, 'icu78.pkg', 10, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, 'app.pkg', 10, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (2, 'runtime', 'icu')",
[],
)
.unwrap();
drop(conn);
let icu_matches = find_cached_binary_repo_packages("repo", &db_path, "icu").unwrap();
assert_eq!(icu_matches.len(), 1);
assert_eq!(icu_matches[0].name, "icu78");
assert_eq!(icu_matches[0].real_name.as_deref(), Some("icu"));
let app = find_cached_binary_repo_packages("repo", &db_path, "app")
.unwrap()
.pop()
.unwrap();
assert_eq!(app.runtime_dependencies, vec!["icu".to_string()]);
assert_eq!(app.built_against, vec!["icu78".to_string()]);
}
#[test]
fn test_verify_binary_package_record_checksums_accepts_valid_hashes() {
use sha2::{Digest, Sha512};
let tmp = tempfile::tempdir().unwrap();
let pkg = tmp.path().join("pkg.depot.pkg.tar.zst");
fs::write(&pkg, b"payload").unwrap();
let sha512 = {
let mut h = Sha512::new();
h.update(b"payload");
crate::hex::encode_lower(h.finalize())
};
let rec = BinaryRepoPackageRecord {
repo_name: "repo".into(),
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: "pkg.depot.pkg.tar.zst".into(),
size: 7,
sha512,
description: None,
homepage: None,
license: None,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
};
verify_binary_package_record_checksums(&pkg, &rec).unwrap();
}
#[test]
fn test_verify_binary_package_record_checksums_requires_valid_sha512() {
use sha2::{Digest, Sha512};
let tmp = tempfile::tempdir().unwrap();
let pkg = tmp.path().join("pkg.depot.pkg.tar.zst");
fs::write(&pkg, b"payload").unwrap();
let mut rec = test_record_for_payload("pkg.depot.pkg.tar.zst", b"payload");
verify_binary_package_record_checksums(&pkg, &rec).unwrap();
let mut wrong_sha512 = Sha512::new();
wrong_sha512.update(b"different payload");
rec.sha512 = crate::hex::encode_lower(wrong_sha512.finalize());
let err = verify_binary_package_record_checksums(&pkg, &rec).unwrap_err();
assert!(err.to_string().contains("SHA-512 mismatch"));
}
fn test_record_for_payload(filename: &str, payload: &[u8]) -> BinaryRepoPackageRecord {
use sha2::{Digest, Sha512};
let sha512 = {
let mut h = Sha512::new();
h.update(payload);
crate::hex::encode_lower(h.finalize())
};
BinaryRepoPackageRecord {
repo_name: "repo".into(),
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: filename.to_string(),
size: payload.len() as u64,
sha512,
description: None,
homepage: None,
license: None,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
}
}
#[test]
fn test_fetch_binary_package_archive_requires_signature_when_unsigned_disallowed() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
let payload = b"package payload";
std::fs::write(repo_dir.path().join(filename), payload).unwrap();
let rec = test_record_for_payload(filename, payload);
let repo_url = url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string();
let repo_cfg = crate::config::BinaryRepo {
url: repo_url,
allow_unsigned: false,
..Default::default()
};
let err =
fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path())
.expect_err("missing detached signature should fail");
assert!(err.to_string().to_ascii_lowercase().contains("signature"));
}
#[test]
fn test_fetch_binary_package_archive_verifies_signature_and_checksum() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path());
std::fs::create_dir_all(&trusted_dir).unwrap();
let keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap();
std::fs::write(
trusted_dir.join("repo.pub"),
keypair.pk.to_box().unwrap().to_bytes(),
)
.unwrap();
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
let payload = b"signed package payload";
let package_path = repo_dir.path().join(filename);
std::fs::write(&package_path, payload).unwrap();
let sig = minisign::sign(
Some(&keypair.pk),
&keypair.sk,
std::fs::File::open(&package_path).unwrap(),
None,
Some("test signature"),
)
.unwrap();
std::fs::write(format!("{}.sig", package_path.display()), sig.to_bytes()).unwrap();
let rec = test_record_for_payload(filename, payload);
let repo_url = url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string();
let repo_cfg = crate::config::BinaryRepo {
url: repo_url,
allow_unsigned: false,
..Default::default()
};
let fetched =
fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path())
.unwrap();
assert_eq!(std::fs::read(&fetched).unwrap(), payload);
assert!(PathBuf::from(format!("{}.sig", fetched.display())).exists());
}
#[test]
fn test_cache_binary_package_archive_supports_combined_integrity_verification() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path());
std::fs::create_dir_all(&trusted_dir).unwrap();
let keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap();
std::fs::write(
trusted_dir.join("repo.pub"),
keypair.pk.to_box().unwrap().to_bytes(),
)
.unwrap();
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
let payload = b"staged verification payload";
let package_path = repo_dir.path().join(filename);
std::fs::write(&package_path, payload).unwrap();
let sig = minisign::sign(
Some(&keypair.pk),
&keypair.sk,
std::fs::File::open(&package_path).unwrap(),
None,
Some("test signature"),
)
.unwrap();
std::fs::write(format!("{}.sig", package_path.display()), sig.to_bytes()).unwrap();
let rec = test_record_for_payload(filename, payload);
let repo_url = url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string();
let repo_cfg = crate::config::BinaryRepo {
url: repo_url,
allow_unsigned: false,
..Default::default()
};
let cached = cache_binary_package_archive("repo", &repo_cfg, &rec, cache_dir.path())
.expect("cache should succeed");
assert!(cached.package_path.exists());
assert!(cached.signature_path.exists());
verify_binary_package_archive_checksums(&cached.package_path, &rec)
.expect("checksum verification should succeed");
let trusted_keys = crate::signing::load_trusted_public_keys(rootfs.path()).unwrap();
verify_binary_package_archive_integrity_with_trusted_keys(
"repo",
&repo_cfg,
&rec,
&cached.package_path,
&cached.signature_path,
&trusted_keys,
)
.expect("combined integrity verification should succeed");
let mut wrong_record = rec.clone();
wrong_record.sha512 = crate::hex::encode_lower(Sha512::digest(b"wrong payload"));
let error = verify_binary_package_archive_integrity_with_trusted_keys(
"repo",
&repo_cfg,
&wrong_record,
&cached.package_path,
&cached.signature_path,
&trusted_keys,
)
.expect_err("combined verification must reject a checksum mismatch");
assert!(error.to_string().contains("SHA-512 mismatch"));
}
#[test]
fn test_fetch_binary_package_archive_allows_missing_signature_when_configured() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
let payload = b"unsigned package payload";
std::fs::write(repo_dir.path().join(filename), payload).unwrap();
let rec = test_record_for_payload(filename, payload);
let repo_url = url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string();
let repo_cfg = crate::config::BinaryRepo {
url: repo_url,
allow_unsigned: true,
..Default::default()
};
let fetched =
fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path())
.unwrap();
assert_eq!(std::fs::read(&fetched).unwrap(), payload);
assert!(!PathBuf::from(format!("{}.sig", fetched.display())).exists());
}
#[test]
fn test_copy_file_url_to_path_supports_file_scheme() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("repo.db.zst");
let dst = tmp.path().join("copy.zst");
fs::write(&src, b"repo-db").unwrap();
let url = format!("file://{}", src.display());
let outcome = copy_file_url_to_path(&url, &dst).unwrap();
assert_eq!(outcome, FileUrlCopyOutcome::Copied);
assert_eq!(fs::read(&dst).unwrap(), b"repo-db");
}
#[test]
fn test_copy_file_url_to_path_reports_missing_file() {
let tmp = tempfile::tempdir().unwrap();
let missing = tmp.path().join("missing.db.zst");
let dst = tmp.path().join("copy.zst");
let url = format!("file://{}", missing.display());
let outcome = copy_file_url_to_path(&url, &dst).unwrap();
assert_eq!(outcome, FileUrlCopyOutcome::Missing);
assert!(!dst.exists());
}
#[test]
fn test_repo_db_fetch_cache_roundtrip_and_prunes_stale_entry() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("repo.db");
fs::write(&db_path, b"db").unwrap();
let key = RepoDbFetchCacheKey {
repo_name: "core".to_string(),
base_url: "https://repo.example.test/core".to_string(),
repo_db_rel: "repo.db.zst".to_string(),
rootfs: PathBuf::from("/tmp/rootfs-test"),
package_cache_dir: PathBuf::from("/tmp/pkg-cache-test"),
};
remember_repo_db_path(key.clone(), db_path.clone());
assert_eq!(get_cached_repo_db_path(&key), Some(db_path.clone()));
fs::remove_file(&db_path).unwrap();
assert_eq!(get_cached_repo_db_path(&key), None);
}
#[test]
fn test_extract_html_href_targets_parses_common_forms() {
let html = r#"
<html><body>
<a href="alpha.pub">alpha</a>
<a HREF='nested/beta.pub'>beta</a>
<a href=gamma.pub>gamma</a>
<a href="../">parent</a>
</body></html>
"#;
let hrefs = extract_html_href_targets(html);
assert!(hrefs.iter().any(|h| h == "alpha.pub"));
assert!(hrefs.iter().any(|h| h == "nested/beta.pub"));
assert!(hrefs.iter().any(|h| h == "gamma.pub"));
assert!(hrefs.iter().any(|h| h == "../"));
}
#[test]
fn test_list_repo_public_key_urls_reads_file_repo_keys_dir() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path().join("repo");
let keys_dir = repo_dir.join("keys");
fs::create_dir_all(&keys_dir).unwrap();
fs::write(keys_dir.join("repo.pub"), b"pubkey").unwrap();
fs::write(keys_dir.join("ignore.txt"), b"nope").unwrap();
fs::create_dir_all(keys_dir.join("subdir")).unwrap();
let base_url = url::Url::from_directory_path(&repo_dir)
.expect("file URL")
.to_string();
let client = reqwest::blocking::Client::builder().build().unwrap();
let keys = list_repo_public_key_urls(&base_url, &client).unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].0, "repo.pub");
assert!(keys[0].1.ends_with("/repo.pub"));
}
#[test]
fn test_list_repo_public_key_urls_probes_common_names_when_index_missing() {
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::thread;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let server = thread::spawn(move || {
for _ in 0..7 {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut request_line = String::new();
reader.read_line(&mut request_line).unwrap();
loop {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
if line == "\r\n" || line.is_empty() {
break;
}
}
let path = request_line
.split_whitespace()
.nth(1)
.unwrap_or_default()
.to_string();
let (status, body) = if path == "/core/keys/vertex.pub" {
("200 OK", "trusted-key")
} else {
("404 Not Found", "missing")
};
write!(
stream,
"HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
)
.unwrap();
stream.flush().unwrap();
}
});
let base_url = format!("http://{}/core", addr);
let client = reqwest::blocking::Client::builder().build().unwrap();
let keys = list_repo_public_key_urls(&base_url, &client).unwrap();
server.join().unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].0, "vertex.pub");
assert!(keys[0].1.ends_with("/core/keys/vertex.pub"));
}
#[test]
fn test_fetch_binary_repo_db_can_recover_from_stale_trusted_key() {
use std::io::Write;
struct AssumeYesReset;
impl Drop for AssumeYesReset {
fn drop(&mut self) {
crate::ui::set_assume_yes(false);
}
}
crate::ui::set_assume_yes(true);
let _reset = AssumeYesReset;
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let stale_keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap();
let repo_keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap();
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path());
fs::create_dir_all(&trusted_dir).unwrap();
fs::write(
trusted_dir.join("vertex.pub"),
stale_keypair.pk.to_box().unwrap().to_bytes(),
)
.unwrap();
let repo_keys_dir = repo_dir.path().join("keys");
fs::create_dir_all(&repo_keys_dir).unwrap();
fs::write(
repo_keys_dir.join("vertex.pub"),
repo_keypair.pk.to_box().unwrap().to_bytes(),
)
.unwrap();
let repo_db_path = repo_dir.path().join("repo.db.zst");
let mut encoder =
zstd::stream::write::Encoder::new(fs::File::create(&repo_db_path).unwrap(), 3).unwrap();
encoder.write_all(b"repo-db-content").unwrap();
encoder.finish().unwrap();
let sig = minisign::sign(
Some(&repo_keypair.pk),
&repo_keypair.sk,
fs::File::open(&repo_db_path).unwrap(),
None,
Some("repo db signature"),
)
.unwrap();
fs::write(repo_dir.path().join("repo.db.zst.sig"), sig.to_bytes()).unwrap();
let repo_cfg = crate::config::BinaryRepo {
url: url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string(),
allow_unsigned: false,
..Default::default()
};
let sqlite_db =
fetch_binary_repo_db("core", &repo_cfg, rootfs.path(), cache_dir.path()).unwrap();
assert_eq!(fs::read(sqlite_db).unwrap(), b"repo-db-content");
let installed_key = trusted_dir.join("core-vertex.pub");
assert!(installed_key.exists());
assert_eq!(
fs::read(installed_key).unwrap(),
repo_keypair.pk.to_box().unwrap().to_bytes()
);
}
#[test]
fn test_normalize_git_mirror_url_converts_file_scheme() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path().join("repo.git");
fs::create_dir_all(&repo_dir).unwrap();
let url = format!("file://{}", repo_dir.display());
let normalized = normalize_git_mirror_url(&url).unwrap();
assert_eq!(normalized, repo_dir.to_string_lossy());
}
+488
View File
@@ -0,0 +1,488 @@
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
};
use crate::test_support::TestEnv;
use std::path::PathBuf;
fn mk_spec(name: &str, version: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: name.into(),
real_name: None,
version: version.into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives {
provides: vec![format!("{}-virtual", name)],
conflicts: Vec::new(),
replaces: Vec::new(),
lib32: None,
},
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
#[test]
fn register_package_updates_in_place_and_replaces_file_list() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let spec_v1 = mk_spec("foo", "1.0");
let dest1 = tmp.path().join("dest1");
std::fs::create_dir_all(dest1.join("usr/bin")).unwrap();
std::fs::write(dest1.join("usr/bin/foo"), "v1").unwrap();
register_package(&db_path, &spec_v1, &dest1).unwrap();
// Capture package id
let conn = Connection::open(&db_path).unwrap();
let id1: i64 = conn
.query_row(
"SELECT id FROM packages WHERE name = ?1",
params!["foo"],
|r| r.get(0),
)
.unwrap();
// Update with different file set
let spec_v2 = mk_spec("foo", "2.0");
let dest2 = tmp.path().join("dest2");
std::fs::create_dir_all(dest2.join("usr/bin")).unwrap();
std::fs::write(dest2.join("usr/bin/foo"), "v2").unwrap();
std::fs::write(dest2.join("usr/bin/new_only"), "x").unwrap();
register_package(&db_path, &spec_v2, &dest2).unwrap();
let id2: i64 = conn
.query_row(
"SELECT id FROM packages WHERE name = ?1",
params!["foo"],
|r| r.get(0),
)
.unwrap();
assert_eq!(id1, id2);
let files = get_package_files(&db_path, "foo").unwrap();
assert!(files.contains(&"usr/bin/foo".to_string()));
assert!(files.contains(&"usr/bin/new_only".to_string()));
let version = get_package_version(&db_path, "foo").unwrap();
assert_eq!(version.as_deref(), Some("2.0"));
}
#[test]
fn installed_dependency_names_include_real_name_aliases() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&destdir).unwrap();
let mut spec = mk_spec("libressl43", "4.3.2");
spec.package.real_name = Some("libressl".into());
register_package(&db_path, &spec, &destdir).unwrap();
let names = get_installed_dependency_names(&db_path).unwrap();
assert!(names.contains("libressl43"));
assert!(names.contains("libressl"));
assert_eq!(
get_dependency_version(&db_path, "libressl")
.unwrap()
.as_deref(),
Some("4.3.2")
);
}
#[test]
fn register_package_uses_metadata_completed_at_when_present() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let spec = mk_spec("foo", "1.0");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "bin").unwrap();
std::fs::write(
dest.join(".metadata.toml"),
"completed_at = \"2026-03-10T12:34:56Z\"\n",
)
.unwrap();
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].completed_at, Some(1_773_146_096));
}
#[test]
fn register_package_falls_back_to_destdir_mtime_when_metadata_missing() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let spec = mk_spec("foo", "1.0");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
let file = dest.join("usr/bin/foo");
std::fs::write(&file, "bin").unwrap();
let ts = filetime::FileTime::from_unix_time(1_700_000_000, 0);
filetime::set_file_mtime(&file, ts).unwrap();
filetime::set_file_mtime(dest.join("usr"), ts).unwrap();
filetime::set_file_mtime(dest.join("usr/bin"), ts).unwrap();
filetime::set_file_mtime(&dest, ts).unwrap();
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].completed_at, Some(1_700_000_000));
}
#[test]
fn register_package_detects_conflicting_files() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
// Install package 'alpha' owning usr/bin/shared
let spec_a = mk_spec("alpha", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
std::fs::write(dest_a.join("usr/bin/shared"), "a").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
// Try to install package 'beta' that also includes the same path -> should fail
let spec_b = mk_spec("beta", "1.0");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
std::fs::write(dest_b.join("usr/bin/shared"), "b").unwrap();
let res = register_package(&db_path, &spec_b, &dest_b);
assert!(res.is_err());
let err = format!("{}", res.err().unwrap());
assert!(err.contains("File ownership conflict detected"));
assert!(err.contains("usr/bin/shared"));
assert!(err.contains("alpha"));
}
#[test]
fn register_package_auto_clears_safe_conflicts() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
// Install package 'alpha' owning a known shared Perl path
let spec_a = mk_spec("alpha", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/share/perl5")).unwrap();
std::fs::write(dest_a.join("usr/share/perl5/shared.pm"), "package A;").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
// Now install package 'beta' that also provides the same shared path -> should auto-clear
let spec_b = mk_spec("beta", "1.0");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/share/perl5")).unwrap();
std::fs::write(dest_b.join("usr/share/perl5/shared.pm"), "package B;").unwrap();
// This should succeed and transfer ownership of the shared path to beta
register_package(&db_path, &spec_b, &dest_b).unwrap();
// Verify DB: alpha should no longer own the path, beta should
let files_a = get_package_files(&db_path, "alpha").unwrap();
assert!(!files_a.contains(&"usr/share/perl5/shared.pm".to_string()));
let files_b = get_package_files(&db_path, "beta").unwrap();
assert!(files_b.contains(&"usr/share/perl5/shared.pm".to_string()));
}
#[test]
fn register_package_auto_clears_sbase_conflicts_when_requested() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("rootfs");
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
std::fs::write(rootfs.join("usr/bin/find"), "sbase find").unwrap();
let spec_a = mk_spec("sbase", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
std::fs::write(dest_a.join("usr/bin/find"), "sbase find").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
let spec_b = mk_spec("bfs", "4.1");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
std::fs::write(dest_b.join("usr/bin/find"), "bfs find").unwrap();
let mut env = TestEnv::new();
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
register_package(&db_path, &spec_b, &dest_b).unwrap();
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
assert!(!files_sbase.contains(&"usr/bin/find".to_string()));
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
assert!(files_bfs.contains(&"usr/bin/find".to_string()));
}
#[test]
fn register_package_auto_clear_preserves_new_payload_on_disk() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("rootfs");
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
std::fs::write(rootfs.join("usr/bin/find"), "sbase find").unwrap();
let spec_a = mk_spec("sbase", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
std::fs::write(dest_a.join("usr/bin/find"), "sbase find").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
std::fs::write(rootfs.join("usr/bin/find"), "bfs find").unwrap();
let spec_b = mk_spec("bfs", "4.1");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
std::fs::write(dest_b.join("usr/bin/find"), "bfs find").unwrap();
let mut env = TestEnv::new();
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
register_package(&db_path, &spec_b, &dest_b).unwrap();
assert_eq!(
std::fs::read_to_string(rootfs.join("usr/bin/find")).unwrap(),
"bfs find"
);
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
assert!(!files_sbase.contains(&"usr/bin/find".to_string()));
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
assert!(files_bfs.contains(&"usr/bin/find".to_string()));
}
#[test]
fn get_package_files_missing_package_returns_empty() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
// Create an empty database file with schema but no packages
let conn = Connection::open(&db_path).unwrap();
init_db(&conn).unwrap();
drop(conn);
// Querying files for a package that doesn't exist should return an empty list
let files = get_package_files(&db_path, "nonexistent").unwrap();
assert!(files.is_empty());
}
#[test]
fn get_package_version_missing_db_returns_none_without_creating_db() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let version = get_package_version(&db_path, "nonexistent").unwrap();
assert!(version.is_none());
assert!(!db_path.exists());
}
#[test]
fn calculate_upgrade_paths_handles_existing_db_file_without_schema() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
std::fs::File::create(&db_path).unwrap();
let manifest = staging::Manifest {
files: vec!["usr/bin/foo".to_string()],
directories: Vec::new(),
};
let remove_paths = calculate_upgrade_paths(&db_path, "nonexistent", &manifest).unwrap();
assert!(remove_paths.is_empty());
}
#[test]
fn remove_package_tolerates_missing_files_and_cleans_db() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&rootfs).unwrap();
let spec = mk_spec("foo", "1.0");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "bin").unwrap();
register_package(&db_path, &spec, &dest).unwrap();
// Create the installed file in rootfs (one real)
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
std::fs::write(rootfs.join("usr/bin/foo"), "bin").unwrap();
// Inject an extra missing file into DB to ensure we tolerate it.
let conn = Connection::open(&db_path).unwrap();
let pkg_id: i64 = conn
.query_row(
"SELECT id FROM packages WHERE name = ?1",
params!["foo"],
|r| r.get(0),
)
.unwrap();
conn.execute(
"INSERT OR IGNORE INTO files (package_id, path) VALUES (?1, ?2)",
params![pkg_id, "usr/bin/does_not_exist"],
)
.unwrap();
remove_package(&db_path, "foo", &rootfs).unwrap();
assert!(get_package_version(&db_path, "foo").unwrap().is_none());
}
#[test]
fn test_package_upgrade_removes_orphaned_files() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let rootfs = tmp.path().join("root");
let tx_base = tmp.path().join("tx");
std::fs::create_dir_all(&rootfs).unwrap();
// 1. Install v1: usr/bin/foo, usr/bin/shared_dir/old_file
let spec_v1 = mk_spec("foo", "1.0");
let dest1 = tmp.path().join("dest1");
std::fs::create_dir_all(dest1.join("usr/bin/shared_dir")).unwrap();
std::fs::write(dest1.join("usr/bin/foo"), "v1").unwrap();
std::fs::write(dest1.join("usr/bin/shared_dir/old_file"), "old").unwrap();
register_package(&db_path, &spec_v1, &dest1).unwrap();
let _ = crate::staging::install_atomic(&dest1, &rootfs, &tx_base, &[], &[]).unwrap();
assert!(rootfs.join("usr/bin/foo").exists());
assert!(rootfs.join("usr/bin/shared_dir/old_file").exists());
// 2. Prepare v2: usr/bin/foo (updated), usr/bin/new_file
// (shared_dir/old_file is removed from spec)
let spec_v2 = mk_spec("foo", "2.0");
let dest2 = tmp.path().join("dest2");
std::fs::create_dir_all(dest2.join("usr/bin")).unwrap();
std::fs::write(dest2.join("usr/bin/foo"), "v2").unwrap();
std::fs::write(dest2.join("usr/bin/new_file"), "new").unwrap();
let manifest2 = crate::staging::generate_manifest_with_dirs(&dest2).unwrap();
let remove_paths = calculate_upgrade_paths(&db_path, "foo", &manifest2).unwrap();
assert_eq!(
remove_paths,
vec![
"usr/bin/shared_dir/old_file".to_string(),
"usr/bin/shared_dir".to_string()
]
);
let tx = crate::staging::install_atomic(&dest2, &rootfs, &tx_base, &remove_paths, &[]).unwrap();
register_package(&db_path, &spec_v2, &dest2).unwrap();
tx.commit().unwrap();
// 3. Verify filesystem
assert_eq!(
std::fs::read_to_string(rootfs.join("usr/bin/foo")).unwrap(),
"v2"
);
assert!(rootfs.join("usr/bin/new_file").exists());
assert!(!rootfs.join("usr/bin/shared_dir/old_file").exists());
// Check DB
let files = get_package_files(&db_path, "foo").unwrap();
assert!(files.contains(&"usr/bin/foo".to_string()));
assert!(files.contains(&"usr/bin/new_file".to_string()));
assert!(!files.contains(&"usr/bin/shared_dir/old_file".to_string()));
}
#[test]
fn register_package_persists_replacements() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/vx"), "vx").unwrap();
let mut spec = mk_spec("vx", "1.0");
spec.alternatives.replaces = vec!["grep".into(), "patch".into()];
register_package(&db_path, &spec, &dest).unwrap();
let replaces = get_all_replaces(&db_path).unwrap();
assert!(replaces.contains("grep"));
assert!(replaces.contains("patch"));
}
#[test]
fn register_package_persists_groups() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "foo").unwrap();
let mut spec = mk_spec("foo", "1.0");
spec.dependencies.groups = vec!["base".into(), "desktop".into()];
register_package(&db_path, &spec, &dest).unwrap();
assert_eq!(
get_package_groups(&db_path, "foo").unwrap(),
vec!["base".to_string(), "desktop".to_string()]
);
}
#[test]
fn register_package_records_built_against_metadata() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(&dest).unwrap();
std::fs::write(dest.join("payload"), "x").unwrap();
let mut spec = mk_spec("app", "1.0");
spec.package.built_against = vec!["icu78".into()];
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].built_against, vec!["icu78".to_string()]);
}
#[test]
fn installed_group_helpers_round_trip_membership() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
std::fs::write(dest.join("usr/bin/foo"), "foo").unwrap();
let mut spec = mk_spec("foo", "1.0");
spec.dependencies.groups = vec!["base".into()];
register_package(&db_path, &spec, &dest).unwrap();
record_installed_groups(&db_path, &[String::from("base")]).unwrap();
assert!(is_installed_group(&db_path, "base").unwrap());
assert_eq!(
get_packages_in_installed_group(&db_path, "base").unwrap(),
vec!["foo".to_string()]
);
remove_installed_group(&db_path, "base").unwrap();
assert!(!is_installed_group(&db_path, "base").unwrap());
}
+145 -8
View File
@@ -1,14 +1,24 @@
//! Rootless install command support backed by Linux user namespaces.
use anyhow::{Context, Result};
use std::ffi::{CString, OsString};
use std::fs;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
const UID_MAP_PATH: &[u8] = b"/proc/self/uid_map\0";
const GID_MAP_PATH: &[u8] = b"/proc/self/gid_map\0";
const SETGROUPS_PATH: &[u8] = b"/proc/self/setgroups\0";
const UID_XATTR: &[u8] = b"user.depot.fakeroot.uid\0";
const GID_XATTR: &[u8] = b"user.depot.fakeroot.gid\0";
const PRELOAD_LIBRARY_BYTES: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/libdepot_fakeroot.so"));
static PRELOAD_LIBRARY_DIR: OnceLock<tempfile::TempDir> = OnceLock::new();
/// Check if the current process is running as root.
pub fn is_root() -> bool {
@@ -20,15 +30,133 @@ pub fn is_root() -> bool {
/// The namespace maps UID/GID 0 to the invoking user, allowing install scripts
/// to perform root-owned staged installs without changing host ownership or
/// requiring an external fakeroot implementation.
pub fn wrap_install_command(program: &str, destdir: &Path) -> Command {
pub fn wrap_install_command(program: &str, destdir: &Path) -> Result<Command> {
let mut command = build_command(program, shell_script_path(program));
command.env("DESTDIR", destdir);
if !is_root() {
configure_ownership_preload(&mut command)?;
configure_rootless_namespace(&mut command);
}
command
Ok(command)
}
fn configure_ownership_preload(command: &mut Command) -> Result<()> {
let library_path = materialize_preload_library()?;
let mut preload = OsString::from(&library_path);
if let Some(existing) = std::env::var_os("LD_PRELOAD").filter(|value| !value.is_empty()) {
preload.push(":");
preload.push(existing);
}
command.env("LD_PRELOAD", preload);
Ok(())
}
fn materialize_preload_library() -> Result<PathBuf> {
if let Some(dir) = PRELOAD_LIBRARY_DIR.get() {
return Ok(dir.path().join("libdepot_fakeroot.so"));
}
let candidate = tempfile::Builder::new()
.prefix("depot-fakeroot-")
.tempdir()
.context("Failed to create private fakeroot library dir")?;
let candidate_path = candidate.path().join("libdepot_fakeroot.so");
fs::write(&candidate_path, PRELOAD_LIBRARY_BYTES).with_context(|| {
format!(
"Failed to write fakeroot library {}",
candidate_path.display()
)
})?;
fs::set_permissions(&candidate_path, fs::Permissions::from_mode(0o500)).with_context(|| {
format!(
"Failed to set permissions on fakeroot library {}",
candidate_path.display()
)
})?;
let candidate_path = candidate_path.canonicalize().with_context(|| {
format!(
"Failed to resolve fakeroot library {}",
candidate_path.display()
)
})?;
if PRELOAD_LIBRARY_DIR.set(candidate).is_ok() {
Ok(candidate_path)
} else {
Ok(PRELOAD_LIBRARY_DIR
.get()
.expect("fakeroot library dir must be initialized")
.path()
.join("libdepot_fakeroot.so"))
}
}
pub(crate) fn archive_ownership(
path: &Path,
metadata: &fs::Metadata,
symlink: bool,
) -> Result<(u64, u64)> {
use std::os::unix::fs::MetadataExt;
let (default_uid, default_gid) = if is_root() {
(u64::from(metadata.uid()), u64::from(metadata.gid()))
} else {
(0, 0)
};
let uid = read_id_xattr(path, UID_XATTR, symlink)?.unwrap_or(default_uid);
let gid = read_id_xattr(path, GID_XATTR, symlink)?.unwrap_or(default_gid);
Ok((uid, gid))
}
fn read_id_xattr(path: &Path, name: &[u8], symlink: bool) -> Result<Option<u64>> {
let path = CString::new(path.as_os_str().as_bytes())
.with_context(|| format!("Path contains an embedded NUL: {}", path.display()))?;
let mut value = [0_u8; 16];
let length = unsafe {
if symlink {
nix::libc::lgetxattr(
path.as_ptr(),
name.as_ptr().cast(),
value.as_mut_ptr().cast(),
value.len(),
)
} else {
nix::libc::getxattr(
path.as_ptr(),
name.as_ptr().cast(),
value.as_mut_ptr().cast(),
value.len(),
)
}
};
if length < 0 {
let error = io::Error::last_os_error();
return match error.raw_os_error() {
Some(code) if code == nix::libc::ENODATA || code == nix::libc::ENOTSUP => Ok(None),
_ => Err(error).with_context(|| {
format!(
"Failed to read fakeroot ownership from {}",
path.to_string_lossy()
)
}),
};
}
let value = std::str::from_utf8(&value[..length as usize]).with_context(|| {
format!(
"Invalid fakeroot ownership metadata on {}",
path.to_string_lossy()
)
})?;
let id = value.parse::<u64>().with_context(|| {
format!(
"Invalid fakeroot ownership value {value:?} on {}",
path.to_string_lossy()
)
})?;
Ok(Some(id))
}
fn build_command(program: &str, script_path: Option<&Path>) -> Command {
@@ -126,11 +254,14 @@ mod tests {
#[test]
fn rootless_command_runs_as_namespace_root() -> Result<()> {
if is_root() {
return Ok(());
}
let destdir = tempfile::tempdir()?;
let mut command = wrap_install_command("/bin/sh", destdir.path());
let mut command = wrap_install_command("/bin/sh", destdir.path())?;
command
.arg("-c")
.arg("test \"$(/usr/bin/id -u)\" = 0; test \"$(/usr/bin/id -g)\" = 0; /usr/bin/touch \"$DESTDIR/root-owned\"; /usr/bin/chown 0:0 \"$DESTDIR/root-owned\"; /usr/bin/chmod 4755 \"$DESTDIR/root-owned\"");
.arg("test \"$(/usr/bin/id -u)\" = 0; test \"$(/usr/bin/id -g)\" = 0; /usr/bin/touch \"$DESTDIR/root-owned\"; if test -x /usr/bin/python3; then /usr/bin/python3 -c 'import os; os.chown(os.environ[\"DESTDIR\"] + \"/root-owned\", 0, 81)'; else /usr/bin/chown 0:81 \"$DESTDIR/root-owned\"; fi; /usr/bin/chmod 4755 \"$DESTDIR/root-owned\"");
let status = command
.status()
@@ -141,16 +272,22 @@ mod tests {
assert_eq!(metadata.uid(), nix::unistd::geteuid().as_raw());
assert_eq!(metadata.gid(), nix::unistd::getegid().as_raw());
assert_eq!(metadata.mode() & 0o7777, 0o4755);
assert_eq!(
archive_ownership(&destdir.path().join("root-owned"), &metadata, false)?,
(0, 81)
);
Ok(())
}
#[test]
fn wrapped_command_preserves_program_and_sets_destdir() {
let command = wrap_install_command("make", Path::new("/tmp/package-root"));
fn wrapped_command_preserves_program_and_sets_destdir() -> Result<()> {
let destdir = tempfile::tempdir()?;
let command = wrap_install_command("make", destdir.path())?;
assert_eq!(command.get_program(), "make");
assert!(command.get_envs().any(|(key, value)| {
key == "DESTDIR" && value.is_some_and(|value| value == "/tmp/package-root")
key == "DESTDIR" && value.is_some_and(|value| value == destdir.path())
}));
Ok(())
}
}
+144
View File
@@ -0,0 +1,144 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <unistd.h>
#define DEPOT_UID_XATTR "user.depot.fakeroot.uid"
#define DEPOT_GID_XATTR "user.depot.fakeroot.gid"
static uid_t mapped_uid(uid_t uid) {
return uid == (uid_t)-1 ? uid : 0;
}
static gid_t mapped_gid(gid_t gid) {
return gid == (gid_t)-1 ? gid : 0;
}
static int missing_xattr_error(int error) {
return error == ENODATA || error == ENOTSUP;
}
static int update_id_path(const char *path, const char *name, unsigned int id,
int nofollow) {
char value[16];
int length = snprintf(value, sizeof(value), "%u", id);
if (length < 0 || (size_t)length >= sizeof(value)) {
errno = EOVERFLOW;
return -1;
}
if (id == 0) {
int result = nofollow != 0 ? lremovexattr(path, name)
: removexattr(path, name);
if (result == 0 || missing_xattr_error(errno) ||
(nofollow != 0 && errno == EPERM)) {
return 0;
}
return -1;
}
if (nofollow != 0) {
return lsetxattr(path, name, value, (size_t)length, 0);
}
return setxattr(path, name, value, (size_t)length, 0);
}
static int update_id_fd(int fd, const char *name, unsigned int id) {
char value[16];
int length = snprintf(value, sizeof(value), "%u", id);
if (length < 0 || (size_t)length >= sizeof(value)) {
errno = EOVERFLOW;
return -1;
}
if (id == 0) {
int result = fremovexattr(fd, name);
if (result == 0 || missing_xattr_error(errno)) {
return 0;
}
return -1;
}
return fsetxattr(fd, name, value, (size_t)length, 0);
}
static int record_path(const char *path, uid_t uid, gid_t gid, int nofollow) {
if (uid != (uid_t)-1 &&
update_id_path(path, DEPOT_UID_XATTR, (unsigned int)uid, nofollow) !=
0) {
return -1;
}
if (gid != (gid_t)-1 &&
update_id_path(path, DEPOT_GID_XATTR, (unsigned int)gid, nofollow) !=
0) {
return -1;
}
return 0;
}
static int record_fd(int fd, uid_t uid, gid_t gid) {
if (uid != (uid_t)-1 &&
update_id_fd(fd, DEPOT_UID_XATTR, (unsigned int)uid) != 0) {
return -1;
}
if (gid != (gid_t)-1 &&
update_id_fd(fd, DEPOT_GID_XATTR, (unsigned int)gid) != 0) {
return -1;
}
return 0;
}
int chown(const char *path, uid_t uid, gid_t gid) {
if (syscall(SYS_chown, path, mapped_uid(uid), mapped_gid(gid)) != 0) {
return -1;
}
return record_path(path, uid, gid, 0);
}
int lchown(const char *path, uid_t uid, gid_t gid) {
if (syscall(SYS_lchown, path, mapped_uid(uid), mapped_gid(gid)) != 0) {
return -1;
}
return record_path(path, uid, gid, 1);
}
int fchown(int fd, uid_t uid, gid_t gid) {
if (syscall(SYS_fchown, fd, mapped_uid(uid), mapped_gid(gid)) != 0) {
return -1;
}
return record_fd(fd, uid, gid);
}
int fchownat(int dirfd, const char *path, uid_t uid, gid_t gid, int flags) {
int fd;
int open_flags = O_RDONLY | O_CLOEXEC | O_NONBLOCK;
if (syscall(SYS_fchownat, dirfd, path, mapped_uid(uid), mapped_gid(gid),
flags) != 0) {
return -1;
}
if ((flags & AT_SYMLINK_NOFOLLOW) != 0) {
if (dirfd == AT_FDCWD || path[0] == '/') {
return record_path(path, uid, gid, 1);
}
errno = ENOTSUP;
return -1;
}
fd = openat(dirfd, path, open_flags);
if (fd < 0) {
return -1;
}
if (record_fd(fd, uid, gid) != 0) {
int saved_errno = errno;
close(fd);
errno = saved_errno;
return -1;
}
return close(fd);
}
+1 -445
View File
@@ -1549,448 +1549,4 @@ fn cleanup_empty_package_dir(rootfs: &Path, pkg_name: &str) -> Result<()> {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source,
};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
fn mk_spec(spec_dir: &Path) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.to_path_buf(),
}
}
#[test]
fn stage_scripts_from_spec_dir_copies_tree() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(spec_dir.join("scripts/lib")).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(spec_dir.join("scripts/pre_install"), "echo pre").unwrap();
std::fs::write(spec_dir.join("scripts/lib/common.sh"), "echo lib").unwrap();
let spec = mk_spec(&spec_dir);
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(staged);
assert!(destdir.join("scripts/pre_install").exists());
assert!(destdir.join("scripts/lib/common.sh").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(destdir.join("scripts/pre_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn run_hook_if_present_executes_script() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("pre_install"),
"echo \"$DEPOT_ACTION:$DEPOT_PHASE:$DEPOT_PACKAGE\" > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
"install:pre:foo\n"
);
}
#[test]
fn run_hook_if_present_uses_safe_script_path() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("pre_install"),
"echo \"$PATH\" > \"$DEPOT_ROOTFS/path.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("path.out"))
.unwrap()
.trim_end(),
crate::runtime_env::safe_script_path()
);
}
#[test]
fn run_hook_if_present_accepts_compact_script_name() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("postinstall.sh"),
"echo compact > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
"compact\n"
);
}
#[test]
fn run_hook_if_present_prefers_lib32_specific_script_name() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("post_install"),
"echo generic > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
std::fs::write(
scripts.join("post_install-lib32"),
"echo lib32 > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "lib32-foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
"lib32\n"
);
}
#[test]
fn run_hook_if_present_rejects_ambiguous_names() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(scripts.join("pre_update"), "echo one").unwrap();
std::fs::write(scripts.join("pre-update"), "echo two").unwrap();
let err = run_hook_if_present(&scripts, Hook::PreUpdate, &rootfs, "foo")
.expect_err("expected ambiguous script names to fail");
assert!(err.to_string().contains("Ambiguous lifecycle hook"));
}
#[test]
fn run_hook_if_present_with_relative_rootfs_uses_correct_script_and_env_paths() {
let cwd = std::env::current_dir().unwrap();
let tmp = tempfile::Builder::new()
.prefix("depot-hook-rel-rootfs-")
.tempdir_in(&cwd)
.unwrap();
let rootfs_abs = tmp.path().join("root");
std::fs::create_dir_all(&rootfs_abs).unwrap();
let rootfs_rel = rootfs_abs.strip_prefix(&cwd).unwrap().to_path_buf();
let scripts = rootfs_rel.join("scripts");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::write(
scripts.join("pre_install"),
"echo ok > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs_rel, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs_abs.join("hook.out")).unwrap(),
"ok\n"
);
}
#[test]
fn should_bootstrap_host_shell_only_for_chrooted_root_installs_without_shell() {
assert!(should_bootstrap_host_shell(true, true, false));
assert!(!should_bootstrap_host_shell(true, true, true));
assert!(!should_bootstrap_host_shell(true, false, false));
assert!(!should_bootstrap_host_shell(false, true, false));
}
#[test]
fn parse_ldd_dependency_paths_extracts_absolute_paths() {
let parsed = parse_ldd_dependency_paths(
"linux-vdso.so.1 (0x0000)\nlibc.so.6 => /lib/libc.so.6 (0x0000)\n/lib64/ld-linux-x86-64.so.2 (0x0000)\nlibc.so.6 => /lib/libc.so.6 (0x0001)\n",
)
.unwrap();
assert_eq!(
parsed,
vec![
PathBuf::from("/lib/libc.so.6"),
PathBuf::from("/lib64/ld-linux-x86-64.so.2")
]
);
}
#[test]
fn parse_ldd_dependency_paths_rejects_missing_dependencies() {
let err = parse_ldd_dependency_paths("libedit.so.0 => not found\n")
.expect_err("expected ldd parse to fail when a dependency is missing");
assert!(
err.to_string()
.contains("Missing shared-library dependency")
);
}
#[test]
fn parse_hook_command_candidates_finds_commands_after_assignments_and_operators() {
let commands = parse_hook_command_candidates(
"PATH=/tmp:$PATH grep -q foo etc/shells || echo foo >> etc/shells\ncat \"$DEPOT_ROOTFS/usr/bin/find\" | sed 's/x/y/'\n",
);
assert_eq!(
commands,
vec!["cat".to_string(), "grep".to_string(), "sed".to_string()]
);
}
#[test]
fn parse_hook_command_candidates_ignores_builtins_and_control_words() {
let commands = parse_hook_command_candidates("if true; then export FOO=bar; echo hi; fi\n");
assert!(commands.is_empty());
}
#[test]
fn deferred_hooks_file_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("hooks.tsv");
let hooks = vec![
DeferredHook {
pkg_name: "foo".into(),
hook: Hook::PostInstall,
script_rel: PathBuf::from("usr/share/depot/foo/scripts/post_install"),
},
DeferredHook {
pkg_name: "bar".into(),
hook: Hook::PostUpdate,
script_rel: PathBuf::from("usr/share/depot/bar/scripts/post_update"),
},
];
write_deferred_hooks(&path, &hooks).unwrap();
let loaded = read_deferred_hooks(&path).unwrap();
assert_eq!(loaded.len(), hooks.len());
assert_eq!(loaded[0].pkg_name, hooks[0].pkg_name);
assert_eq!(loaded[0].hook, hooks[0].hook);
assert_eq!(loaded[0].script_rel, hooks[0].script_rel);
assert_eq!(loaded[1].pkg_name, hooks[1].pkg_name);
assert_eq!(loaded[1].hook, hooks[1].hook);
assert_eq!(loaded[1].script_rel, hooks[1].script_rel);
}
#[test]
fn queue_deferred_hook_dedupes_entries() {
let tmp = tempfile::tempdir().unwrap();
queue_deferred_hook(
tmp.path(),
"foo",
Hook::PostInstall,
Path::new("usr/share/depot/foo/scripts/post_install"),
)
.unwrap();
queue_deferred_hook(
tmp.path(),
"foo",
Hook::PostInstall,
Path::new("usr/share/depot/foo/scripts/post_install"),
)
.unwrap();
let loaded = read_deferred_hooks(&deferred_hooks_file(tmp.path())).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].pkg_name, "foo");
assert_eq!(loaded[0].hook, Hook::PostInstall);
assert_eq!(
loaded[0].script_rel,
PathBuf::from("usr/share/depot/foo/scripts/post_install")
);
}
#[test]
fn sync_staged_scripts_to_rootfs_replaces_existing_tree() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("root");
let staged = tmp.path().join("staged");
std::fs::create_dir_all(staged.join("scripts")).unwrap();
let installed = installed_scripts_dir(&rootfs, "foo");
std::fs::create_dir_all(&installed).unwrap();
std::fs::write(installed.join("old"), "old").unwrap();
std::fs::write(staged.join("scripts/post_install"), "echo ok").unwrap();
let has_scripts =
sync_staged_scripts_to_rootfs(&staged.join("scripts"), &rootfs, "foo").unwrap();
assert!(has_scripts);
let installed = installed_scripts_dir(&rootfs, "foo");
assert!(!installed.join("old").exists());
assert!(installed.join("post_install").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(installed.join("post_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn sync_staged_scripts_to_rootfs_removes_old_when_none_staged() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("root");
let staged = tmp.path().join("staged");
std::fs::create_dir_all(&staged).unwrap();
let installed = installed_scripts_dir(&rootfs, "foo");
std::fs::create_dir_all(&installed).unwrap();
std::fs::write(installed.join("pre_remove"), "echo old").unwrap();
let has_scripts =
sync_staged_scripts_to_rootfs(&staged.join("scripts"), &rootfs, "foo").unwrap();
assert!(!has_scripts);
assert!(!installed_scripts_dir(&rootfs, "foo").exists());
}
#[test]
fn stage_scripts_from_spec_dir_stages_legacy_root_hook() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(spec_dir.join("postinstall.sh"), "echo post").unwrap();
let spec = mk_spec(&spec_dir);
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(staged);
assert!(destdir.join("scripts/post_install").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(destdir.join("scripts/post_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn stage_scripts_from_spec_dir_stages_lib32_prefixed_legacy_root_hook() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(spec_dir.join("lib32-postinstall.sh"), "echo lib32-post").unwrap();
let mut spec = mk_spec(&spec_dir);
spec.package.name = "lib32-foo".into();
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(staged);
assert!(destdir.join("scripts/post_install").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(destdir.join("scripts/post_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn stage_scripts_from_spec_dir_lib32_ignores_generic_legacy_root_hook() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
// No lib32-prefixed hook; native-only scripts must NOT be staged for lib32 packages.
std::fs::write(spec_dir.join("postinstall.sh"), "echo fallback").unwrap();
let mut spec = mk_spec(&spec_dir);
spec.package.name = "lib32-foo".into();
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(!staged);
assert!(!destdir.join("scripts/post_install").exists());
}
#[test]
fn run_hook_if_present_lib32_ignores_generic_script() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
// Only a generic script exists; lib32 package must NOT execute it.
std::fs::write(
scripts.join("post_install"),
"echo generic > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "lib32-foo").unwrap();
assert!(!ran);
assert!(!rootfs.join("hook.out").exists());
}
}
mod tests;
+443
View File
@@ -0,0 +1,443 @@
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source,
};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
fn mk_spec(spec_dir: &Path) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.to_path_buf(),
}
}
#[test]
fn stage_scripts_from_spec_dir_copies_tree() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(spec_dir.join("scripts/lib")).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(spec_dir.join("scripts/pre_install"), "echo pre").unwrap();
std::fs::write(spec_dir.join("scripts/lib/common.sh"), "echo lib").unwrap();
let spec = mk_spec(&spec_dir);
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(staged);
assert!(destdir.join("scripts/pre_install").exists());
assert!(destdir.join("scripts/lib/common.sh").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(destdir.join("scripts/pre_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn run_hook_if_present_executes_script() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("pre_install"),
"echo \"$DEPOT_ACTION:$DEPOT_PHASE:$DEPOT_PACKAGE\" > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
"install:pre:foo\n"
);
}
#[test]
fn run_hook_if_present_uses_safe_script_path() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("pre_install"),
"echo \"$PATH\" > \"$DEPOT_ROOTFS/path.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("path.out"))
.unwrap()
.trim_end(),
crate::runtime_env::safe_script_path()
);
}
#[test]
fn run_hook_if_present_accepts_compact_script_name() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("postinstall.sh"),
"echo compact > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
"compact\n"
);
}
#[test]
fn run_hook_if_present_prefers_lib32_specific_script_name() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(
scripts.join("post_install"),
"echo generic > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
std::fs::write(
scripts.join("post_install-lib32"),
"echo lib32 > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "lib32-foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
"lib32\n"
);
}
#[test]
fn run_hook_if_present_rejects_ambiguous_names() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(scripts.join("pre_update"), "echo one").unwrap();
std::fs::write(scripts.join("pre-update"), "echo two").unwrap();
let err = run_hook_if_present(&scripts, Hook::PreUpdate, &rootfs, "foo")
.expect_err("expected ambiguous script names to fail");
assert!(err.to_string().contains("Ambiguous lifecycle hook"));
}
#[test]
fn run_hook_if_present_with_relative_rootfs_uses_correct_script_and_env_paths() {
let cwd = std::env::current_dir().unwrap();
let tmp = tempfile::Builder::new()
.prefix("depot-hook-rel-rootfs-")
.tempdir_in(&cwd)
.unwrap();
let rootfs_abs = tmp.path().join("root");
std::fs::create_dir_all(&rootfs_abs).unwrap();
let rootfs_rel = rootfs_abs.strip_prefix(&cwd).unwrap().to_path_buf();
let scripts = rootfs_rel.join("scripts");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::write(
scripts.join("pre_install"),
"echo ok > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs_rel, "foo").unwrap();
assert!(ran);
assert_eq!(
std::fs::read_to_string(rootfs_abs.join("hook.out")).unwrap(),
"ok\n"
);
}
#[test]
fn should_bootstrap_host_shell_only_for_chrooted_root_installs_without_shell() {
assert!(should_bootstrap_host_shell(true, true, false));
assert!(!should_bootstrap_host_shell(true, true, true));
assert!(!should_bootstrap_host_shell(true, false, false));
assert!(!should_bootstrap_host_shell(false, true, false));
}
#[test]
fn parse_ldd_dependency_paths_extracts_absolute_paths() {
let parsed = parse_ldd_dependency_paths(
"linux-vdso.so.1 (0x0000)\nlibc.so.6 => /lib/libc.so.6 (0x0000)\n/lib64/ld-linux-x86-64.so.2 (0x0000)\nlibc.so.6 => /lib/libc.so.6 (0x0001)\n",
)
.unwrap();
assert_eq!(
parsed,
vec![
PathBuf::from("/lib/libc.so.6"),
PathBuf::from("/lib64/ld-linux-x86-64.so.2")
]
);
}
#[test]
fn parse_ldd_dependency_paths_rejects_missing_dependencies() {
let err = parse_ldd_dependency_paths("libedit.so.0 => not found\n")
.expect_err("expected ldd parse to fail when a dependency is missing");
assert!(
err.to_string()
.contains("Missing shared-library dependency")
);
}
#[test]
fn parse_hook_command_candidates_finds_commands_after_assignments_and_operators() {
let commands = parse_hook_command_candidates(
"PATH=/tmp:$PATH grep -q foo etc/shells || echo foo >> etc/shells\ncat \"$DEPOT_ROOTFS/usr/bin/find\" | sed 's/x/y/'\n",
);
assert_eq!(
commands,
vec!["cat".to_string(), "grep".to_string(), "sed".to_string()]
);
}
#[test]
fn parse_hook_command_candidates_ignores_builtins_and_control_words() {
let commands = parse_hook_command_candidates("if true; then export FOO=bar; echo hi; fi\n");
assert!(commands.is_empty());
}
#[test]
fn deferred_hooks_file_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("hooks.tsv");
let hooks = vec![
DeferredHook {
pkg_name: "foo".into(),
hook: Hook::PostInstall,
script_rel: PathBuf::from("usr/share/depot/foo/scripts/post_install"),
},
DeferredHook {
pkg_name: "bar".into(),
hook: Hook::PostUpdate,
script_rel: PathBuf::from("usr/share/depot/bar/scripts/post_update"),
},
];
write_deferred_hooks(&path, &hooks).unwrap();
let loaded = read_deferred_hooks(&path).unwrap();
assert_eq!(loaded.len(), hooks.len());
assert_eq!(loaded[0].pkg_name, hooks[0].pkg_name);
assert_eq!(loaded[0].hook, hooks[0].hook);
assert_eq!(loaded[0].script_rel, hooks[0].script_rel);
assert_eq!(loaded[1].pkg_name, hooks[1].pkg_name);
assert_eq!(loaded[1].hook, hooks[1].hook);
assert_eq!(loaded[1].script_rel, hooks[1].script_rel);
}
#[test]
fn queue_deferred_hook_dedupes_entries() {
let tmp = tempfile::tempdir().unwrap();
queue_deferred_hook(
tmp.path(),
"foo",
Hook::PostInstall,
Path::new("usr/share/depot/foo/scripts/post_install"),
)
.unwrap();
queue_deferred_hook(
tmp.path(),
"foo",
Hook::PostInstall,
Path::new("usr/share/depot/foo/scripts/post_install"),
)
.unwrap();
let loaded = read_deferred_hooks(&deferred_hooks_file(tmp.path())).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].pkg_name, "foo");
assert_eq!(loaded[0].hook, Hook::PostInstall);
assert_eq!(
loaded[0].script_rel,
PathBuf::from("usr/share/depot/foo/scripts/post_install")
);
}
#[test]
fn sync_staged_scripts_to_rootfs_replaces_existing_tree() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("root");
let staged = tmp.path().join("staged");
std::fs::create_dir_all(staged.join("scripts")).unwrap();
let installed = installed_scripts_dir(&rootfs, "foo");
std::fs::create_dir_all(&installed).unwrap();
std::fs::write(installed.join("old"), "old").unwrap();
std::fs::write(staged.join("scripts/post_install"), "echo ok").unwrap();
let has_scripts =
sync_staged_scripts_to_rootfs(&staged.join("scripts"), &rootfs, "foo").unwrap();
assert!(has_scripts);
let installed = installed_scripts_dir(&rootfs, "foo");
assert!(!installed.join("old").exists());
assert!(installed.join("post_install").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(installed.join("post_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn sync_staged_scripts_to_rootfs_removes_old_when_none_staged() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("root");
let staged = tmp.path().join("staged");
std::fs::create_dir_all(&staged).unwrap();
let installed = installed_scripts_dir(&rootfs, "foo");
std::fs::create_dir_all(&installed).unwrap();
std::fs::write(installed.join("pre_remove"), "echo old").unwrap();
let has_scripts =
sync_staged_scripts_to_rootfs(&staged.join("scripts"), &rootfs, "foo").unwrap();
assert!(!has_scripts);
assert!(!installed_scripts_dir(&rootfs, "foo").exists());
}
#[test]
fn stage_scripts_from_spec_dir_stages_legacy_root_hook() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(spec_dir.join("postinstall.sh"), "echo post").unwrap();
let spec = mk_spec(&spec_dir);
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(staged);
assert!(destdir.join("scripts/post_install").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(destdir.join("scripts/post_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn stage_scripts_from_spec_dir_stages_lib32_prefixed_legacy_root_hook() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(spec_dir.join("lib32-postinstall.sh"), "echo lib32-post").unwrap();
let mut spec = mk_spec(&spec_dir);
spec.package.name = "lib32-foo".into();
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(staged);
assert!(destdir.join("scripts/post_install").exists());
#[cfg(unix)]
{
let mode = std::fs::metadata(destdir.join("scripts/post_install"))
.unwrap()
.permissions()
.mode();
assert_ne!(mode & 0o111, 0);
}
}
#[test]
fn stage_scripts_from_spec_dir_lib32_ignores_generic_legacy_root_hook() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
// No lib32-prefixed hook; native-only scripts must NOT be staged for lib32 packages.
std::fs::write(spec_dir.join("postinstall.sh"), "echo fallback").unwrap();
let mut spec = mk_spec(&spec_dir);
spec.package.name = "lib32-foo".into();
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
assert!(!staged);
assert!(!destdir.join("scripts/post_install").exists());
}
#[test]
fn run_hook_if_present_lib32_ignores_generic_script() {
let tmp = tempfile::tempdir().unwrap();
let scripts = tmp.path().join("scripts");
let rootfs = tmp.path().join("root");
std::fs::create_dir_all(&scripts).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
// Only a generic script exists; lib32 package must NOT execute it.
std::fs::write(
scripts.join("post_install"),
"echo generic > \"$DEPOT_ROOTFS/hook.out\"\n",
)
.unwrap();
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "lib32-foo").unwrap();
assert!(!ran);
assert!(!rootfs.join("hook.out").exists());
}
+1 -600
View File
@@ -1743,603 +1743,4 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{
BuildFlags, BuildType, Dependencies, ManualSource, PackageInfo, PackageSpec, Source,
};
#[test]
fn spec_to_minimal_toml_omits_defaults() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("name = \"foo\""));
assert!(toml.contains("version = \"1.0\""));
assert!(toml.contains("description = \"A test\""));
// defaults should not be present
assert!(!toml.contains("cflags"));
assert!(!toml.contains("rustflags"));
// sha256="skip" should be omitted
assert!(!toml.contains("sha256"));
}
#[test]
fn spec_to_minimal_toml_includes_additional_packages() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: vec![PackageInfo {
name: "foo-dev".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "dev files".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
}],
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("[[packages]]"));
assert!(toml.contains("name = \"foo-dev\""));
}
#[test]
fn spec_to_minimal_toml_includes_multiple_licenses_as_array() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into(), "Apache-2.0".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("license = ["));
assert!(toml.contains("\"MIT\""));
assert!(toml.contains("\"Apache-2.0\""));
}
#[test]
fn spec_to_minimal_toml_includes_extended_build_flags() {
let flags = BuildFlags {
source_subdir: "project/subdir".into(),
configure: vec!["--disable-static".into(), "--enable-foo".into()],
configure_file: "build-aux/configure".into(),
post_configure: vec!["./configure-helper.sh".into()],
post_compile: vec!["make check".into()],
post_install: vec!["strip $DESTDIR/usr/bin/foo".into()],
makefile_commands: vec!["make".into()],
makefile_install_commands: vec!["make DESTDIR=$DESTDIR install".into()],
cargs: vec!["--locked".into()],
config_settings: vec!["editable_mode=compat".into()],
rustflags: vec!["-Ctarget-cpu=native".into()],
cxxflags: vec!["-O2".into(), "-fno-rtti".into()],
fuse_ld: "lld".into(),
ltoflags: vec!["-flto=auto".into()],
target: "x86_64-unknown-linux-gnu".into(),
keep: vec!["etc/locale.gen".into()],
sbindir: "/usr/sbin".into(),
libdir: "/usr/lib64".into(),
libexecdir: "/usr/libexec".into(),
sysconfdir: "/etc/custom".into(),
localstatedir: "/var/custom".into(),
sharedstatedir: "/var/lib/custom".into(),
includedir: "/usr/include/custom".into(),
datarootdir: "/usr/share/root".into(),
datadir: "/usr/share/custom".into(),
mandir: "/usr/share/custom/man".into(),
infodir: "/usr/share/custom/info".into(),
use_lto: false,
no_flags: true,
no_strip: true,
no_delete_static: true,
no_compress_man: true,
skip_tests: true,
makeflags: "-j12 --output-sync=target".into(),
make_vars: vec!["V=1".into()],
make_dirs: vec!["lib".into(), "libelf".into()],
make_test_vars: vec!["TESTS=unit".into()],
make_test_dirs: vec!["tests".into()],
make_install_vars: vec!["STRIPPROG=true".into()],
make_install_dirs: vec!["lib".into(), "apps".into()],
env_vars: vec![
"SETUPTOOLS_SCM_PRETEND_VERSION=$version".into(),
"PYO3_CONFIG_FILE=$specdir/pyo3.toml".into(),
],
..BuildFlags::default()
};
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: vec!["fix.patch".into()],
post_extract: vec!["autoreconf -fi".into()],
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("source_subdir = \"project/subdir\""));
assert!(toml.contains("configure = ["));
assert!(toml.contains("configure_file = \"build-aux/configure\""));
assert!(toml.contains("post_configure = ["));
assert!(toml.contains("post_compile = ["));
assert!(toml.contains("post_install = ["));
assert!(toml.contains("makefile_commands = ["));
assert!(toml.contains("makefile_install_commands = ["));
assert!(toml.contains("cargs = ["));
assert!(toml.contains("config_setting = ["));
assert!(toml.contains("rustflags = ["));
assert!(toml.contains("cxxflags = ["));
assert!(toml.contains("fuse_ld = \"lld\""));
assert!(toml.contains("ltoflags = ["));
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
assert!(toml.contains("keep = ["));
assert!(toml.contains("\"etc/locale.gen\""));
assert!(toml.contains("sbindir = \"/usr/sbin\""));
assert!(toml.contains("libdir = \"/usr/lib64\""));
assert!(toml.contains("libexecdir = \"/usr/libexec\""));
assert!(toml.contains("sysconfdir = \"/etc/custom\""));
assert!(toml.contains("localstatedir = \"/var/custom\""));
assert!(toml.contains("sharedstatedir = \"/var/lib/custom\""));
assert!(toml.contains("includedir = \"/usr/include/custom\""));
assert!(toml.contains("datarootdir = \"/usr/share/root\""));
assert!(toml.contains("datadir = \"/usr/share/custom\""));
assert!(toml.contains("mandir = \"/usr/share/custom/man\""));
assert!(toml.contains("infodir = \"/usr/share/custom/info\""));
assert!(toml.contains("use_lto = false"));
assert!(toml.contains("no_flags = true"));
assert!(toml.contains("no_strip = true"));
assert!(toml.contains("no_delete_static = true"));
assert!(toml.contains("no_compress_man = true"));
assert!(toml.contains("skip_tests = true"));
assert!(toml.contains("makeflags = \"-j12 --output-sync=target\""));
assert!(toml.contains("make_vars = ["));
assert!(toml.contains("make_dirs = ["));
assert!(toml.contains("make_test_vars = ["));
assert!(toml.contains("make_test_dirs = ["));
assert!(toml.contains("make_install_vars = ["));
assert!(toml.contains("make_install_dirs = ["));
assert!(toml.contains("env_vars = ["));
assert!(toml.contains("patches = ["));
assert!(toml.contains("post_extract = ["));
}
#[test]
fn spec_to_minimal_toml_includes_extract_dir_for_variable_default() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "$name-$version".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("extract_dir = \"$name-$version\""));
}
#[test]
fn spec_to_minimal_toml_includes_test_and_optional_dependencies() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: vec![],
runtime: vec![],
test: vec!["python".into(), "bats".into()],
optional: vec!["gtk-doc".into()],
groups: vec!["base".into(), "devtools".into()],
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
let val: toml::Value = toml::from_str(&toml).unwrap();
let test_deps = val
.get("dependencies")
.and_then(|d| d.get("test"))
.and_then(|t| t.as_array())
.expect("expected dependencies.test array");
assert_eq!(test_deps.len(), 2);
assert_eq!(test_deps[0].as_str(), Some("python"));
assert_eq!(test_deps[1].as_str(), Some("bats"));
let optional_deps = val
.get("dependencies")
.and_then(|d| d.get("optional"))
.and_then(|t| t.as_array())
.expect("expected dependencies.optional array");
assert_eq!(optional_deps.len(), 1);
assert_eq!(optional_deps[0].as_str(), Some("gtk-doc"));
let groups = val
.get("dependencies")
.and_then(|d| d.get("groups"))
.and_then(|t| t.as_array())
.expect("expected dependencies.groups array");
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].as_str(), Some("base"));
assert_eq!(groups[1].as_str(), Some("devtools"));
}
#[test]
fn spec_to_minimal_toml_includes_alternatives_conflicts_and_provides() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives {
provides: vec!["editor".into(), "sh".into()],
conflicts: vec!["nano".into(), "busybox-sh".into()],
replaces: vec!["vi".into()],
lib32: Some(crate::package::AlternativeGroup {
provides: Vec::new(),
conflicts: Vec::new(),
replaces: vec!["lib32-vi".into()],
}),
},
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
let val: toml::Value = toml::from_str(&toml).unwrap();
let alternatives = val
.get("alternatives")
.and_then(|v| v.as_table())
.expect("expected alternatives table");
let provides = alternatives
.get("provides")
.and_then(|v| v.as_array())
.expect("expected alternatives.provides array");
let conflicts = alternatives
.get("conflicts")
.and_then(|v| v.as_array())
.expect("expected alternatives.conflicts array");
let replaces = alternatives
.get("replaces")
.and_then(|v| v.as_array())
.expect("expected alternatives.replaces array");
let lib32 = alternatives
.get("lib32")
.and_then(|v| v.as_table())
.expect("expected alternatives.lib32 table");
let lib32_replaces = lib32
.get("replaces")
.and_then(|v| v.as_array())
.expect("expected alternatives.lib32.replaces array");
assert_eq!(provides.len(), 2);
assert_eq!(provides[0].as_str(), Some("editor"));
assert_eq!(provides[1].as_str(), Some("sh"));
assert_eq!(conflicts.len(), 2);
assert_eq!(conflicts[0].as_str(), Some("nano"));
assert_eq!(conflicts[1].as_str(), Some("busybox-sh"));
assert_eq!(replaces.len(), 1);
assert_eq!(replaces[0].as_str(), Some("vi"));
assert_eq!(lib32_replaces.len(), 1);
assert_eq!(lib32_replaces[0].as_str(), Some("lib32-vi"));
}
#[test]
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo-meta".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "Meta package".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Meta,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: Vec::new(),
runtime: vec!["foo".into(), "bar".into()],
test: Vec::new(),
optional: Vec::new(),
groups: vec!["base".into()],
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("type = \"meta\""));
assert!(!toml.contains("[[source]]"));
let val: toml::Value = toml::from_str(&toml).unwrap();
assert!(val.get("source").is_none());
}
#[test]
fn spec_to_minimal_toml_includes_manual_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "vertex-keyring".into(),
real_name: None,
version: "1.0.0".into(),
revision: 1,
description: "keyring".into(),
homepage: "https://www.vertexlinux.net".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: vec![
ManualSource {
file: Some("vertex.pub".into()),
files: Vec::new(),
url: None,
urls: Vec::new(),
sha256: None,
dest: Some("usr/share/depot/keys/public/vertex.pub".into()),
},
ManualSource {
file: None,
files: Vec::new(),
url: Some("file:///tmp/vertex.minisig".into()),
urls: Vec::new(),
sha256: Some("skip".into()),
dest: Some("usr/share/depot/keys/sign/vertex.minisig".into()),
},
],
source: Vec::new(),
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("[[manual_sources]]"));
assert!(toml.contains("file = \"vertex.pub\""));
assert!(toml.contains("url = \"file:///tmp/vertex.minisig\""));
assert!(toml.contains("dest = \"usr/share/depot/keys/public/vertex.pub\""));
assert!(!toml.contains("sha256 = \"skip\""));
let val: toml::Value = toml::from_str(&toml).unwrap();
let arr = val
.get("manual_sources")
.and_then(|v| v.as_array())
.expect("expected manual_sources array");
assert_eq!(arr.len(), 2);
}
#[test]
fn compute_sha256_for_local_path_and_file_url() {
use sha2::Digest as TestDigest;
use sha2::Sha256 as TestSha256;
use tempfile::NamedTempFile;
let mut tmp = NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut tmp, b"abc").unwrap();
let expected = {
let mut h = TestSha256::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
// plain path
let p = tmp.path().to_str().unwrap().to_string();
assert_eq!(compute_sha256_for_url(&p).unwrap(), expected);
// file:// URL
let file_url = format!("file://{}", tmp.path().to_str().unwrap());
assert_eq!(compute_sha256_for_url(&file_url).unwrap(), expected);
}
#[test]
fn expand_known_package_vars_replaces_name_and_version_patterns() {
let input = "https://example.com/$name/${name}-$version-${version}.tar.xz";
let out = expand_known_package_vars(input, "python", "3.13.1");
assert_eq!(
out,
"https://example.com/python/python-3.13.1-3.13.1.tar.xz"
);
}
}
mod tests;
+598
View File
@@ -0,0 +1,598 @@
use super::*;
use crate::package::{
BuildFlags, BuildType, Dependencies, ManualSource, PackageInfo, PackageSpec, Source,
};
#[test]
fn spec_to_minimal_toml_omits_defaults() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("name = \"foo\""));
assert!(toml.contains("version = \"1.0\""));
assert!(toml.contains("description = \"A test\""));
// defaults should not be present
assert!(!toml.contains("cflags"));
assert!(!toml.contains("rustflags"));
// sha256="skip" should be omitted
assert!(!toml.contains("sha256"));
}
#[test]
fn spec_to_minimal_toml_includes_additional_packages() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: vec![PackageInfo {
name: "foo-dev".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "dev files".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
}],
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("[[packages]]"));
assert!(toml.contains("name = \"foo-dev\""));
}
#[test]
fn spec_to_minimal_toml_includes_multiple_licenses_as_array() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into(), "Apache-2.0".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("license = ["));
assert!(toml.contains("\"MIT\""));
assert!(toml.contains("\"Apache-2.0\""));
}
#[test]
fn spec_to_minimal_toml_includes_extended_build_flags() {
let flags = BuildFlags {
source_subdir: "project/subdir".into(),
configure: vec!["--disable-static".into(), "--enable-foo".into()],
configure_file: "build-aux/configure".into(),
post_configure: vec!["./configure-helper.sh".into()],
post_compile: vec!["make check".into()],
post_install: vec!["strip $DESTDIR/usr/bin/foo".into()],
makefile_commands: vec!["make".into()],
makefile_install_commands: vec!["make DESTDIR=$DESTDIR install".into()],
cargs: vec!["--locked".into()],
config_settings: vec!["editable_mode=compat".into()],
rustflags: vec!["-Ctarget-cpu=native".into()],
cxxflags: vec!["-O2".into(), "-fno-rtti".into()],
fuse_ld: "lld".into(),
ltoflags: vec!["-flto=auto".into()],
target: "x86_64-unknown-linux-gnu".into(),
keep: vec!["etc/locale.gen".into()],
sbindir: "/usr/sbin".into(),
libdir: "/usr/lib64".into(),
libexecdir: "/usr/libexec".into(),
sysconfdir: "/etc/custom".into(),
localstatedir: "/var/custom".into(),
sharedstatedir: "/var/lib/custom".into(),
includedir: "/usr/include/custom".into(),
datarootdir: "/usr/share/root".into(),
datadir: "/usr/share/custom".into(),
mandir: "/usr/share/custom/man".into(),
infodir: "/usr/share/custom/info".into(),
use_lto: false,
no_flags: true,
no_strip: true,
no_delete_static: true,
no_compress_man: true,
skip_tests: true,
makeflags: "-j12 --output-sync=target".into(),
make_vars: vec!["V=1".into()],
make_dirs: vec!["lib".into(), "libelf".into()],
make_test_vars: vec!["TESTS=unit".into()],
make_test_dirs: vec!["tests".into()],
make_install_vars: vec!["STRIPPROG=true".into()],
make_install_dirs: vec!["lib".into(), "apps".into()],
env_vars: vec![
"SETUPTOOLS_SCM_PRETEND_VERSION=$version".into(),
"PYO3_CONFIG_FILE=$specdir/pyo3.toml".into(),
],
..BuildFlags::default()
};
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: vec!["fix.patch".into()],
post_extract: vec!["autoreconf -fi".into()],
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("source_subdir = \"project/subdir\""));
assert!(toml.contains("configure = ["));
assert!(toml.contains("configure_file = \"build-aux/configure\""));
assert!(toml.contains("post_configure = ["));
assert!(toml.contains("post_compile = ["));
assert!(toml.contains("post_install = ["));
assert!(toml.contains("makefile_commands = ["));
assert!(toml.contains("makefile_install_commands = ["));
assert!(toml.contains("cargs = ["));
assert!(toml.contains("config_setting = ["));
assert!(toml.contains("rustflags = ["));
assert!(toml.contains("cxxflags = ["));
assert!(toml.contains("fuse_ld = \"lld\""));
assert!(toml.contains("ltoflags = ["));
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
assert!(toml.contains("keep = ["));
assert!(toml.contains("\"etc/locale.gen\""));
assert!(toml.contains("sbindir = \"/usr/sbin\""));
assert!(toml.contains("libdir = \"/usr/lib64\""));
assert!(toml.contains("libexecdir = \"/usr/libexec\""));
assert!(toml.contains("sysconfdir = \"/etc/custom\""));
assert!(toml.contains("localstatedir = \"/var/custom\""));
assert!(toml.contains("sharedstatedir = \"/var/lib/custom\""));
assert!(toml.contains("includedir = \"/usr/include/custom\""));
assert!(toml.contains("datarootdir = \"/usr/share/root\""));
assert!(toml.contains("datadir = \"/usr/share/custom\""));
assert!(toml.contains("mandir = \"/usr/share/custom/man\""));
assert!(toml.contains("infodir = \"/usr/share/custom/info\""));
assert!(toml.contains("use_lto = false"));
assert!(toml.contains("no_flags = true"));
assert!(toml.contains("no_strip = true"));
assert!(toml.contains("no_delete_static = true"));
assert!(toml.contains("no_compress_man = true"));
assert!(toml.contains("skip_tests = true"));
assert!(toml.contains("makeflags = \"-j12 --output-sync=target\""));
assert!(toml.contains("make_vars = ["));
assert!(toml.contains("make_dirs = ["));
assert!(toml.contains("make_test_vars = ["));
assert!(toml.contains("make_test_dirs = ["));
assert!(toml.contains("make_install_vars = ["));
assert!(toml.contains("make_install_dirs = ["));
assert!(toml.contains("env_vars = ["));
assert!(toml.contains("patches = ["));
assert!(toml.contains("post_extract = ["));
}
#[test]
fn spec_to_minimal_toml_includes_extract_dir_for_variable_default() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "$name-$version".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("extract_dir = \"$name-$version\""));
}
#[test]
fn spec_to_minimal_toml_includes_test_and_optional_dependencies() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: vec![],
runtime: vec![],
test: vec!["python".into(), "bats".into()],
optional: vec!["gtk-doc".into()],
groups: vec!["base".into(), "devtools".into()],
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
let val: toml::Value = toml::from_str(&toml).unwrap();
let test_deps = val
.get("dependencies")
.and_then(|d| d.get("test"))
.and_then(|t| t.as_array())
.expect("expected dependencies.test array");
assert_eq!(test_deps.len(), 2);
assert_eq!(test_deps[0].as_str(), Some("python"));
assert_eq!(test_deps[1].as_str(), Some("bats"));
let optional_deps = val
.get("dependencies")
.and_then(|d| d.get("optional"))
.and_then(|t| t.as_array())
.expect("expected dependencies.optional array");
assert_eq!(optional_deps.len(), 1);
assert_eq!(optional_deps[0].as_str(), Some("gtk-doc"));
let groups = val
.get("dependencies")
.and_then(|d| d.get("groups"))
.and_then(|t| t.as_array())
.expect("expected dependencies.groups array");
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].as_str(), Some("base"));
assert_eq!(groups[1].as_str(), Some("devtools"));
}
#[test]
fn spec_to_minimal_toml_includes_alternatives_conflicts_and_provides() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives {
provides: vec!["editor".into(), "sh".into()],
conflicts: vec!["nano".into(), "busybox-sh".into()],
replaces: vec!["vi".into()],
lib32: Some(crate::package::AlternativeGroup {
provides: Vec::new(),
conflicts: Vec::new(),
replaces: vec!["lib32-vi".into()],
}),
},
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.com/foo-1.0.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo-1.0".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
let val: toml::Value = toml::from_str(&toml).unwrap();
let alternatives = val
.get("alternatives")
.and_then(|v| v.as_table())
.expect("expected alternatives table");
let provides = alternatives
.get("provides")
.and_then(|v| v.as_array())
.expect("expected alternatives.provides array");
let conflicts = alternatives
.get("conflicts")
.and_then(|v| v.as_array())
.expect("expected alternatives.conflicts array");
let replaces = alternatives
.get("replaces")
.and_then(|v| v.as_array())
.expect("expected alternatives.replaces array");
let lib32 = alternatives
.get("lib32")
.and_then(|v| v.as_table())
.expect("expected alternatives.lib32 table");
let lib32_replaces = lib32
.get("replaces")
.and_then(|v| v.as_array())
.expect("expected alternatives.lib32.replaces array");
assert_eq!(provides.len(), 2);
assert_eq!(provides[0].as_str(), Some("editor"));
assert_eq!(provides[1].as_str(), Some("sh"));
assert_eq!(conflicts.len(), 2);
assert_eq!(conflicts[0].as_str(), Some("nano"));
assert_eq!(conflicts[1].as_str(), Some("busybox-sh"));
assert_eq!(replaces.len(), 1);
assert_eq!(replaces[0].as_str(), Some("vi"));
assert_eq!(lib32_replaces.len(), 1);
assert_eq!(lib32_replaces[0].as_str(), Some("lib32-vi"));
}
#[test]
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo-meta".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "Meta package".into(),
homepage: "".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Meta,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: Vec::new(),
runtime: vec!["foo".into(), "bar".into()],
test: Vec::new(),
optional: Vec::new(),
groups: vec!["base".into()],
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("type = \"meta\""));
assert!(!toml.contains("[[source]]"));
let val: toml::Value = toml::from_str(&toml).unwrap();
assert!(val.get("source").is_none());
}
#[test]
fn spec_to_minimal_toml_includes_manual_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "vertex-keyring".into(),
real_name: None,
version: "1.0.0".into(),
revision: 1,
description: "keyring".into(),
homepage: "https://www.vertexlinux.net".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: vec![
ManualSource {
file: Some("vertex.pub".into()),
files: Vec::new(),
url: None,
urls: Vec::new(),
sha256: None,
dest: Some("usr/share/depot/keys/public/vertex.pub".into()),
},
ManualSource {
file: None,
files: Vec::new(),
url: Some("file:///tmp/vertex.minisig".into()),
urls: Vec::new(),
sha256: Some("skip".into()),
dest: Some("usr/share/depot/keys/sign/vertex.minisig".into()),
},
],
source: Vec::new(),
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("[[manual_sources]]"));
assert!(toml.contains("file = \"vertex.pub\""));
assert!(toml.contains("url = \"file:///tmp/vertex.minisig\""));
assert!(toml.contains("dest = \"usr/share/depot/keys/public/vertex.pub\""));
assert!(!toml.contains("sha256 = \"skip\""));
let val: toml::Value = toml::from_str(&toml).unwrap();
let arr = val
.get("manual_sources")
.and_then(|v| v.as_array())
.expect("expected manual_sources array");
assert_eq!(arr.len(), 2);
}
#[test]
fn compute_sha256_for_local_path_and_file_url() {
use sha2::Digest as TestDigest;
use sha2::Sha256 as TestSha256;
use tempfile::NamedTempFile;
let mut tmp = NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut tmp, b"abc").unwrap();
let expected = {
let mut h = TestSha256::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
// plain path
let p = tmp.path().to_str().unwrap().to_string();
assert_eq!(compute_sha256_for_url(&p).unwrap(), expected);
// file:// URL
let file_url = format!("file://{}", tmp.path().to_str().unwrap());
assert_eq!(compute_sha256_for_url(&file_url).unwrap(), expected);
}
#[test]
fn expand_known_package_vars_replaces_name_and_version_patterns() {
let input = "https://example.com/$name/${name}-$version-${version}.tar.xz";
let out = expand_known_package_vars(input, "python", "3.13.1");
assert_eq!(
out,
"https://example.com/python/python-3.13.1-3.13.1.tar.xz"
);
}
+63 -10
View File
@@ -8,7 +8,7 @@ use std::collections::HashMap;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use tar::{Builder, EntryType};
use tar::{Builder, EntryType, Header, HeaderMode};
use zstd::stream::write::Encoder;
type HardlinkKey = (u64, u64);
@@ -42,6 +42,20 @@ fn license_value(licenses: &[String]) -> toml::Value {
}
}
fn package_header(
path: &Path,
metadata: &fs::Metadata,
symlink: bool,
mode: HeaderMode,
) -> Result<Header> {
let mut header = Header::new_gnu();
header.set_metadata_in_mode(metadata, mode);
let (uid, gid) = crate::fakeroot::archive_ownership(path, metadata, symlink)?;
header.set_uid(uid);
header.set_gid(gid);
Ok(header)
}
pub struct Packager {
pub spec: PackageSpec,
pub destdir: PathBuf,
@@ -109,15 +123,16 @@ impl Packager {
let file_type = entry.file_type();
if file_type.is_dir() {
tar.append_dir(rel_path, path)?;
let metadata = fs::symlink_metadata(path)?;
let mut header = package_header(path, &metadata, false, HeaderMode::Complete)?;
header.set_size(0);
tar.append_data(&mut header, rel_path, std::io::empty())?;
} else if file_type.is_symlink() {
// For symlinks, we need to read the link and append to tar correctly
let target = fs::read_link(path)?;
let mut header = tar::Header::new_gnu();
header.set_metadata_in_mode(
&fs::symlink_metadata(path)?,
tar::HeaderMode::Deterministic,
);
let metadata = fs::symlink_metadata(path)?;
let mut header = package_header(path, &metadata, true, HeaderMode::Deterministic)?;
header.set_size(0);
tar.append_link(&mut header, rel_path, target)?;
} else {
// Files
@@ -126,14 +141,15 @@ impl Packager {
if let Some(first_rel_path) =
hardlink_key.and_then(|key| archived_hardlinks.get(&key))
{
let mut header = tar::Header::new_gnu();
header.set_metadata_in_mode(&metadata, tar::HeaderMode::Deterministic);
let mut header =
package_header(path, &metadata, false, HeaderMode::Deterministic)?;
header.set_entry_type(EntryType::Link);
header.set_size(0);
tar.append_link(&mut header, rel_path, first_rel_path)?;
} else {
let mut file = fs::File::open(path)?;
tar.append_file(rel_path, &mut file)?;
let mut header = package_header(path, &metadata, false, HeaderMode::Complete)?;
tar.append_data(&mut header, rel_path, &mut file)?;
if let Some(key) = hardlink_key {
archived_hardlinks.insert(key, rel_path.to_path_buf());
}
@@ -683,4 +699,41 @@ mod tests {
assert_eq!(coreutils_meta.nlink(), 2);
assert_eq!(ls_meta.nlink(), 2);
}
#[test]
fn test_create_package_preserves_rootless_fakeroot_ownership() -> Result<()> {
let tmp = tempfile::tempdir()?;
let dest = tmp.path().join("dest");
let out = tmp.path().join("out");
fs::create_dir_all(dest.join("usr/lib"))?;
fs::create_dir_all(&out)?;
let mut command = crate::fakeroot::wrap_install_command("/bin/sh", &dest)?;
command.arg("-c").arg(
"/usr/bin/touch \"$DESTDIR/usr/lib/dbus-daemon-launch-helper\"; \
/usr/bin/chown 0:81 \"$DESTDIR/usr/lib/dbus-daemon-launch-helper\"; \
/usr/bin/chmod 4750 \"$DESTDIR/usr/lib/dbus-daemon-launch-helper\"",
);
let status = command.status()?;
assert!(status.success());
let packager = mk_packager(dest);
let archive_path = packager.create_package(&out, "x86_64")?;
let archive_file = fs::File::open(archive_path)?;
let decoder = zstd::Decoder::new(archive_file)?;
let mut archive = tar::Archive::new(decoder);
let mut found = false;
for entry in archive.entries()? {
let entry = entry?;
if entry.path()?.as_ref() == Path::new("usr/lib/dbus-daemon-launch-helper") {
assert_eq!(entry.header().uid()?, 0);
assert_eq!(entry.header().gid()?, 81);
assert_eq!(entry.header().mode()? & 0o7777, 0o4750);
found = true;
break;
}
}
assert!(found);
Ok(())
}
}
+5 -2474
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+657
View File
@@ -0,0 +1,657 @@
use super::*;
#[test]
fn parse_package_built_against_metadata() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
built-against = ["icu78"]
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.package.built_against, vec!["icu78".to_string()]);
}
#[test]
fn parse_package_dependencies_overrides() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/llvm.tar.gz"
sha256 = "skip"
extract_dir = "llvm"
[build]
type = "custom"
[dependencies]
runtime = ["base"]
groups = ["toolchain"]
[package_dependencies.clang]
runtime = ["llvm-libs", "llvm-libgcc"]
groups = ["compiler"]
[package_dependencies.llvm-libs]
runtime = ["llvm-libgcc", "zstd"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.dependencies_for_output("llvm").runtime,
vec!["base".to_string()]
);
assert_eq!(
spec.dependencies_for_output("llvm").groups,
vec!["toolchain".to_string()]
);
assert_eq!(
spec.dependencies_for_output("clang").runtime,
vec!["llvm-libs".to_string(), "llvm-libgcc".to_string()]
);
assert_eq!(
spec.dependencies_for_output("clang").groups,
vec!["compiler".to_string()]
);
assert_eq!(
spec.dependencies_for_output("llvm-libs").runtime,
vec!["llvm-libgcc".to_string(), "zstd".to_string()]
);
assert_eq!(
spec.dependencies_for_output("llvm-libs").groups,
Vec::<String>::new()
);
}
#[test]
fn parse_lib32_dependencies_override() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/llvm.tar.gz"
sha256 = "skip"
extract_dir = "llvm"
[build]
type = "custom"
[dependencies]
runtime = ["base"]
groups = ["toolchain"]
[dependencies.lib32]
build = ["gcc-multilib"]
runtime = ["lib32-zlib"]
test = ["bats"]
groups = ["lib32-toolchain"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.dependencies_for_output("llvm").runtime,
vec!["base".to_string()]
);
assert_eq!(
spec.dependencies_for_output("llvm").groups,
vec!["toolchain".to_string()]
);
assert_eq!(
spec.dependencies_for_output("lib32-llvm").build,
vec!["gcc-multilib".to_string()]
);
assert_eq!(
spec.dependencies_for_output("lib32-llvm").runtime,
vec!["lib32-zlib".to_string(), "llvm".to_string()]
);
assert_eq!(
spec.dependencies_for_output("lib32-llvm").test,
vec!["bats".to_string()]
);
assert_eq!(
spec.dependencies_for_output("lib32-llvm").groups,
vec!["lib32-toolchain".to_string()]
);
}
#[test]
fn parse_package_alternatives_overrides() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/llvm.tar.gz"
sha256 = "skip"
extract_dir = "llvm"
[build]
type = "custom"
[alternatives]
provides = ["toolchain"]
conflicts = ["gcc"]
[package_alternatives.clang]
provides = ["cc", "c++", "gcc"]
conflicts = ["clang-legacy"]
[package_alternatives.llvm]
provides = ["binutils"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.alternatives_for_output("llvm").provides,
vec!["binutils".to_string()]
);
assert_eq!(
spec.alternatives_for_output("llvm").conflicts,
Vec::<String>::new()
);
assert_eq!(
spec.alternatives_for_output("clang").provides,
vec!["cc".to_string(), "c++".to_string(), "gcc".to_string()]
);
assert_eq!(
spec.alternatives_for_output("clang").conflicts,
vec!["clang-legacy".to_string()]
);
assert_eq!(
spec.alternatives_for_output("other").provides,
vec!["toolchain".to_string()]
);
assert_eq!(
spec.alternatives_for_output("other").conflicts,
vec!["gcc".to_string()]
);
}
#[test]
fn parse_lib32_alternatives_override() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/llvm.tar.gz"
sha256 = "skip"
extract_dir = "llvm"
[build]
type = "custom"
[alternatives]
provides = ["toolchain"]
replaces = ["clang"]
[alternatives.lib32]
provides = ["lib32-toolchain"]
conflicts = ["lib32-gcc"]
replaces = ["lib32-clang"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.alternatives_for_output("llvm").replaces,
vec!["clang".to_string()]
);
assert_eq!(
spec.alternatives_for_output("lib32-llvm").provides,
vec!["lib32-toolchain".to_string()]
);
assert_eq!(
spec.alternatives_for_output("lib32-llvm").conflicts,
vec!["lib32-gcc".to_string()]
);
assert_eq!(
spec.alternatives_for_output("lib32-llvm").replaces,
vec!["lib32-clang".to_string()]
);
}
#[test]
fn lib32_output_does_not_fallback_to_primary_alternatives() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
build_32 = true
[alternatives]
provides = ["toolchain"]
conflicts = ["gcc"]
replaces = ["clang"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
let lib32 = spec.alternatives_for_output("lib32-llvm");
assert!(lib32.provides.is_empty());
assert!(lib32.conflicts.is_empty());
assert!(lib32.replaces.is_empty());
}
#[test]
fn parse_python_build_type() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "python"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(matches!(spec.build.build_type, BuildType::Python));
}
#[test]
fn parse_perl_build_type() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "perl"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(matches!(spec.build.build_type, BuildType::Perl));
}
#[test]
fn parse_multiple_licenses() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = ["MIT", "Apache-2.0"]
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.package.license,
vec!["MIT".to_string(), "Apache-2.0".to_string()]
);
}
#[test]
fn parse_keep_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
keep = ["etc/locale.gen", "etc/resolv.conf"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.keep,
vec!["etc/locale.gen".to_string(), "etc/resolv.conf".to_string()]
);
}
#[test]
fn parse_split_docs_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
split_docs = true
doc_dirs = ["/opt/docs", "usr/share/devhelp"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.split_docs);
assert_eq!(
spec.build.flags.doc_dirs,
vec!["/opt/docs".to_string(), "usr/share/devhelp".to_string()]
);
}
#[test]
fn test_package_filename() {
let mut spec = mk_spec("foo", "1.0");
spec.package.revision = 2;
assert_eq!(
spec.package_filename("x86_64"),
"foo-1.0-2-x86_64.depot.pkg.tar.zst"
);
}
#[test]
fn parse_packages_array() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[packages]]
name = "foo-dev"
version = "1.0"
description = "development files"
homepage = "h"
license = "MIT"
[[source]]
url = "https://example.com/foo-1.0.tar.gz"
sha256 = "skip"
extract_dir = "foo-1.0"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
let outputs = spec.outputs();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].name, "foo");
assert_eq!(outputs[1].name, "foo-dev");
}
#[test]
fn docs_output_uses_runtime_dependency_on_parent_package() {
let mut spec = mk_spec("foo", "1.0");
spec.build.flags.split_docs = true;
let docs_name = PackageSpec::docs_package_name("foo");
let deps = spec.dependencies_for_output(&docs_name);
assert_eq!(deps.runtime, vec!["foo".to_string()]);
let alternatives = spec.alternatives_for_output(&docs_name);
assert!(alternatives.provides.is_empty());
assert!(alternatives.conflicts.is_empty());
}
#[test]
fn docs_package_for_output_derives_name_and_description() {
let mut spec = mk_spec("foo", "1.0");
spec.build.flags.split_docs = true;
let docs = spec.docs_package_for_output(&spec.package);
assert_eq!(docs.name, "foo-docs");
assert_eq!(docs.description, "Documentation for foo");
assert_eq!(docs.version, "1.0");
}
#[test]
fn parse_dkms_build_type_and_modules() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("zfs.toml");
std::fs::write(
&path,
r#"
[package]
name = "zfs-dkms"
version = "2.4.3"
description = "d"
homepage = "h"
license = "CDDL"
[source]
url = "https://github.com/openzfs/zfs/releases/download/zfs-$version/zfs-$version.tar.gz"
sha256 = "skip"
extract_dir = "zfs-$version"
[build]
type = "dkms"
[build.flags]
dkms_name = "zfs"
dkms_version = "$version"
dkms_source_dir = "."
dkms_install_dir = "updates/depot"
dkms_make_args = ["V=1"]
dkms_pre_build = [
"./configure --with-config=kernel --with-linux=$kernel_build_dir --with-linux-obj=$kernel_build_dir"
]
[[build.flags.dkms_modules]]
name = "zfs"
path = "module"
built_location = "module/zfs"
[[build.flags.dkms_modules]]
name = "spl"
dest_name = "spl_compat"
build_dir = "module"
built_location = "module/spl"
install_dir = "/updates/storage"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(matches!(spec.build.build_type, BuildType::Dkms));
assert_eq!(spec.effective_dkms_name(), "zfs");
assert_eq!(spec.effective_dkms_version(), "2.4.3");
assert_eq!(spec.effective_dkms_install_dir(), "updates/depot");
assert_eq!(spec.build.flags.dkms_pre_build.len(), 1);
assert_eq!(spec.build.flags.dkms_modules.len(), 2);
assert_eq!(spec.build.flags.dkms_modules[0].build_dir, "module");
assert_eq!(
spec.effective_dkms_module_install_dir(&spec.build.flags.dkms_modules[1]),
"updates/storage"
);
}
#[test]
fn parse_dkms_rejects_unsafe_module_paths() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("bad.toml");
std::fs::write(
&path,
r#"
[package]
name = "bad-dkms"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/bad.tar.gz"
sha256 = "skip"
extract_dir = "bad"
[build]
type = "dkms"
[[build.flags.dkms_modules]]
name = "bad"
path = "../outside"
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).unwrap_err().to_string();
assert!(err.contains("unsafe path component"), "{err}");
}
+328
View File
@@ -0,0 +1,328 @@
use super::*;
#[test]
fn parse_single_source_table() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo-$version.tar.gz"
sha256 = "skip"
extract_dir = "foo-$version"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.package.name, "foo");
assert_eq!(spec.sources().len(), 1);
assert_eq!(
spec.expand_vars(&spec.sources()[0].url),
"https://example.com/foo-1.0.tar.gz"
);
assert!(spec.sources()[0].patches.is_empty());
assert!(spec.sources()[0].post_extract.is_empty());
assert!(spec.sources()[0].cherry_pick.is_empty());
assert_eq!(spec.spec_dir, tmp.path());
}
#[test]
fn parse_source_array() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[source]]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[[source]]
url = "https://example.com/bar.tar.gz"
sha256 = "skip"
extract_dir = "bar"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.sources().len(), 2);
assert_eq!(spec.sources()[0].extract_dir, "foo");
assert_eq!(spec.sources()[1].extract_dir, "bar");
}
#[test]
fn parse_source_without_sha256_defaults_to_skip() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
extract_dir = "foo"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.sources()[0].sha256, "skip");
}
#[test]
fn parse_git_source_with_cherry_pick() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.git#main"
sha256 = "skip"
extract_dir = "foo"
cherry_pick = ["deadbeef", "cafebabe"]
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.sources()[0].cherry_pick,
vec!["deadbeef".to_string(), "cafebabe".to_string()]
);
}
#[test]
fn parse_rejects_empty_sources() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
// `source = []` is not accepted (must have at least one entry)
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
source = []
[build]
type = "custom"
"#,
)
.unwrap();
assert!(PackageSpec::from_file(&path).is_err());
}
#[test]
fn parse_allows_metapackage_without_sources() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo-meta"
version = "1.0"
description = "metapackage"
homepage = "https://example.com"
license = "MIT"
[build]
type = "meta"
[dependencies]
runtime = ["foo", "bar"]
groups = ["base"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.source.is_empty());
assert!(spec.manual_sources.is_empty());
assert!(spec.is_metapackage());
assert_eq!(spec.dependencies.runtime, vec!["foo", "bar"]);
assert_eq!(spec.dependencies.groups, vec!["base"]);
}
#[test]
fn parse_manual_source_with_url() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
url = "https://example.com/manual.patch"
sha256 = "skip"
dest = "patches/manual.patch"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.manual_sources.len(), 1);
assert_eq!(
spec.manual_sources[0].url.as_deref(),
Some("https://example.com/manual.patch")
);
assert_eq!(spec.manual_sources[0].file, None);
}
#[test]
fn parse_manual_source_rejects_missing_file_and_url() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
sha256 = "skip"
[build]
type = "custom"
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).expect_err("spec should be rejected");
assert!(
err.to_string()
.contains("must specify one of 'file', 'files', 'url', or 'urls'")
);
}
#[test]
fn parse_manual_source_rejects_file_and_url_together() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
file = "manual.patch"
url = "https://example.com/manual.patch"
[build]
type = "custom"
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).expect_err("spec should be rejected");
assert!(
err.to_string()
.contains("cannot mix local ('file'/'files') and remote ('url'/'urls') entries")
);
}
#[test]
fn parse_manual_source_with_files_array() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
files = ["other", "system-auth"]
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.manual_sources.len(), 1);
assert_eq!(spec.manual_sources[0].files, vec!["other", "system-auth"]);
assert!(spec.manual_sources[0].urls.is_empty());
}
@@ -0,0 +1,38 @@
use super::*;
#[test]
fn reject_unknown_nested_keys_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[dependencies]
runtime = ["glibc"]
skip_tests = true
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).expect_err("expected unknown nested key to fail");
assert!(
err.to_string()
.contains("unknown key: dependencies.skip_tests")
);
}
+1 -136
View File
@@ -1234,139 +1234,4 @@ fn sh_single_quote(value: &str) -> String {
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_single_package_starbuild_generates_custom_spec_and_build_script() -> Result<()> {
let temp = tempfile::tempdir()?;
let starbuild = temp.path().join("STARBUILD");
fs::write(
&starbuild,
r#"
package_name="meson"
package_version="1.2.3"
description="build system"
license=( "Apache-2.0" )
dependencies=( "python" "ninja" )
build_dependencies=( "git" )
sources=( "helper.sh" "https://github.com/mesonbuild/meson.git#v$package_version" "ne+https://example.com/extra.tar.xz" )
BUILD_ZLIB=True
compile() {
cd meson
python -m build
}
assemble() {
cd meson
python -m installer --destdir="$pkgdir" dist/*.whl
}
"#,
)?;
let converted = convert_starbuild_file(&starbuild, None)?;
let spec_path = temp.path().join("meson.toml");
assert_eq!(converted.output_path, spec_path);
assert!(converted.build_script.is_some());
assert_eq!(
converted.build_script_path,
Some(temp.path().join("build.sh"))
);
assert!(converted.toml.contains("type = \"custom\""));
assert!(
converted
.toml
.contains("url = \"https://github.com/mesonbuild/meson.git#v1.2.3\"")
);
assert!(converted.toml.contains("file = \"helper.sh\""));
assert!(
converted
.toml
.contains("url = \"https://example.com/extra.tar.xz\"")
);
fs::write(&spec_path, &converted.toml)?;
fs::write(
temp.path().join("build.sh"),
converted.build_script.unwrap(),
)?;
let spec = PackageSpec::from_file(&spec_path)?;
assert_eq!(spec.package.name, "meson");
assert_eq!(spec.dependencies.runtime, vec!["python", "ninja"]);
assert_eq!(spec.dependencies.build, vec!["git"]);
assert_eq!(spec.manual_sources.len(), 2);
assert_eq!(spec.source.len(), 1);
Ok(())
}
#[test]
fn convert_multioutput_starbuild_maps_output_metadata() -> Result<()> {
let temp = tempfile::tempdir()?;
let starbuild = temp.path().join("STARBUILD");
fs::write(
&starbuild,
r#"
package_name=( "mesa" "vulkan-intel" )
package_version="25.3.1"
package_descriptions=( "Mesa" "Intel Vulkan" )
license=( "MIT" )
dependencies=( "expat" )
build_dependencies=( "meson" )
sources=( "https://mesa.freedesktop.org/archive/mesa-$package_version.tar.xz" )
dependencies_vulkan-intel=( "vulkan-icd-loader" "mesa" )
gives_vulkan-intel=( "vulkan-driver" )
clashes_vulkan-intel=( "old-vulkan-intel" )
keep_vulkan-intel=( "etc/vulkan/intel.conf" )
compile() {
cd mesa-$package_version
meson setup build
}
assemble_mesa() {
cd mesa-$package_version
mesoni -C build
}
assemble_vulkan-intel() {
starmove usr/lib/libvulkan_intel.so
}
"#,
)?;
let converted = convert_starbuild_file(&starbuild, None)?;
assert!(converted.toml.contains("name = \"vulkan-intel\""));
assert!(
converted
.toml
.contains("keep = [\"etc/vulkan/intel.conf\"]")
);
let build_script = converted.build_script.as_deref().unwrap_or("");
assert!(build_script.contains("depot_install_vulkan_intel()"));
assert!(build_script.contains("haul \"$DEPOT_OUTPUT_NAME\" \"$@\""));
assert!(build_script.contains("packages/vulkan-intel/files"));
let spec_path = temp.path().join("mesa.toml");
fs::write(&spec_path, &converted.toml)?;
fs::write(temp.path().join("build.sh"), build_script)?;
let spec = PackageSpec::from_file(&spec_path)?;
assert_eq!(spec.packages.len(), 1);
assert_eq!(spec.packages[0].name, "vulkan-intel");
assert_eq!(
spec.package_dependencies["vulkan-intel"].runtime,
vec!["vulkan-icd-loader", "mesa"]
);
assert_eq!(
spec.package_alternatives["vulkan-intel"].provides,
vec!["vulkan-driver"]
);
assert_eq!(
spec.package_alternatives["vulkan-intel"].conflicts,
vec!["old-vulkan-intel"]
);
Ok(())
}
}
mod tests;
+134
View File
@@ -0,0 +1,134 @@
use super::*;
#[test]
fn convert_single_package_starbuild_generates_custom_spec_and_build_script() -> Result<()> {
let temp = tempfile::tempdir()?;
let starbuild = temp.path().join("STARBUILD");
fs::write(
&starbuild,
r#"
package_name="meson"
package_version="1.2.3"
description="build system"
license=( "Apache-2.0" )
dependencies=( "python" "ninja" )
build_dependencies=( "git" )
sources=( "helper.sh" "https://github.com/mesonbuild/meson.git#v$package_version" "ne+https://example.com/extra.tar.xz" )
BUILD_ZLIB=True
compile() {
cd meson
python -m build
}
assemble() {
cd meson
python -m installer --destdir="$pkgdir" dist/*.whl
}
"#,
)?;
let converted = convert_starbuild_file(&starbuild, None)?;
let spec_path = temp.path().join("meson.toml");
assert_eq!(converted.output_path, spec_path);
assert!(converted.build_script.is_some());
assert_eq!(
converted.build_script_path,
Some(temp.path().join("build.sh"))
);
assert!(converted.toml.contains("type = \"custom\""));
assert!(
converted
.toml
.contains("url = \"https://github.com/mesonbuild/meson.git#v1.2.3\"")
);
assert!(converted.toml.contains("file = \"helper.sh\""));
assert!(
converted
.toml
.contains("url = \"https://example.com/extra.tar.xz\"")
);
fs::write(&spec_path, &converted.toml)?;
fs::write(
temp.path().join("build.sh"),
converted.build_script.unwrap(),
)?;
let spec = PackageSpec::from_file(&spec_path)?;
assert_eq!(spec.package.name, "meson");
assert_eq!(spec.dependencies.runtime, vec!["python", "ninja"]);
assert_eq!(spec.dependencies.build, vec!["git"]);
assert_eq!(spec.manual_sources.len(), 2);
assert_eq!(spec.source.len(), 1);
Ok(())
}
#[test]
fn convert_multioutput_starbuild_maps_output_metadata() -> Result<()> {
let temp = tempfile::tempdir()?;
let starbuild = temp.path().join("STARBUILD");
fs::write(
&starbuild,
r#"
package_name=( "mesa" "vulkan-intel" )
package_version="25.3.1"
package_descriptions=( "Mesa" "Intel Vulkan" )
license=( "MIT" )
dependencies=( "expat" )
build_dependencies=( "meson" )
sources=( "https://mesa.freedesktop.org/archive/mesa-$package_version.tar.xz" )
dependencies_vulkan-intel=( "vulkan-icd-loader" "mesa" )
gives_vulkan-intel=( "vulkan-driver" )
clashes_vulkan-intel=( "old-vulkan-intel" )
keep_vulkan-intel=( "etc/vulkan/intel.conf" )
compile() {
cd mesa-$package_version
meson setup build
}
assemble_mesa() {
cd mesa-$package_version
mesoni -C build
}
assemble_vulkan-intel() {
starmove usr/lib/libvulkan_intel.so
}
"#,
)?;
let converted = convert_starbuild_file(&starbuild, None)?;
assert!(converted.toml.contains("name = \"vulkan-intel\""));
assert!(
converted
.toml
.contains("keep = [\"etc/vulkan/intel.conf\"]")
);
let build_script = converted.build_script.as_deref().unwrap_or("");
assert!(build_script.contains("depot_install_vulkan_intel()"));
assert!(build_script.contains("haul \"$DEPOT_OUTPUT_NAME\" \"$@\""));
assert!(build_script.contains("packages/vulkan-intel/files"));
let spec_path = temp.path().join("mesa.toml");
fs::write(&spec_path, &converted.toml)?;
fs::write(temp.path().join("build.sh"), build_script)?;
let spec = PackageSpec::from_file(&spec_path)?;
assert_eq!(spec.packages.len(), 1);
assert_eq!(spec.packages[0].name, "vulkan-intel");
assert_eq!(
spec.package_dependencies["vulkan-intel"].runtime,
vec!["vulkan-icd-loader", "mesa"]
);
assert_eq!(
spec.package_alternatives["vulkan-intel"].provides,
vec!["vulkan-driver"]
);
assert_eq!(
spec.package_alternatives["vulkan-intel"].conflicts,
vec!["old-vulkan-intel"]
);
Ok(())
}
+1 -578
View File
@@ -924,581 +924,4 @@ pub(crate) fn build_dependency_install_plan(
}
#[cfg(test)]
mod tests {
use super::{
Candidate, CandidateKind, MatchKind, PlannerOptions, build_dependency_install_plan,
dedupe_candidate_packages, prune_replacement_fallback_candidates, sort_candidates,
source_deps_for_install,
};
use crate::config::{BinaryRepo, Config};
use crate::db;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
fn mk_spec() -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: vec![PackageInfo {
name: "foo-libs".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
}],
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.test/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: vec!["make".into()],
runtime: vec!["foo-libs".into(), "zlib".into()],
test: vec!["bats".into()],
optional: vec!["docs-viewer".into()],
groups: Vec::new(),
lib32: None,
},
package_alternatives: BTreeMap::from([(
"foo-libs".into(),
Alternatives {
provides: vec!["libfoo".into()],
conflicts: Vec::new(),
replaces: Vec::new(),
lib32: None,
},
)]),
package_dependencies: BTreeMap::from([(
"foo".into(),
Dependencies {
build: Vec::new(),
runtime: vec!["foo-libs".into(), "libfoo".into(), "openssl".into()],
test: Vec::new(),
optional: Vec::new(),
groups: Vec::new(),
lib32: None,
},
)]),
spec_dir: PathBuf::from("."),
}
}
fn mk_installed_spec(name: &str, version: &str) -> PackageSpec {
let mut spec = mk_spec();
spec.package.name = name.to_string();
spec.package.version = version.to_string();
spec
}
fn mk_binary_candidate(name: &str, repo_name: &str, priority: i32) -> Candidate {
Candidate {
package: name.to_string(),
kind: CandidateKind::Binary {
repo_name: repo_name.to_string(),
record: Box::new(db::repo::BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: name.to_string(),
real_name: None,
version: "1.0.0".to_string(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
size: 1024,
sha512: "sha512".to_string(),
description: None,
homepage: None,
license: None,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
}),
},
match_kind: MatchKind::Exact,
sort_repo_priority: priority,
sort_label: format!("binary:{repo_name}"),
}
}
fn mk_source_candidate(name: &str, path: &Path, local_sibling: bool) -> Candidate {
Candidate {
package: name.to_string(),
kind: CandidateKind::Source {
path: path.to_path_buf(),
local_sibling,
},
match_kind: MatchKind::Exact,
sort_repo_priority: if local_sibling { -10 } else { 0 },
sort_label: if local_sibling {
"source:local-sibling".to_string()
} else {
"source:local".to_string()
},
}
}
#[test]
fn source_deps_for_install_excludes_local_runtime_outputs_and_provides() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, false, false);
assert!(deps.contains(&"make".to_string()));
assert!(deps.contains(&"zlib".to_string()));
assert!(deps.contains(&"openssl".to_string()));
assert!(!deps.contains(&"foo-libs".to_string()));
assert!(!deps.contains(&"libfoo".to_string()));
}
#[test]
fn source_deps_for_install_does_not_include_test_deps() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, false, false);
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_includes_test_deps_when_enabled() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, true, false);
assert!(deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_uses_lib32_only_dependencies_when_requested() {
let mut spec = mk_spec();
spec.dependencies.lib32 = Some(crate::package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: vec!["lib32-bats".into()],
optional: Vec::new(),
groups: Vec::new(),
});
let deps = source_deps_for_install(&spec, true, true);
assert!(deps.contains(&"gcc-multilib".to_string()));
assert!(deps.contains(&"lib32-zlib".to_string()));
assert!(!deps.contains(&"lib32-bats".to_string()));
assert!(!deps.contains(&"make".to_string()));
assert!(!deps.contains(&"zlib".to_string()));
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_uses_lib32_only_dependencies_from_spec_flag() {
let mut spec = mk_spec();
spec.build.flags.lib32_only = true;
spec.dependencies.lib32 = Some(crate::package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: vec!["lib32-bats".into()],
optional: Vec::new(),
groups: Vec::new(),
});
let deps = source_deps_for_install(&spec, true, false);
assert!(deps.contains(&"gcc-multilib".to_string()));
assert!(deps.contains(&"lib32-zlib".to_string()));
assert!(!deps.contains(&"lib32-bats".to_string()));
assert!(!deps.contains(&"make".to_string()));
assert!(!deps.contains(&"zlib".to_string()));
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn candidate_dedup_keeps_highest_priority_origin_for_same_package() {
let candidates = vec![
mk_source_candidate("meson", Path::new("packages/core/meson/meson.toml"), false),
mk_binary_candidate("meson", "core", 0),
mk_source_candidate(
"meson",
Path::new("../packages/core/meson/meson.toml"),
true,
),
];
let deduped = dedupe_candidate_packages(sort_candidates(&candidates, true));
assert_eq!(deduped.len(), 1);
assert!(matches!(deduped[0].kind, CandidateKind::Binary { .. }));
}
#[test]
fn candidate_dedup_uses_local_origin_when_binaries_are_not_preferred() {
let candidates = vec![
mk_binary_candidate("ninja", "core", 0),
mk_source_candidate("ninja", Path::new("packages/core/ninja/ninja.toml"), false),
];
let deduped = dedupe_candidate_packages(sort_candidates(&candidates, false));
assert_eq!(deduped.len(), 1);
assert!(matches!(deduped[0].kind, CandidateKind::Source { .. }));
}
#[test]
fn replacement_candidates_are_pruned_when_direct_matches_exist() {
let mut replacement = mk_binary_candidate("vx", "core", 0);
replacement.match_kind = MatchKind::Replaces;
let mut exact = mk_binary_candidate("patch", "core", 0);
exact.match_kind = MatchKind::Exact;
let mut provides = mk_binary_candidate("busybox", "core", 0);
provides.match_kind = MatchKind::Provides;
let pruned = prune_replacement_fallback_candidates(vec![replacement, exact, provides]);
assert_eq!(pruned.len(), 2);
assert!(
pruned
.iter()
.all(|candidate| candidate.match_kind != MatchKind::Replaces)
);
}
#[test]
fn replacement_candidates_remain_when_they_are_the_only_matches() {
let mut replacement = mk_binary_candidate("vx", "core", 0);
replacement.match_kind = MatchKind::Replaces;
let pruned = prune_replacement_fallback_candidates(vec![replacement]);
assert_eq!(pruned.len(), 1);
assert!(matches!(pruned[0].match_kind, MatchKind::Replaces));
assert_eq!(pruned[0].package, "vx");
}
#[test]
fn build_dependency_install_plan_skips_installed_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let db_path = config.db_dir.join("packages.db");
let destdir = rootfs.path().join("dest");
std::fs::create_dir_all(&destdir).unwrap();
db::register_package(&db_path, &mk_installed_spec("meson", "1.0.0"), &destdir).unwrap();
let plan = build_dependency_install_plan(
&config,
rootfs.path(),
&["meson".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
assert!(plan.steps.is_empty());
assert!(plan.actionable_steps().next().is_none());
}
#[test]
fn build_dependency_install_plan_reports_source_cycle_chain() {
let rootfs = tempfile::tempdir().unwrap();
let repo_root = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let alpha_dir = repo_root.path().join("alpha");
let beta_dir = repo_root.path().join("beta");
fs::create_dir_all(&alpha_dir).unwrap();
fs::create_dir_all(&beta_dir).unwrap();
fs::write(
alpha_dir.join("alpha.toml"),
r#"
[build]
type = "meta"
[dependencies]
runtime = ["beta"]
[package]
description = "alpha"
homepage = "https://example.test/alpha"
license = "MIT"
name = "alpha"
version = "1.0.0"
"#,
)
.unwrap();
fs::write(
beta_dir.join("beta.toml"),
r#"
[build]
type = "meta"
[dependencies]
runtime = ["alpha"]
[package]
description = "beta"
homepage = "https://example.test/beta"
license = "MIT"
name = "beta"
version = "1.0.0"
"#,
)
.unwrap();
let err = build_dependency_install_plan(
&config,
rootfs.path(),
&["alpha".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: false,
local_sibling_root: Some(repo_root.path().to_path_buf()),
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
"Dependency cycle detected: alpha -> beta -> alpha"
);
}
#[test]
fn build_dependency_install_plan_matches_local_sibling_real_name() {
let rootfs = tempfile::tempdir().unwrap();
let repo_root = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let libressl_dir = repo_root.path().join("libressl43");
fs::create_dir_all(&libressl_dir).unwrap();
fs::write(
libressl_dir.join("libressl43.toml"),
r#"
[build]
type = "meta"
[package]
description = "LibreSSL"
homepage = "https://www.libressl.org/"
license = "ISC"
name = "libressl43"
real_name = "libressl"
version = "4.3.2"
"#,
)
.unwrap();
let plan = build_dependency_install_plan(
&config,
rootfs.path(),
&["libressl".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: false,
local_sibling_root: Some(repo_root.path().to_path_buf()),
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].package, "libressl43");
assert!(matches!(
plan.steps[0].origin,
super::PlanOrigin::Source {
local_sibling: true,
..
}
));
}
fn write_compressed_repo_db(db_path: &Path, zst_path: &Path) {
let mut input = fs::File::open(db_path).unwrap();
let output = fs::File::create(zst_path).unwrap();
let mut encoder = zstd::stream::write::Encoder::new(output, 3).unwrap();
std::io::copy(&mut input, &mut encoder).unwrap();
encoder.finish().unwrap();
}
#[test]
fn binary_plan_uses_built_against_concrete_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let mut config = Config::for_rootfs(rootfs.path());
config.package_cache_dir = cache_dir.path().to_path_buf();
config.repo_settings.prefer_binary = true;
config.binary_repos.insert(
"core".into(),
BinaryRepo {
url: url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string(),
allow_unsigned: true,
..BinaryRepo::default()
},
);
let installed_dest = rootfs.path().join("installed");
fs::create_dir_all(&installed_dest).unwrap();
let mut installed = mk_installed_spec("icu79", "79.1");
installed.package.real_name = Some("icu".into());
db::register_package(
&config.installed_db_path(rootfs.path()),
&installed,
&installed_dest,
)
.unwrap();
let db_path = repo_dir.path().join("repo.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"
CREATE TABLE packages (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
real_name TEXT,
version TEXT NOT NULL,
revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER,
description TEXT,
homepage TEXT,
license TEXT,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
sha512 TEXT NOT NULL
);
CREATE TABLE provides (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE replaces (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE conflicts (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE dependencies (package_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL);
CREATE TABLE groups (package_id INTEGER, name TEXT NOT NULL);
",
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, NULL, 'app.pkg', 1, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (1, 'runtime', 'icu')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu78.pkg', 1, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (3, 'icu79', 'icu', '79.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu79.pkg', 1, 'ee', 'ff')",
[],
)
.unwrap();
drop(conn);
write_compressed_repo_db(&db_path, &repo_dir.path().join("repo.db.zst"));
fs::remove_file(&db_path).unwrap();
let plan = super::build_install_plan(
&config,
rootfs.path(),
super::InstallTarget::PackageName("app".into()),
PlannerOptions {
assume_yes: true,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
let actionable: Vec<_> = plan
.actionable_steps()
.map(|step| step.package.as_str())
.collect();
assert_eq!(actionable, vec!["icu78", "app"]);
}
#[test]
fn add_dependency_edge_skips_active_binary_cycle_back_edge() {
let rootfs = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let mut resolver = super::Resolver::new(
&config,
rootfs.path(),
PlannerOptions {
assume_yes: false,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
);
let freetype2 = resolver.graph.add_node(super::NodeData {
step: super::PlannedStep {
package: "freetype2".into(),
action: super::PlanAction::InstallBinary,
origin: super::PlanOrigin::Installed,
requested_by: vec!["requested".into()],
},
});
let harfbuzz = resolver.graph.add_node(super::NodeData {
step: super::PlannedStep {
package: "harfbuzz".into(),
action: super::PlanAction::InstallBinary,
origin: super::PlanOrigin::Installed,
requested_by: vec!["requested".into()],
},
});
resolver.stack = vec!["freetype2".into(), "harfbuzz".into()];
resolver
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
.unwrap();
assert_eq!(resolver.graph.edge_count(), 0);
resolver.stack.clear();
resolver
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
.unwrap();
assert_eq!(resolver.graph.edge_count(), 1);
}
}
mod tests;
+576
View File
@@ -0,0 +1,576 @@
use super::{
Candidate, CandidateKind, MatchKind, PlannerOptions, build_dependency_install_plan,
dedupe_candidate_packages, prune_replacement_fallback_candidates, sort_candidates,
source_deps_for_install,
};
use crate::config::{BinaryRepo, Config};
use crate::db;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
fn mk_spec() -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: vec![PackageInfo {
name: "foo-libs".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
}],
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.test/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: vec!["make".into()],
runtime: vec!["foo-libs".into(), "zlib".into()],
test: vec!["bats".into()],
optional: vec!["docs-viewer".into()],
groups: Vec::new(),
lib32: None,
},
package_alternatives: BTreeMap::from([(
"foo-libs".into(),
Alternatives {
provides: vec!["libfoo".into()],
conflicts: Vec::new(),
replaces: Vec::new(),
lib32: None,
},
)]),
package_dependencies: BTreeMap::from([(
"foo".into(),
Dependencies {
build: Vec::new(),
runtime: vec!["foo-libs".into(), "libfoo".into(), "openssl".into()],
test: Vec::new(),
optional: Vec::new(),
groups: Vec::new(),
lib32: None,
},
)]),
spec_dir: PathBuf::from("."),
}
}
fn mk_installed_spec(name: &str, version: &str) -> PackageSpec {
let mut spec = mk_spec();
spec.package.name = name.to_string();
spec.package.version = version.to_string();
spec
}
fn mk_binary_candidate(name: &str, repo_name: &str, priority: i32) -> Candidate {
Candidate {
package: name.to_string(),
kind: CandidateKind::Binary {
repo_name: repo_name.to_string(),
record: Box::new(db::repo::BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: name.to_string(),
real_name: None,
version: "1.0.0".to_string(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
size: 1024,
sha512: "sha512".to_string(),
description: None,
homepage: None,
license: None,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
}),
},
match_kind: MatchKind::Exact,
sort_repo_priority: priority,
sort_label: format!("binary:{repo_name}"),
}
}
fn mk_source_candidate(name: &str, path: &Path, local_sibling: bool) -> Candidate {
Candidate {
package: name.to_string(),
kind: CandidateKind::Source {
path: path.to_path_buf(),
local_sibling,
},
match_kind: MatchKind::Exact,
sort_repo_priority: if local_sibling { -10 } else { 0 },
sort_label: if local_sibling {
"source:local-sibling".to_string()
} else {
"source:local".to_string()
},
}
}
#[test]
fn source_deps_for_install_excludes_local_runtime_outputs_and_provides() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, false, false);
assert!(deps.contains(&"make".to_string()));
assert!(deps.contains(&"zlib".to_string()));
assert!(deps.contains(&"openssl".to_string()));
assert!(!deps.contains(&"foo-libs".to_string()));
assert!(!deps.contains(&"libfoo".to_string()));
}
#[test]
fn source_deps_for_install_does_not_include_test_deps() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, false, false);
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_includes_test_deps_when_enabled() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, true, false);
assert!(deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_uses_lib32_only_dependencies_when_requested() {
let mut spec = mk_spec();
spec.dependencies.lib32 = Some(crate::package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: vec!["lib32-bats".into()],
optional: Vec::new(),
groups: Vec::new(),
});
let deps = source_deps_for_install(&spec, true, true);
assert!(deps.contains(&"gcc-multilib".to_string()));
assert!(deps.contains(&"lib32-zlib".to_string()));
assert!(!deps.contains(&"lib32-bats".to_string()));
assert!(!deps.contains(&"make".to_string()));
assert!(!deps.contains(&"zlib".to_string()));
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_uses_lib32_only_dependencies_from_spec_flag() {
let mut spec = mk_spec();
spec.build.flags.lib32_only = true;
spec.dependencies.lib32 = Some(crate::package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: vec!["lib32-bats".into()],
optional: Vec::new(),
groups: Vec::new(),
});
let deps = source_deps_for_install(&spec, true, false);
assert!(deps.contains(&"gcc-multilib".to_string()));
assert!(deps.contains(&"lib32-zlib".to_string()));
assert!(!deps.contains(&"lib32-bats".to_string()));
assert!(!deps.contains(&"make".to_string()));
assert!(!deps.contains(&"zlib".to_string()));
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn candidate_dedup_keeps_highest_priority_origin_for_same_package() {
let candidates = vec![
mk_source_candidate("meson", Path::new("packages/core/meson/meson.toml"), false),
mk_binary_candidate("meson", "core", 0),
mk_source_candidate(
"meson",
Path::new("../packages/core/meson/meson.toml"),
true,
),
];
let deduped = dedupe_candidate_packages(sort_candidates(&candidates, true));
assert_eq!(deduped.len(), 1);
assert!(matches!(deduped[0].kind, CandidateKind::Binary { .. }));
}
#[test]
fn candidate_dedup_uses_local_origin_when_binaries_are_not_preferred() {
let candidates = vec![
mk_binary_candidate("ninja", "core", 0),
mk_source_candidate("ninja", Path::new("packages/core/ninja/ninja.toml"), false),
];
let deduped = dedupe_candidate_packages(sort_candidates(&candidates, false));
assert_eq!(deduped.len(), 1);
assert!(matches!(deduped[0].kind, CandidateKind::Source { .. }));
}
#[test]
fn replacement_candidates_are_pruned_when_direct_matches_exist() {
let mut replacement = mk_binary_candidate("vx", "core", 0);
replacement.match_kind = MatchKind::Replaces;
let mut exact = mk_binary_candidate("patch", "core", 0);
exact.match_kind = MatchKind::Exact;
let mut provides = mk_binary_candidate("busybox", "core", 0);
provides.match_kind = MatchKind::Provides;
let pruned = prune_replacement_fallback_candidates(vec![replacement, exact, provides]);
assert_eq!(pruned.len(), 2);
assert!(
pruned
.iter()
.all(|candidate| candidate.match_kind != MatchKind::Replaces)
);
}
#[test]
fn replacement_candidates_remain_when_they_are_the_only_matches() {
let mut replacement = mk_binary_candidate("vx", "core", 0);
replacement.match_kind = MatchKind::Replaces;
let pruned = prune_replacement_fallback_candidates(vec![replacement]);
assert_eq!(pruned.len(), 1);
assert!(matches!(pruned[0].match_kind, MatchKind::Replaces));
assert_eq!(pruned[0].package, "vx");
}
#[test]
fn build_dependency_install_plan_skips_installed_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let db_path = config.db_dir.join("packages.db");
let destdir = rootfs.path().join("dest");
std::fs::create_dir_all(&destdir).unwrap();
db::register_package(&db_path, &mk_installed_spec("meson", "1.0.0"), &destdir).unwrap();
let plan = build_dependency_install_plan(
&config,
rootfs.path(),
&["meson".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
assert!(plan.steps.is_empty());
assert!(plan.actionable_steps().next().is_none());
}
#[test]
fn build_dependency_install_plan_reports_source_cycle_chain() {
let rootfs = tempfile::tempdir().unwrap();
let repo_root = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let alpha_dir = repo_root.path().join("alpha");
let beta_dir = repo_root.path().join("beta");
fs::create_dir_all(&alpha_dir).unwrap();
fs::create_dir_all(&beta_dir).unwrap();
fs::write(
alpha_dir.join("alpha.toml"),
r#"
[build]
type = "meta"
[dependencies]
runtime = ["beta"]
[package]
description = "alpha"
homepage = "https://example.test/alpha"
license = "MIT"
name = "alpha"
version = "1.0.0"
"#,
)
.unwrap();
fs::write(
beta_dir.join("beta.toml"),
r#"
[build]
type = "meta"
[dependencies]
runtime = ["alpha"]
[package]
description = "beta"
homepage = "https://example.test/beta"
license = "MIT"
name = "beta"
version = "1.0.0"
"#,
)
.unwrap();
let err = build_dependency_install_plan(
&config,
rootfs.path(),
&["alpha".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: false,
local_sibling_root: Some(repo_root.path().to_path_buf()),
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
"Dependency cycle detected: alpha -> beta -> alpha"
);
}
#[test]
fn build_dependency_install_plan_matches_local_sibling_real_name() {
let rootfs = tempfile::tempdir().unwrap();
let repo_root = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let libressl_dir = repo_root.path().join("libressl43");
fs::create_dir_all(&libressl_dir).unwrap();
fs::write(
libressl_dir.join("libressl43.toml"),
r#"
[build]
type = "meta"
[package]
description = "LibreSSL"
homepage = "https://www.libressl.org/"
license = "ISC"
name = "libressl43"
real_name = "libressl"
version = "4.3.2"
"#,
)
.unwrap();
let plan = build_dependency_install_plan(
&config,
rootfs.path(),
&["libressl".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: false,
local_sibling_root: Some(repo_root.path().to_path_buf()),
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].package, "libressl43");
assert!(matches!(
plan.steps[0].origin,
super::PlanOrigin::Source {
local_sibling: true,
..
}
));
}
fn write_compressed_repo_db(db_path: &Path, zst_path: &Path) {
let mut input = fs::File::open(db_path).unwrap();
let output = fs::File::create(zst_path).unwrap();
let mut encoder = zstd::stream::write::Encoder::new(output, 3).unwrap();
std::io::copy(&mut input, &mut encoder).unwrap();
encoder.finish().unwrap();
}
#[test]
fn binary_plan_uses_built_against_concrete_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let mut config = Config::for_rootfs(rootfs.path());
config.package_cache_dir = cache_dir.path().to_path_buf();
config.repo_settings.prefer_binary = true;
config.binary_repos.insert(
"core".into(),
BinaryRepo {
url: url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string(),
allow_unsigned: true,
..BinaryRepo::default()
},
);
let installed_dest = rootfs.path().join("installed");
fs::create_dir_all(&installed_dest).unwrap();
let mut installed = mk_installed_spec("icu79", "79.1");
installed.package.real_name = Some("icu".into());
db::register_package(
&config.installed_db_path(rootfs.path()),
&installed,
&installed_dest,
)
.unwrap();
let db_path = repo_dir.path().join("repo.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"
CREATE TABLE packages (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
real_name TEXT,
version TEXT NOT NULL,
revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER,
description TEXT,
homepage TEXT,
license TEXT,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
sha512 TEXT NOT NULL
);
CREATE TABLE provides (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE replaces (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE conflicts (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE dependencies (package_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL);
CREATE TABLE groups (package_id INTEGER, name TEXT NOT NULL);
",
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, NULL, 'app.pkg', 1, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (1, 'runtime', 'icu')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu78.pkg', 1, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (3, 'icu79', 'icu', '79.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu79.pkg', 1, 'ee', 'ff')",
[],
)
.unwrap();
drop(conn);
write_compressed_repo_db(&db_path, &repo_dir.path().join("repo.db.zst"));
fs::remove_file(&db_path).unwrap();
let plan = super::build_install_plan(
&config,
rootfs.path(),
super::InstallTarget::PackageName("app".into()),
PlannerOptions {
assume_yes: true,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
let actionable: Vec<_> = plan
.actionable_steps()
.map(|step| step.package.as_str())
.collect();
assert_eq!(actionable, vec!["icu78", "app"]);
}
#[test]
fn add_dependency_edge_skips_active_binary_cycle_back_edge() {
let rootfs = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let mut resolver = super::Resolver::new(
&config,
rootfs.path(),
PlannerOptions {
assume_yes: false,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
);
let freetype2 = resolver.graph.add_node(super::NodeData {
step: super::PlannedStep {
package: "freetype2".into(),
action: super::PlanAction::InstallBinary,
origin: super::PlanOrigin::Installed,
requested_by: vec!["requested".into()],
},
});
let harfbuzz = resolver.graph.add_node(super::NodeData {
step: super::PlannedStep {
package: "harfbuzz".into(),
action: super::PlanAction::InstallBinary,
origin: super::PlanOrigin::Installed,
requested_by: vec!["requested".into()],
},
});
resolver.stack = vec!["freetype2".into(), "harfbuzz".into()];
resolver
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
.unwrap();
assert_eq!(resolver.graph.edge_count(), 0);
resolver.stack.clear();
resolver
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
.unwrap();
assert_eq!(resolver.graph.edge_count(), 1);
}
Executable → Regular
+1 -371
View File
@@ -693,374 +693,4 @@ fn signal_child_group(child: &std::process::Child, signal: i32) {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo};
use lz4_flex::frame::FrameEncoder as Lz4FrameEncoder;
use lzma_rust2::{LzipOptions, LzipWriter, LzmaOptions, LzmaWriter};
use std::io::Write;
use std::time::{Duration, SystemTime};
use tempfile::tempdir;
fn test_spec() -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
fn test_source(extract_dir: &str) -> Source {
Source {
url: "https://example.test/src.tar".into(),
sha256: "skip".into(),
extract_dir: extract_dir.into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}
}
fn simple_tar_bytes(top_dir: &str, file_name: &str, contents: &[u8]) -> Vec<u8> {
let mut tar_buf = Vec::new();
{
let mut tar = tar::Builder::new(&mut tar_buf);
let mut header = tar::Header::new_gnu();
header.set_size(contents.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, format!("{top_dir}/{file_name}"), contents)
.unwrap();
tar.finish().unwrap();
}
tar_buf
}
#[test]
fn test_extract_deb_roundtrip() {
let tmp = tempdir().unwrap();
let deb_path = tmp.path().join("test.deb");
let extract_dir = tmp.path().join("out-deb");
// create a small tar.gz payload with one file
let mut tar_buf = Vec::new();
{
let gz = flate2::write::GzEncoder::new(&mut tar_buf, flate2::Compression::default());
let mut tar = tar::Builder::new(gz);
let mut header = tar::Header::new_gnu();
let data = b"hello-deb";
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, "usr/bin/hello-deb", &data[..])
.unwrap();
tar.finish().unwrap();
}
// write ar archive with member data.tar.gz (use temp files because ar::Builder expects File)
{
let db = tmp.path().join("debian-binary");
let ct = tmp.path().join("control.tar.gz");
let dt = tmp.path().join("data.tar.gz");
std::fs::write(&db, b"2.0\n").unwrap();
std::fs::write(&ct, b"").unwrap();
std::fs::write(&dt, &tar_buf[..]).unwrap();
let mut f = File::create(&deb_path).unwrap();
let mut builder = ar::Builder::new(&mut f);
let mut dbf = File::open(&db).unwrap();
let mut ctf = File::open(&ct).unwrap();
let mut dtf = File::open(&dt).unwrap();
builder.append_file(b"debian-binary", &mut dbf).unwrap();
builder.append_file(b"control.tar.gz", &mut ctf).unwrap();
builder.append_file(b"data.tar.gz", &mut dtf).unwrap();
}
fs::create_dir_all(&extract_dir).unwrap();
extract_deb(&deb_path, &extract_dir).unwrap();
assert!(extract_dir.join("usr/bin/hello-deb").exists());
}
#[test]
fn test_archive_format_for_new_extensions() {
assert_eq!(
archive_format_for_filename("pkg.tar.lz4"),
Some(ArchiveFormat::TarLz4)
);
assert_eq!(
archive_format_for_filename("pkg.tar.lzma"),
Some(ArchiveFormat::TarLzma)
);
assert_eq!(
archive_format_for_filename("pkg.tar.lz"),
Some(ArchiveFormat::TarLzip)
);
assert_eq!(
archive_format_for_filename("pkg.tar.Z"),
Some(ArchiveFormat::TarCompress)
);
assert_eq!(
archive_format_for_filename("pkg.cpio"),
Some(ArchiveFormat::Cpio)
);
}
#[test]
fn test_extract_archive_tar_lz4_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.tar.lz4");
let build_dir = tmp.path().join("build");
let tar_buf = simple_tar_bytes("pkg-1.0", "hello.txt", b"hello-lz4");
let mut compressed = Vec::new();
{
let mut encoder = Lz4FrameEncoder::new(&mut compressed);
encoder.write_all(&tar_buf).unwrap();
encoder.finish().unwrap();
}
fs::write(&archive_path, compressed).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-lz4"
);
}
#[test]
fn test_extract_archive_tar_lzma_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.tar.lzma");
let build_dir = tmp.path().join("build");
let tar_buf = simple_tar_bytes("pkg-1.0", "hello.txt", b"hello-lzma");
let mut compressed = Vec::new();
{
let options = LzmaOptions::default();
let mut writer =
LzmaWriter::new_use_header(&mut compressed, &options, Some(tar_buf.len() as u64))
.unwrap();
writer.write_all(&tar_buf).unwrap();
writer.finish().unwrap();
}
fs::write(&archive_path, compressed).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-lzma"
);
}
#[test]
fn test_extract_archive_tar_lzip_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.tar.lz");
let build_dir = tmp.path().join("build");
let tar_buf = simple_tar_bytes("pkg-1.0", "hello.txt", b"hello-lzip");
let mut compressed = Vec::new();
{
let mut writer = LzipWriter::new(&mut compressed, LzipOptions::default());
writer.write_all(&tar_buf).unwrap();
writer.finish().unwrap();
}
fs::write(&archive_path, compressed).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-lzip"
);
}
#[test]
fn test_extract_archive_cpio_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.cpio");
let build_dir = tmp.path().join("build");
let mut cpio = Vec::new();
write_cpio_newc_one_file(&mut cpio, "pkg-1.0/hello.txt", b"hello-cpio");
write_cpio_trailer(&mut cpio);
fs::write(&archive_path, cpio).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-cpio"
);
}
#[test]
fn copy_file_preserve_metadata_keeps_mtime() {
let tmp = tempdir().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
std::fs::write(&src, b"hello").unwrap();
let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(946684800); // 2000-01-01 UTC
let ts = FileTime::from_system_time(fixed);
filetime::set_file_times(&src, ts, ts).unwrap();
copy_file_preserve_metadata(&src, &dst).unwrap();
let src_meta = std::fs::metadata(&src).unwrap();
let dst_meta = std::fs::metadata(&dst).unwrap();
assert_eq!(
FileTime::from_last_modification_time(&dst_meta),
FileTime::from_last_modification_time(&src_meta)
);
}
#[test]
fn copy_entry_fallback_preserves_symlink_when_target_is_missing() {
let tmp = tempdir().unwrap();
let src_dir = tmp.path().join("src");
let dst_dir = tmp.path().join("dst");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::create_dir_all(&dst_dir).unwrap();
let src_link = src_dir.join("RELEASE-NOTES");
std::os::unix::fs::symlink("doc/RelNotes/v1.47.3.txt", &src_link).unwrap();
// Simulate target already moved/removed before copying this symlink entry.
let src_target_dir = src_dir.join("doc");
std::fs::create_dir_all(src_target_dir.join("RelNotes")).unwrap();
std::fs::write(src_target_dir.join("RelNotes/v1.47.3.txt"), "notes").unwrap();
std::fs::remove_dir_all(&src_target_dir).unwrap();
let dst_link = dst_dir.join("RELEASE-NOTES");
let file_type = std::fs::symlink_metadata(&src_link).unwrap().file_type();
copy_entry_fallback(&src_link, &dst_link, file_type).unwrap();
assert!(std::fs::symlink_metadata(&src_link).is_err());
let dst_meta = std::fs::symlink_metadata(&dst_link).unwrap();
assert!(dst_meta.file_type().is_symlink());
assert_eq!(
std::fs::read_link(&dst_link).unwrap(),
PathBuf::from("doc/RelNotes/v1.47.3.txt")
);
}
fn write_cpio_newc_one_file(w: &mut Vec<u8>, name: &str, data: &[u8]) {
// magic + 13 fields of 8 hex chars each
fn h8(v: u64) -> String {
format!("{:08x}", v)
}
let namesize = name.len() + 1;
let filesize = data.len();
let mut header = Vec::new();
header.extend_from_slice(b"070701");
// ino, mode, uid, gid, nlink, mtime, filesize, devmajor, devminor, rdevmajor, rdevminor, namesize, check
header.extend_from_slice(h8(0).as_bytes()); // ino
header.extend_from_slice(h8(0o100644).as_bytes()); // mode regular file with perms
header.extend_from_slice(h8(0).as_bytes()); // uid
header.extend_from_slice(h8(0).as_bytes()); // gid
header.extend_from_slice(h8(1).as_bytes()); // nlink
header.extend_from_slice(h8(0).as_bytes()); // mtime
header.extend_from_slice(h8(filesize as u64).as_bytes());
header.extend_from_slice(h8(0).as_bytes()); // devmajor
header.extend_from_slice(h8(0).as_bytes()); // devminor
header.extend_from_slice(h8(0).as_bytes()); // rdevmajor
header.extend_from_slice(h8(0).as_bytes()); // rdevminor
header.extend_from_slice(h8(namesize as u64).as_bytes());
header.extend_from_slice(h8(0).as_bytes()); // check
w.extend_from_slice(&header);
w.extend_from_slice(name.as_bytes());
w.push(0);
// pad to 4
let pad = (4 - ((110 + namesize) % 4)) % 4;
for _ in 0..pad {
w.push(0);
}
// file data
w.extend_from_slice(data);
let dpad = (4 - (filesize % 4)) % 4;
for _ in 0..dpad {
w.push(0);
}
}
fn write_cpio_trailer(w: &mut Vec<u8>) {
write_cpio_newc_one_file(w, "TRAILER!!!", &[]);
}
#[test]
fn test_extract_rpm_roundtrip() {
let tmp = tempdir().unwrap();
let rpm_path = tmp.path().join("test.rpm");
let extract_dir = tmp.path().join("out-rpm");
// build cpio newc stream with one file
let mut cpio = Vec::new();
write_cpio_newc_one_file(&mut cpio, "usr/bin/hello-rpm", b"hello-rpm");
write_cpio_trailer(&mut cpio);
// gzip compress
let mut gz = Vec::new();
{
let mut enc = flate2::write::GzEncoder::new(&mut gz, flate2::Compression::default());
enc.write_all(&cpio).unwrap();
enc.finish().unwrap();
}
// write fake rpm: some header bytes then gz payload
{
let mut f = File::create(&rpm_path).unwrap();
f.write_all(b"RPMHEAD").unwrap();
f.write_all(&gz).unwrap();
}
fs::create_dir_all(&extract_dir).unwrap();
extract_rpm(&rpm_path, &extract_dir).unwrap();
assert!(extract_dir.join("usr/bin/hello-rpm").exists());
}
}
mod tests;
+369
View File
@@ -0,0 +1,369 @@
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo};
use lz4_flex::frame::FrameEncoder as Lz4FrameEncoder;
use lzma_rust2::{LzipOptions, LzipWriter, LzmaOptions, LzmaWriter};
use std::io::Write;
use std::time::{Duration, SystemTime};
use tempfile::tempdir;
fn test_spec() -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "pkg".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
fn test_source(extract_dir: &str) -> Source {
Source {
url: "https://example.test/src.tar".into(),
sha256: "skip".into(),
extract_dir: extract_dir.into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}
}
fn simple_tar_bytes(top_dir: &str, file_name: &str, contents: &[u8]) -> Vec<u8> {
let mut tar_buf = Vec::new();
{
let mut tar = tar::Builder::new(&mut tar_buf);
let mut header = tar::Header::new_gnu();
header.set_size(contents.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, format!("{top_dir}/{file_name}"), contents)
.unwrap();
tar.finish().unwrap();
}
tar_buf
}
#[test]
fn test_extract_deb_roundtrip() {
let tmp = tempdir().unwrap();
let deb_path = tmp.path().join("test.deb");
let extract_dir = tmp.path().join("out-deb");
// create a small tar.gz payload with one file
let mut tar_buf = Vec::new();
{
let gz = flate2::write::GzEncoder::new(&mut tar_buf, flate2::Compression::default());
let mut tar = tar::Builder::new(gz);
let mut header = tar::Header::new_gnu();
let data = b"hello-deb";
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, "usr/bin/hello-deb", &data[..])
.unwrap();
tar.finish().unwrap();
}
// write ar archive with member data.tar.gz (use temp files because ar::Builder expects File)
{
let db = tmp.path().join("debian-binary");
let ct = tmp.path().join("control.tar.gz");
let dt = tmp.path().join("data.tar.gz");
std::fs::write(&db, b"2.0\n").unwrap();
std::fs::write(&ct, b"").unwrap();
std::fs::write(&dt, &tar_buf[..]).unwrap();
let mut f = File::create(&deb_path).unwrap();
let mut builder = ar::Builder::new(&mut f);
let mut dbf = File::open(&db).unwrap();
let mut ctf = File::open(&ct).unwrap();
let mut dtf = File::open(&dt).unwrap();
builder.append_file(b"debian-binary", &mut dbf).unwrap();
builder.append_file(b"control.tar.gz", &mut ctf).unwrap();
builder.append_file(b"data.tar.gz", &mut dtf).unwrap();
}
fs::create_dir_all(&extract_dir).unwrap();
extract_deb(&deb_path, &extract_dir).unwrap();
assert!(extract_dir.join("usr/bin/hello-deb").exists());
}
#[test]
fn test_archive_format_for_new_extensions() {
assert_eq!(
archive_format_for_filename("pkg.tar.lz4"),
Some(ArchiveFormat::TarLz4)
);
assert_eq!(
archive_format_for_filename("pkg.tar.lzma"),
Some(ArchiveFormat::TarLzma)
);
assert_eq!(
archive_format_for_filename("pkg.tar.lz"),
Some(ArchiveFormat::TarLzip)
);
assert_eq!(
archive_format_for_filename("pkg.tar.Z"),
Some(ArchiveFormat::TarCompress)
);
assert_eq!(
archive_format_for_filename("pkg.cpio"),
Some(ArchiveFormat::Cpio)
);
}
#[test]
fn test_extract_archive_tar_lz4_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.tar.lz4");
let build_dir = tmp.path().join("build");
let tar_buf = simple_tar_bytes("pkg-1.0", "hello.txt", b"hello-lz4");
let mut compressed = Vec::new();
{
let mut encoder = Lz4FrameEncoder::new(&mut compressed);
encoder.write_all(&tar_buf).unwrap();
encoder.finish().unwrap();
}
fs::write(&archive_path, compressed).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-lz4"
);
}
#[test]
fn test_extract_archive_tar_lzma_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.tar.lzma");
let build_dir = tmp.path().join("build");
let tar_buf = simple_tar_bytes("pkg-1.0", "hello.txt", b"hello-lzma");
let mut compressed = Vec::new();
{
let options = LzmaOptions::default();
let mut writer =
LzmaWriter::new_use_header(&mut compressed, &options, Some(tar_buf.len() as u64))
.unwrap();
writer.write_all(&tar_buf).unwrap();
writer.finish().unwrap();
}
fs::write(&archive_path, compressed).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-lzma"
);
}
#[test]
fn test_extract_archive_tar_lzip_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.tar.lz");
let build_dir = tmp.path().join("build");
let tar_buf = simple_tar_bytes("pkg-1.0", "hello.txt", b"hello-lzip");
let mut compressed = Vec::new();
{
let mut writer = LzipWriter::new(&mut compressed, LzipOptions::default());
writer.write_all(&tar_buf).unwrap();
writer.finish().unwrap();
}
fs::write(&archive_path, compressed).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-lzip"
);
}
#[test]
fn test_extract_archive_cpio_roundtrip() {
let tmp = tempdir().unwrap();
let archive_path = tmp.path().join("pkg.cpio");
let build_dir = tmp.path().join("build");
let mut cpio = Vec::new();
write_cpio_newc_one_file(&mut cpio, "pkg-1.0/hello.txt", b"hello-cpio");
write_cpio_trailer(&mut cpio);
fs::write(&archive_path, cpio).unwrap();
let extracted = extract_archive(
&archive_path,
&test_spec(),
&test_source("pkg-1.0"),
&build_dir,
)
.unwrap();
assert_eq!(
fs::read_to_string(extracted.join("hello.txt")).unwrap(),
"hello-cpio"
);
}
#[test]
fn copy_file_preserve_metadata_keeps_mtime() {
let tmp = tempdir().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
std::fs::write(&src, b"hello").unwrap();
let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(946684800); // 2000-01-01 UTC
let ts = FileTime::from_system_time(fixed);
filetime::set_file_times(&src, ts, ts).unwrap();
copy_file_preserve_metadata(&src, &dst).unwrap();
let src_meta = std::fs::metadata(&src).unwrap();
let dst_meta = std::fs::metadata(&dst).unwrap();
assert_eq!(
FileTime::from_last_modification_time(&dst_meta),
FileTime::from_last_modification_time(&src_meta)
);
}
#[test]
fn copy_entry_fallback_preserves_symlink_when_target_is_missing() {
let tmp = tempdir().unwrap();
let src_dir = tmp.path().join("src");
let dst_dir = tmp.path().join("dst");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::create_dir_all(&dst_dir).unwrap();
let src_link = src_dir.join("RELEASE-NOTES");
std::os::unix::fs::symlink("doc/RelNotes/v1.47.3.txt", &src_link).unwrap();
// Simulate target already moved/removed before copying this symlink entry.
let src_target_dir = src_dir.join("doc");
std::fs::create_dir_all(src_target_dir.join("RelNotes")).unwrap();
std::fs::write(src_target_dir.join("RelNotes/v1.47.3.txt"), "notes").unwrap();
std::fs::remove_dir_all(&src_target_dir).unwrap();
let dst_link = dst_dir.join("RELEASE-NOTES");
let file_type = std::fs::symlink_metadata(&src_link).unwrap().file_type();
copy_entry_fallback(&src_link, &dst_link, file_type).unwrap();
assert!(std::fs::symlink_metadata(&src_link).is_err());
let dst_meta = std::fs::symlink_metadata(&dst_link).unwrap();
assert!(dst_meta.file_type().is_symlink());
assert_eq!(
std::fs::read_link(&dst_link).unwrap(),
PathBuf::from("doc/RelNotes/v1.47.3.txt")
);
}
fn write_cpio_newc_one_file(w: &mut Vec<u8>, name: &str, data: &[u8]) {
// magic + 13 fields of 8 hex chars each
fn h8(v: u64) -> String {
format!("{:08x}", v)
}
let namesize = name.len() + 1;
let filesize = data.len();
let mut header = Vec::new();
header.extend_from_slice(b"070701");
// ino, mode, uid, gid, nlink, mtime, filesize, devmajor, devminor, rdevmajor, rdevminor, namesize, check
header.extend_from_slice(h8(0).as_bytes()); // ino
header.extend_from_slice(h8(0o100644).as_bytes()); // mode regular file with perms
header.extend_from_slice(h8(0).as_bytes()); // uid
header.extend_from_slice(h8(0).as_bytes()); // gid
header.extend_from_slice(h8(1).as_bytes()); // nlink
header.extend_from_slice(h8(0).as_bytes()); // mtime
header.extend_from_slice(h8(filesize as u64).as_bytes());
header.extend_from_slice(h8(0).as_bytes()); // devmajor
header.extend_from_slice(h8(0).as_bytes()); // devminor
header.extend_from_slice(h8(0).as_bytes()); // rdevmajor
header.extend_from_slice(h8(0).as_bytes()); // rdevminor
header.extend_from_slice(h8(namesize as u64).as_bytes());
header.extend_from_slice(h8(0).as_bytes()); // check
w.extend_from_slice(&header);
w.extend_from_slice(name.as_bytes());
w.push(0);
// pad to 4
let pad = (4 - ((110 + namesize) % 4)) % 4;
for _ in 0..pad {
w.push(0);
}
// file data
w.extend_from_slice(data);
let dpad = (4 - (filesize % 4)) % 4;
for _ in 0..dpad {
w.push(0);
}
}
fn write_cpio_trailer(w: &mut Vec<u8>) {
write_cpio_newc_one_file(w, "TRAILER!!!", &[]);
}
#[test]
fn test_extract_rpm_roundtrip() {
let tmp = tempdir().unwrap();
let rpm_path = tmp.path().join("test.rpm");
let extract_dir = tmp.path().join("out-rpm");
// build cpio newc stream with one file
let mut cpio = Vec::new();
write_cpio_newc_one_file(&mut cpio, "usr/bin/hello-rpm", b"hello-rpm");
write_cpio_trailer(&mut cpio);
// gzip compress
let mut gz = Vec::new();
{
let mut enc = flate2::write::GzEncoder::new(&mut gz, flate2::Compression::default());
enc.write_all(&cpio).unwrap();
enc.finish().unwrap();
}
// write fake rpm: some header bytes then gz payload
{
let mut f = File::create(&rpm_path).unwrap();
f.write_all(b"RPMHEAD").unwrap();
f.write_all(&gz).unwrap();
}
fs::create_dir_all(&extract_dir).unwrap();
extract_rpm(&rpm_path, &extract_dir).unwrap();
assert!(extract_dir.join("usr/bin/hello-rpm").exists());
}
Executable → Regular
+1 -376
View File
@@ -823,379 +823,4 @@ fn ensure_prompt_terminal(url: &str) -> std::result::Result<(), git2::Error> {
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn commit_file(repo: &Repository, workdir: &Path, rel: &str, data: &str) -> Oid {
let full_path = workdir.join(rel);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&full_path, data).unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new(rel)).unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let sig = git2::Signature::now("depot-test", "depot@example.test").unwrap();
let mut parents = Vec::new();
if let Ok(head) = repo.head()
&& let Some(oid) = head.target()
{
parents.push(repo.find_commit(oid).unwrap());
}
let parent_refs: Vec<&git2::Commit<'_>> = parents.iter().collect();
repo.commit(Some("HEAD"), &sig, &sig, "test", &tree, &parent_refs)
.unwrap()
}
#[test]
fn checkout_rev_head_falls_back_to_remote_branch_when_local_head_is_invalid() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello");
repo.reference(
"refs/remotes/origin/main",
commit_oid,
true,
"test setup: remote tracking ref",
)
.unwrap();
repo.reference_symbolic(
"HEAD",
"refs/heads/HEAD",
true,
"test setup: break local HEAD",
)
.unwrap();
checkout_rev(&repo, "HEAD").unwrap();
assert!(repo.head_detached().unwrap());
assert_eq!(repo.head().unwrap().target().unwrap(), commit_oid);
}
#[test]
fn checkout_rev_resolves_named_branch_from_remote_tracking_refs() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "src/main.rs", "fn main() {}");
repo.reference(
"refs/remotes/origin/feature",
commit_oid,
true,
"test setup: remote feature branch",
)
.unwrap();
checkout_rev(&repo, "feature").unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), commit_oid);
}
#[test]
fn apply_cherry_picks_applies_commit_in_order() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let base = commit_file(&repo, &workdir, "README", "base");
let picked = commit_file(&repo, &workdir, "README", "picked");
checkout_rev(&repo, &base.to_string()).unwrap();
apply_cherry_picks(&repo, &[picked.to_string()]).unwrap();
let head = repo.head().unwrap().target().unwrap();
assert_eq!(head, picked);
assert_eq!(
std::fs::read_to_string(workdir.join("README")).unwrap(),
"picked"
);
}
#[test]
fn apply_cherry_picks_errors_for_unknown_rev() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let base = commit_file(&repo, &workdir, "README", "base");
checkout_rev(&repo, &base.to_string()).unwrap();
let err = apply_cherry_picks(&repo, &["deadbeef".to_string()])
.expect_err("unknown cherry-pick rev should fail");
assert!(
err.to_string()
.contains("Could not resolve cherry-pick rev")
);
}
#[test]
fn fetch_attempts_for_head_prefers_heads_only_before_full_fetch() {
let attempts = fetch_attempts_for_rev("HEAD");
assert!(matches!(
attempts.as_slice(),
[FetchAttempt::HeadRefs, FetchAttempt::FullRefs]
));
}
#[test]
fn fetch_attempts_for_named_revision_try_tag_then_branch_then_fallback() {
let attempts = fetch_attempts_for_rev("v1.2.3");
assert_eq!(attempts.len(), 3);
assert!(
matches!(&attempts[0], FetchAttempt::Tag(tag) if tag == "+refs/tags/v1.2.3:refs/tags/v1.2.3")
);
assert!(
matches!(&attempts[1], FetchAttempt::Branch(branch) if branch == "+refs/heads/v1.2.3:refs/heads/v1.2.3")
);
assert!(matches!(attempts[2], FetchAttempt::FullRefs));
}
#[test]
fn fetch_attempts_for_oid_use_full_fetch_only() {
let attempts = fetch_attempts_for_rev("0123456789abcdef");
assert!(matches!(attempts.as_slice(), [FetchAttempt::FullRefs]));
}
#[test]
fn full_fetch_attempt_includes_heads_and_tags_refspecs() {
let refspecs = FetchAttempt::FullRefs.refspecs();
assert_eq!(refspecs, vec![ALL_HEADS_REFSPEC, ALL_TAGS_REFSPEC]);
}
#[test]
fn should_skip_fetch_for_cached_tag_revisions() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello");
let tag_target = repo.find_object(commit_oid, None).unwrap();
repo.tag_lightweight("v1.0.0", &tag_target, false).unwrap();
assert!(should_skip_fetch_for_cached_revs(&repo, "v1.0.0", &[]));
assert!(!should_skip_fetch_for_cached_revs(&repo, "main", &[]));
assert!(!should_skip_fetch_for_cached_revs(&repo, "HEAD", &[]));
}
#[test]
fn should_not_skip_fetch_when_cherry_pick_rev_is_missing() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello");
let tag_target = repo.find_object(commit_oid, None).unwrap();
repo.tag_lightweight("v1.0.0", &tag_target, false).unwrap();
assert!(!should_skip_fetch_for_cached_revs(
&repo,
"v1.0.0",
&[String::from("deadbeef")]
));
}
#[test]
fn git_operation_should_continue_allows_normal_progress() {
let bar = ProgressBar::hidden();
crate::interrupts::reset();
assert!(git_operation_should_continue(Some(&bar)));
}
#[test]
fn ensure_valid_local_head_prefers_main_when_head_branch_is_missing() {
let temp = tempfile::tempdir().unwrap();
let bare_dir = temp.path().join("bare.git");
let workdir = temp.path().join("work");
let repo = Repository::init_bare(&bare_dir).unwrap();
std::fs::create_dir_all(&workdir).unwrap();
let work_repo = Repository::init(&workdir).unwrap();
work_repo.set_head("refs/heads/main").unwrap();
commit_file(&work_repo, &workdir, "README", "hello");
let topic_branch = work_repo
.branch(
"topic",
&work_repo.head().unwrap().peel_to_commit().unwrap(),
false,
)
.unwrap();
drop(topic_branch);
let mut remote = work_repo
.remote("origin", bare_dir.to_str().unwrap())
.unwrap();
remote
.push(
&[
"refs/heads/main:refs/heads/main",
"refs/heads/topic:refs/heads/topic",
],
None,
)
.unwrap();
repo.set_head("refs/heads/missing").unwrap();
ensure_valid_local_head(&repo).unwrap();
assert_eq!(
repo.head().unwrap().resolve().unwrap().name(),
Ok("refs/heads/main")
);
}
#[test]
fn checkout_head_succeeds_with_bare_mirror_heads_only_fetch() {
let temp = tempfile::tempdir().unwrap();
let origin_dir = temp.path().join("origin.git");
let workdir = temp.path().join("work");
let cache_dir = temp.path().join("cache");
let checkout_dir = temp.path().join("checkout");
std::fs::create_dir_all(&workdir).unwrap();
let origin = Repository::init_bare(&origin_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
repo.set_head("refs/heads/main").unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello\n");
let mut remote = repo.remote("origin", origin_dir.to_str().unwrap()).unwrap();
remote
.push(&["refs/heads/main:refs/heads/main"], None)
.unwrap();
origin.set_head("refs/heads/main").unwrap();
let origin_url = url::Url::from_file_path(&origin_dir).unwrap().to_string();
checkout(
&origin_url,
"HEAD",
&checkout_dir,
&cache_dir,
"test-pkg",
&[],
)
.unwrap();
let checkout_repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(checkout_repo.head().unwrap().target(), Some(commit_oid));
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"hello\n"
);
}
#[test]
fn checkout_fetches_cherry_pick_revs_after_tag_checkout_resolves() {
let temp = tempfile::tempdir().unwrap();
let origin_dir = temp.path().join("origin.git");
let workdir = temp.path().join("work");
let cache_dir = temp.path().join("cache");
let checkout_dir = temp.path().join("checkout");
std::fs::create_dir_all(&workdir).unwrap();
let origin = Repository::init_bare(&origin_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
repo.set_head("refs/heads/main").unwrap();
let base = commit_file(&repo, &workdir, "README", "base\n");
let release_target = repo.find_object(base, None).unwrap();
repo.tag_lightweight("v1.0.0", &release_target, false)
.unwrap();
let base_commit = repo.find_commit(base).unwrap();
repo.branch("topic", &base_commit, false).unwrap();
repo.set_head("refs/heads/topic").unwrap();
let mut branch_checkout = git2::build::CheckoutBuilder::new();
branch_checkout.force();
repo.checkout_head(Some(&mut branch_checkout)).unwrap();
let cherry_pick = commit_file(&repo, &workdir, "TOPIC", "topic\n");
let mut remote = repo.remote("origin", origin_dir.to_str().unwrap()).unwrap();
remote
.push(
&[
"refs/heads/main:refs/heads/main",
"refs/heads/topic:refs/heads/topic",
"refs/tags/v1.0.0:refs/tags/v1.0.0",
],
None,
)
.unwrap();
origin.set_head("refs/heads/main").unwrap();
let origin_url = url::Url::from_file_path(&origin_dir).unwrap().to_string();
checkout(
&origin_url,
"v1.0.0",
&checkout_dir,
&cache_dir,
"test-pkg",
&[cherry_pick.to_string()],
)
.unwrap();
let checkout_repo = Repository::open(&checkout_dir).unwrap();
assert_ne!(checkout_repo.head().unwrap().target(), Some(base));
assert_eq!(
std::fs::read_to_string(checkout_dir.join("TOPIC")).unwrap(),
"topic\n"
);
}
#[test]
fn checkout_resolves_annotated_tags_from_remote() {
let temp = tempfile::tempdir().unwrap();
let origin_dir = temp.path().join("origin.git");
let workdir = temp.path().join("work");
let cache_dir = temp.path().join("cache");
let checkout_dir = temp.path().join("checkout");
std::fs::create_dir_all(&workdir).unwrap();
let origin = Repository::init_bare(&origin_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
repo.set_head("refs/heads/main").unwrap();
let release_commit = commit_file(&repo, &workdir, "README", "release\n");
let release_target = repo.find_object(release_commit, None).unwrap();
let sig = git2::Signature::now("depot-test", "depot@example.test").unwrap();
repo.tag("v1.0.0", &release_target, &sig, "release tag", false)
.unwrap();
let mut remote = repo.remote("origin", origin_dir.to_str().unwrap()).unwrap();
remote
.push(
&[
"refs/heads/main:refs/heads/main",
"refs/tags/v1.0.0:refs/tags/v1.0.0",
],
None,
)
.unwrap();
origin.set_head("refs/heads/main").unwrap();
let origin_url = url::Url::from_file_path(&origin_dir).unwrap().to_string();
checkout(
&origin_url,
"v1.0.0",
&checkout_dir,
&cache_dir,
"test-pkg",
&[],
)
.unwrap();
let checkout_repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(checkout_repo.head().unwrap().target(), Some(release_commit));
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"release\n"
);
}
}
mod tests;
+374
View File
@@ -0,0 +1,374 @@
use super::*;
use std::path::Path;
fn commit_file(repo: &Repository, workdir: &Path, rel: &str, data: &str) -> Oid {
let full_path = workdir.join(rel);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&full_path, data).unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new(rel)).unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let sig = git2::Signature::now("depot-test", "depot@example.test").unwrap();
let mut parents = Vec::new();
if let Ok(head) = repo.head()
&& let Some(oid) = head.target()
{
parents.push(repo.find_commit(oid).unwrap());
}
let parent_refs: Vec<&git2::Commit<'_>> = parents.iter().collect();
repo.commit(Some("HEAD"), &sig, &sig, "test", &tree, &parent_refs)
.unwrap()
}
#[test]
fn checkout_rev_head_falls_back_to_remote_branch_when_local_head_is_invalid() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello");
repo.reference(
"refs/remotes/origin/main",
commit_oid,
true,
"test setup: remote tracking ref",
)
.unwrap();
repo.reference_symbolic(
"HEAD",
"refs/heads/HEAD",
true,
"test setup: break local HEAD",
)
.unwrap();
checkout_rev(&repo, "HEAD").unwrap();
assert!(repo.head_detached().unwrap());
assert_eq!(repo.head().unwrap().target().unwrap(), commit_oid);
}
#[test]
fn checkout_rev_resolves_named_branch_from_remote_tracking_refs() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "src/main.rs", "fn main() {}");
repo.reference(
"refs/remotes/origin/feature",
commit_oid,
true,
"test setup: remote feature branch",
)
.unwrap();
checkout_rev(&repo, "feature").unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), commit_oid);
}
#[test]
fn apply_cherry_picks_applies_commit_in_order() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let base = commit_file(&repo, &workdir, "README", "base");
let picked = commit_file(&repo, &workdir, "README", "picked");
checkout_rev(&repo, &base.to_string()).unwrap();
apply_cherry_picks(&repo, &[picked.to_string()]).unwrap();
let head = repo.head().unwrap().target().unwrap();
assert_eq!(head, picked);
assert_eq!(
std::fs::read_to_string(workdir.join("README")).unwrap(),
"picked"
);
}
#[test]
fn apply_cherry_picks_errors_for_unknown_rev() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let base = commit_file(&repo, &workdir, "README", "base");
checkout_rev(&repo, &base.to_string()).unwrap();
let err = apply_cherry_picks(&repo, &["deadbeef".to_string()])
.expect_err("unknown cherry-pick rev should fail");
assert!(
err.to_string()
.contains("Could not resolve cherry-pick rev")
);
}
#[test]
fn fetch_attempts_for_head_prefers_heads_only_before_full_fetch() {
let attempts = fetch_attempts_for_rev("HEAD");
assert!(matches!(
attempts.as_slice(),
[FetchAttempt::HeadRefs, FetchAttempt::FullRefs]
));
}
#[test]
fn fetch_attempts_for_named_revision_try_tag_then_branch_then_fallback() {
let attempts = fetch_attempts_for_rev("v1.2.3");
assert_eq!(attempts.len(), 3);
assert!(
matches!(&attempts[0], FetchAttempt::Tag(tag) if tag == "+refs/tags/v1.2.3:refs/tags/v1.2.3")
);
assert!(
matches!(&attempts[1], FetchAttempt::Branch(branch) if branch == "+refs/heads/v1.2.3:refs/heads/v1.2.3")
);
assert!(matches!(attempts[2], FetchAttempt::FullRefs));
}
#[test]
fn fetch_attempts_for_oid_use_full_fetch_only() {
let attempts = fetch_attempts_for_rev("0123456789abcdef");
assert!(matches!(attempts.as_slice(), [FetchAttempt::FullRefs]));
}
#[test]
fn full_fetch_attempt_includes_heads_and_tags_refspecs() {
let refspecs = FetchAttempt::FullRefs.refspecs();
assert_eq!(refspecs, vec![ALL_HEADS_REFSPEC, ALL_TAGS_REFSPEC]);
}
#[test]
fn should_skip_fetch_for_cached_tag_revisions() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello");
let tag_target = repo.find_object(commit_oid, None).unwrap();
repo.tag_lightweight("v1.0.0", &tag_target, false).unwrap();
assert!(should_skip_fetch_for_cached_revs(&repo, "v1.0.0", &[]));
assert!(!should_skip_fetch_for_cached_revs(&repo, "main", &[]));
assert!(!should_skip_fetch_for_cached_revs(&repo, "HEAD", &[]));
}
#[test]
fn should_not_skip_fetch_when_cherry_pick_rev_is_missing() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("repo");
std::fs::create_dir_all(&workdir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello");
let tag_target = repo.find_object(commit_oid, None).unwrap();
repo.tag_lightweight("v1.0.0", &tag_target, false).unwrap();
assert!(!should_skip_fetch_for_cached_revs(
&repo,
"v1.0.0",
&[String::from("deadbeef")]
));
}
#[test]
fn git_operation_should_continue_allows_normal_progress() {
let bar = ProgressBar::hidden();
crate::interrupts::reset();
assert!(git_operation_should_continue(Some(&bar)));
}
#[test]
fn ensure_valid_local_head_prefers_main_when_head_branch_is_missing() {
let temp = tempfile::tempdir().unwrap();
let bare_dir = temp.path().join("bare.git");
let workdir = temp.path().join("work");
let repo = Repository::init_bare(&bare_dir).unwrap();
std::fs::create_dir_all(&workdir).unwrap();
let work_repo = Repository::init(&workdir).unwrap();
work_repo.set_head("refs/heads/main").unwrap();
commit_file(&work_repo, &workdir, "README", "hello");
let topic_branch = work_repo
.branch(
"topic",
&work_repo.head().unwrap().peel_to_commit().unwrap(),
false,
)
.unwrap();
drop(topic_branch);
let mut remote = work_repo
.remote("origin", bare_dir.to_str().unwrap())
.unwrap();
remote
.push(
&[
"refs/heads/main:refs/heads/main",
"refs/heads/topic:refs/heads/topic",
],
None,
)
.unwrap();
repo.set_head("refs/heads/missing").unwrap();
ensure_valid_local_head(&repo).unwrap();
assert_eq!(
repo.head().unwrap().resolve().unwrap().name(),
Ok("refs/heads/main")
);
}
#[test]
fn checkout_head_succeeds_with_bare_mirror_heads_only_fetch() {
let temp = tempfile::tempdir().unwrap();
let origin_dir = temp.path().join("origin.git");
let workdir = temp.path().join("work");
let cache_dir = temp.path().join("cache");
let checkout_dir = temp.path().join("checkout");
std::fs::create_dir_all(&workdir).unwrap();
let origin = Repository::init_bare(&origin_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
repo.set_head("refs/heads/main").unwrap();
let commit_oid = commit_file(&repo, &workdir, "README", "hello\n");
let mut remote = repo.remote("origin", origin_dir.to_str().unwrap()).unwrap();
remote
.push(&["refs/heads/main:refs/heads/main"], None)
.unwrap();
origin.set_head("refs/heads/main").unwrap();
let origin_url = url::Url::from_file_path(&origin_dir).unwrap().to_string();
checkout(
&origin_url,
"HEAD",
&checkout_dir,
&cache_dir,
"test-pkg",
&[],
)
.unwrap();
let checkout_repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(checkout_repo.head().unwrap().target(), Some(commit_oid));
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"hello\n"
);
}
#[test]
fn checkout_fetches_cherry_pick_revs_after_tag_checkout_resolves() {
let temp = tempfile::tempdir().unwrap();
let origin_dir = temp.path().join("origin.git");
let workdir = temp.path().join("work");
let cache_dir = temp.path().join("cache");
let checkout_dir = temp.path().join("checkout");
std::fs::create_dir_all(&workdir).unwrap();
let origin = Repository::init_bare(&origin_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
repo.set_head("refs/heads/main").unwrap();
let base = commit_file(&repo, &workdir, "README", "base\n");
let release_target = repo.find_object(base, None).unwrap();
repo.tag_lightweight("v1.0.0", &release_target, false)
.unwrap();
let base_commit = repo.find_commit(base).unwrap();
repo.branch("topic", &base_commit, false).unwrap();
repo.set_head("refs/heads/topic").unwrap();
let mut branch_checkout = git2::build::CheckoutBuilder::new();
branch_checkout.force();
repo.checkout_head(Some(&mut branch_checkout)).unwrap();
let cherry_pick = commit_file(&repo, &workdir, "TOPIC", "topic\n");
let mut remote = repo.remote("origin", origin_dir.to_str().unwrap()).unwrap();
remote
.push(
&[
"refs/heads/main:refs/heads/main",
"refs/heads/topic:refs/heads/topic",
"refs/tags/v1.0.0:refs/tags/v1.0.0",
],
None,
)
.unwrap();
origin.set_head("refs/heads/main").unwrap();
let origin_url = url::Url::from_file_path(&origin_dir).unwrap().to_string();
checkout(
&origin_url,
"v1.0.0",
&checkout_dir,
&cache_dir,
"test-pkg",
&[cherry_pick.to_string()],
)
.unwrap();
let checkout_repo = Repository::open(&checkout_dir).unwrap();
assert_ne!(checkout_repo.head().unwrap().target(), Some(base));
assert_eq!(
std::fs::read_to_string(checkout_dir.join("TOPIC")).unwrap(),
"topic\n"
);
}
#[test]
fn checkout_resolves_annotated_tags_from_remote() {
let temp = tempfile::tempdir().unwrap();
let origin_dir = temp.path().join("origin.git");
let workdir = temp.path().join("work");
let cache_dir = temp.path().join("cache");
let checkout_dir = temp.path().join("checkout");
std::fs::create_dir_all(&workdir).unwrap();
let origin = Repository::init_bare(&origin_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
repo.set_head("refs/heads/main").unwrap();
let release_commit = commit_file(&repo, &workdir, "README", "release\n");
let release_target = repo.find_object(release_commit, None).unwrap();
let sig = git2::Signature::now("depot-test", "depot@example.test").unwrap();
repo.tag("v1.0.0", &release_target, &sig, "release tag", false)
.unwrap();
let mut remote = repo.remote("origin", origin_dir.to_str().unwrap()).unwrap();
remote
.push(
&[
"refs/heads/main:refs/heads/main",
"refs/tags/v1.0.0:refs/tags/v1.0.0",
],
None,
)
.unwrap();
origin.set_head("refs/heads/main").unwrap();
let origin_url = url::Url::from_file_path(&origin_dir).unwrap().to_string();
checkout(
&origin_url,
"v1.0.0",
&checkout_dir,
&cache_dir,
"test-pkg",
&[],
)
.unwrap();
let checkout_repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(checkout_repo.head().unwrap().target(), Some(release_commit));
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"release\n"
);
}
Executable → Regular
+1 -491
View File
@@ -715,494 +715,4 @@ fn checkout_hg(url: &str, rev: &str, checkout_dir: &Path) -> Result<()> {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, ManualSource, PackageInfo,
PackageSpec, Source,
};
use git2::{Oid, Repository};
use std::path::Path;
fn commit_file(repo: &Repository, workdir: &Path, rel: &str, data: &str) -> Oid {
let full_path = workdir.join(rel);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&full_path, data).unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new(rel)).unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let sig = git2::Signature::now("depot-test", "depot@example.test").unwrap();
let mut parents = Vec::new();
if let Ok(head) = repo.head()
&& let Some(oid) = head.target()
{
parents.push(repo.find_commit(oid).unwrap());
}
let parent_refs: Vec<&git2::Commit<'_>> = parents.iter().collect();
repo.commit(Some("HEAD"), &sig, &sig, "test", &tree, &parent_refs)
.unwrap()
}
fn make_git_source_spec(source_url: String, extract_dir: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: source_url,
sha256: "skip".into(),
extract_dir: extract_dir.into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
fn make_remote_git_repo() -> (tempfile::TempDir, String, Oid, Oid) {
let tmp = tempfile::tempdir().unwrap();
let remote_dir = tmp.path().join("origin.git");
let workdir = tmp.path().join("work");
Repository::init_bare(&remote_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let tagged = commit_file(&repo, &workdir, "README", "tagged\n");
let tag_target = repo.find_object(tagged, None).unwrap();
repo.tag_lightweight("v1.0.0", &tag_target, false).unwrap();
let hashed = commit_file(&repo, &workdir, "README", "hashed\n");
let branch_ref = repo.head().unwrap().name().unwrap().to_string();
let mut remote = repo.remote("origin", remote_dir.to_str().unwrap()).unwrap();
let push_specs = [
format!("{branch_ref}:{branch_ref}"),
"refs/tags/v1.0.0:refs/tags/v1.0.0".to_string(),
];
let push_spec_refs: Vec<&String> = push_specs.iter().collect();
remote.push(&push_spec_refs, None).unwrap();
let remote_url = url::Url::from_file_path(&remote_dir).unwrap().to_string();
(tmp, remote_url, tagged, hashed)
}
fn mk_spec_with_manuals(spec_dir: PathBuf, manuals: Vec<ManualSource>) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: manuals,
source: vec![Source {
url: "https://example.com/src.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "src".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir,
}
}
#[test]
fn split_git_url_accepts_git_with_rev() {
let (base, rev) = split_git_url("https://example.com/repo.git#v1.2.3").unwrap();
assert_eq!(base, "https://example.com/repo.git");
assert_eq!(rev, "v1.2.3");
}
#[test]
fn split_git_url_accepts_bare_git_url() {
let (base, rev) = split_git_url("https://example.com/repo.git").unwrap();
assert_eq!(base, "https://example.com/repo.git");
assert_eq!(rev, "HEAD");
}
#[test]
fn split_git_url_accepts_bare_git_scheme_url() {
let (base, rev) = split_git_url("git://git.suckless.org/ubase").unwrap();
assert_eq!(base, "git://git.suckless.org/ubase");
assert_eq!(rev, "HEAD");
}
#[test]
fn split_git_url_rejects_archive_urls() {
assert!(split_git_url("https://example.com/foo.tar.gz#deadbeef").is_none());
assert!(split_git_url("https://example.com/foo.zip#v1").is_none());
}
#[test]
fn split_git_url_empty_rev_defaults_to_head() {
let (base, rev) = split_git_url("https://example.com/repo.git#").unwrap();
assert_eq!(base, "https://example.com/repo.git");
assert_eq!(rev, "HEAD");
}
#[test]
fn split_git_url_accepts_expanded_tag_or_hash_revision() {
let spec = PackageSpec {
package: PackageInfo {
name: "json".into(),
real_name: None,
version: "3.11.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://github.com/nlohmann/json.git#v$version".into(),
sha256: "skip".into(),
extract_dir: "json-$version".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let expanded = spec.expand_vars(&spec.source[0].url);
let (base, rev) = split_git_url(&expanded).unwrap();
assert_eq!(base, "https://github.com/nlohmann/json.git");
assert_eq!(rev, "v3.11.3");
let (base, rev) =
split_git_url("https://github.com/nlohmann/json.git#0123456789abcdef").unwrap();
assert_eq!(base, "https://github.com/nlohmann/json.git");
assert_eq!(rev, "0123456789abcdef");
}
#[test]
fn split_hg_url_accepts_revision_and_default_tip() {
let (base, rev) = split_hg_url("hg+https://hg.example.test/repo#v1").unwrap();
assert_eq!(base, "https://hg.example.test/repo");
assert_eq!(rev, "v1");
let (base, rev) = split_hg_url("hg+https://hg.example.test/repo").unwrap();
assert_eq!(base, "https://hg.example.test/repo");
assert_eq!(rev, "tip");
}
#[test]
fn prepare_one_rejects_cherry_pick_for_non_git_sources() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
let mut spec = mk_spec_with_manuals(PathBuf::from("."), Vec::new());
spec.source[0].url = "https://example.com/foo.tar.gz".into();
spec.source[0].cherry_pick = vec!["deadbeef".into()];
let err = prepare_one(&spec, &spec.source[0], &cache_dir, &build_dir)
.expect_err("non-git source with cherry_pick must be rejected");
assert!(
err.to_string()
.contains("source.cherry_pick is only supported for git sources")
);
}
#[test]
fn prepare_one_checks_out_git_tag_revision() {
let (_tmp, remote_url, tagged, _hashed) = make_remote_git_repo();
let cache_dir = tempfile::tempdir().unwrap();
let build_dir = tempfile::tempdir().unwrap();
let spec = make_git_source_spec(format!("{remote_url}#v1.0.0"), "src-tag");
let checkout_dir =
prepare_one(&spec, &spec.source[0], cache_dir.path(), build_dir.path()).unwrap();
let repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), tagged);
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"tagged\n"
);
}
#[test]
fn prepare_one_checks_out_git_commit_hash_revision() {
let (_tmp, remote_url, _tagged, hashed) = make_remote_git_repo();
let cache_dir = tempfile::tempdir().unwrap();
let build_dir = tempfile::tempdir().unwrap();
let spec = make_git_source_spec(format!("{remote_url}#{hashed}"), "src-hash");
let checkout_dir =
prepare_one(&spec, &spec.source[0], cache_dir.path(), build_dir.path()).unwrap();
let repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), hashed);
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"hashed\n"
);
}
#[test]
fn verify_file_hash_accepts_multiple_algorithms() {
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"abc").unwrap();
let sha256_hex = {
let mut h = Sha256::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
let sha512_hex = {
let mut h = Sha512::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
let sha1_hex = {
let mut h = Sha1::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
let md5_hex = format!("{:x}", md5::compute(b"abc"));
let b2_hex = b2sum_rust::Blake2bSum::new(64)
.read(tmp.path())
.to_ascii_lowercase();
assert!(verify_file_hash(tmp.path(), &sha256_hex).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("sha256:{}", sha256_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("sha512:{}", sha512_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("sha1:{}", sha1_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("md5:{}", md5_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("b2:{}", b2_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("b2sum:{}", b2_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!(":{}", sha256_hex)).unwrap());
assert!(!verify_file_hash(tmp.path(), "md5:deadbeef").unwrap());
}
#[test]
fn build_blocking_client_with_and_without_timeout() {
use std::time::Duration;
let ua = "depot/test";
let c1 = build_blocking_client(ua, None).expect("client build failed");
assert!(c1.get("https://example.com").build().is_ok());
let c2 =
build_blocking_client(ua, Some(Duration::from_secs(5))).expect("client build failed");
assert!(c2.get("https://example.com").build().is_ok());
}
#[test]
fn copy_manual_sources_local_file_mode() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::write(spec_dir.join("manual.patch"), "patch-data").unwrap();
let spec = mk_spec_with_manuals(
spec_dir.clone(),
vec![ManualSource {
file: Some("manual.patch".into()),
files: Vec::new(),
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("manual.patch")).unwrap(),
"patch-data"
);
}
#[test]
fn copy_manual_sources_url_mode_file_scheme() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&spec_dir).unwrap();
let remote_file = tmp.path().join("remote-resource.txt");
std::fs::write(&remote_file, "remote-data").unwrap();
let url = format!("file://{}", remote_file.display());
let spec = mk_spec_with_manuals(
spec_dir,
vec![ManualSource {
file: None,
files: Vec::new(),
url: Some(url),
urls: Vec::new(),
sha256: Some("skip".into()),
dest: Some("assets/manual.txt".into()),
}],
);
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("assets/manual.txt")).unwrap(),
"remote-data"
);
}
#[test]
fn preflight_manual_sources_accepts_git_url() {
let (_tmp, remote_url, _tagged, hashed) = make_remote_git_repo();
let spec = mk_spec_with_manuals(
PathBuf::from("."),
vec![ManualSource {
file: None,
files: Vec::new(),
url: Some(format!("{remote_url}#{hashed}")),
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
let cache_dir = tempfile::tempdir().unwrap();
preflight_manual_sources(&spec, cache_dir.path()).unwrap();
}
#[test]
fn copy_manual_sources_git_url_mode_checks_out_repository() {
let (_tmp, remote_url, _tagged, hashed) = make_remote_git_repo();
let spec = mk_spec_with_manuals(
PathBuf::from("."),
vec![ManualSource {
file: None,
files: Vec::new(),
url: Some(format!("{remote_url}#{hashed}")),
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
let cache_dir = tempfile::tempdir().unwrap();
let build_dir = tempfile::tempdir().unwrap();
copy_manual_sources(&spec, cache_dir.path(), build_dir.path()).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.path().join("origin/README")).unwrap(),
"hashed\n"
);
}
#[test]
fn copy_manual_sources_multi_files_in_one_block() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(spec_dir.join("pam")).unwrap();
std::fs::write(spec_dir.join("pam/other"), "other").unwrap();
std::fs::write(spec_dir.join("pam/system-auth"), "auth").unwrap();
let spec = mk_spec_with_manuals(
spec_dir.clone(),
vec![ManualSource {
file: None,
files: vec!["pam/other".into(), "pam/system-auth".into()],
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("pam/other")).unwrap(),
"other"
);
assert_eq!(
std::fs::read_to_string(build_dir.join("pam/system-auth")).unwrap(),
"auth"
);
}
#[test]
fn copy_manual_sources_expands_carch_in_files_entries() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::write(spec_dir.join("build.sh"), "#!/bin/sh\necho hi\n").unwrap();
std::fs::write(spec_dir.join("config.armv7"), "armv7-config").unwrap();
let mut spec = mk_spec_with_manuals(
spec_dir.clone(),
vec![ManualSource {
file: None,
files: vec!["build.sh".into(), "config.$CARCH".into()],
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
spec.build.flags.carch = "armv7".into();
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("build.sh")).unwrap(),
"#!/bin/sh\necho hi\n"
);
assert_eq!(
std::fs::read_to_string(build_dir.join("config.armv7")).unwrap(),
"armv7-config"
);
}
}
mod tests;
+488
View File
@@ -0,0 +1,488 @@
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, ManualSource, PackageInfo,
PackageSpec, Source,
};
use git2::{Oid, Repository};
use std::path::Path;
fn commit_file(repo: &Repository, workdir: &Path, rel: &str, data: &str) -> Oid {
let full_path = workdir.join(rel);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&full_path, data).unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new(rel)).unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let sig = git2::Signature::now("depot-test", "depot@example.test").unwrap();
let mut parents = Vec::new();
if let Ok(head) = repo.head()
&& let Some(oid) = head.target()
{
parents.push(repo.find_commit(oid).unwrap());
}
let parent_refs: Vec<&git2::Commit<'_>> = parents.iter().collect();
repo.commit(Some("HEAD"), &sig, &sig, "test", &tree, &parent_refs)
.unwrap()
}
fn make_git_source_spec(source_url: String, extract_dir: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: source_url,
sha256: "skip".into(),
extract_dir: extract_dir.into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
fn make_remote_git_repo() -> (tempfile::TempDir, String, Oid, Oid) {
let tmp = tempfile::tempdir().unwrap();
let remote_dir = tmp.path().join("origin.git");
let workdir = tmp.path().join("work");
Repository::init_bare(&remote_dir).unwrap();
let repo = Repository::init(&workdir).unwrap();
let tagged = commit_file(&repo, &workdir, "README", "tagged\n");
let tag_target = repo.find_object(tagged, None).unwrap();
repo.tag_lightweight("v1.0.0", &tag_target, false).unwrap();
let hashed = commit_file(&repo, &workdir, "README", "hashed\n");
let branch_ref = repo.head().unwrap().name().unwrap().to_string();
let mut remote = repo.remote("origin", remote_dir.to_str().unwrap()).unwrap();
let push_specs = [
format!("{branch_ref}:{branch_ref}"),
"refs/tags/v1.0.0:refs/tags/v1.0.0".to_string(),
];
let push_spec_refs: Vec<&String> = push_specs.iter().collect();
remote.push(&push_spec_refs, None).unwrap();
let remote_url = url::Url::from_file_path(&remote_dir).unwrap().to_string();
(tmp, remote_url, tagged, hashed)
}
fn mk_spec_with_manuals(spec_dir: PathBuf, manuals: Vec<ManualSource>) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: manuals,
source: vec![Source {
url: "https://example.com/src.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "src".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir,
}
}
#[test]
fn split_git_url_accepts_git_with_rev() {
let (base, rev) = split_git_url("https://example.com/repo.git#v1.2.3").unwrap();
assert_eq!(base, "https://example.com/repo.git");
assert_eq!(rev, "v1.2.3");
}
#[test]
fn split_git_url_accepts_bare_git_url() {
let (base, rev) = split_git_url("https://example.com/repo.git").unwrap();
assert_eq!(base, "https://example.com/repo.git");
assert_eq!(rev, "HEAD");
}
#[test]
fn split_git_url_accepts_bare_git_scheme_url() {
let (base, rev) = split_git_url("git://git.suckless.org/ubase").unwrap();
assert_eq!(base, "git://git.suckless.org/ubase");
assert_eq!(rev, "HEAD");
}
#[test]
fn split_git_url_rejects_archive_urls() {
assert!(split_git_url("https://example.com/foo.tar.gz#deadbeef").is_none());
assert!(split_git_url("https://example.com/foo.zip#v1").is_none());
}
#[test]
fn split_git_url_empty_rev_defaults_to_head() {
let (base, rev) = split_git_url("https://example.com/repo.git#").unwrap();
assert_eq!(base, "https://example.com/repo.git");
assert_eq!(rev, "HEAD");
}
#[test]
fn split_git_url_accepts_expanded_tag_or_hash_revision() {
let spec = PackageSpec {
package: PackageInfo {
name: "json".into(),
real_name: None,
version: "3.11.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://github.com/nlohmann/json.git#v$version".into(),
sha256: "skip".into(),
extract_dir: "json-$version".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Custom,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let expanded = spec.expand_vars(&spec.source[0].url);
let (base, rev) = split_git_url(&expanded).unwrap();
assert_eq!(base, "https://github.com/nlohmann/json.git");
assert_eq!(rev, "v3.11.3");
let (base, rev) =
split_git_url("https://github.com/nlohmann/json.git#0123456789abcdef").unwrap();
assert_eq!(base, "https://github.com/nlohmann/json.git");
assert_eq!(rev, "0123456789abcdef");
}
#[test]
fn split_hg_url_accepts_revision_and_default_tip() {
let (base, rev) = split_hg_url("hg+https://hg.example.test/repo#v1").unwrap();
assert_eq!(base, "https://hg.example.test/repo");
assert_eq!(rev, "v1");
let (base, rev) = split_hg_url("hg+https://hg.example.test/repo").unwrap();
assert_eq!(base, "https://hg.example.test/repo");
assert_eq!(rev, "tip");
}
#[test]
fn prepare_one_rejects_cherry_pick_for_non_git_sources() {
let tmp = tempfile::tempdir().unwrap();
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
let mut spec = mk_spec_with_manuals(PathBuf::from("."), Vec::new());
spec.source[0].url = "https://example.com/foo.tar.gz".into();
spec.source[0].cherry_pick = vec!["deadbeef".into()];
let err = prepare_one(&spec, &spec.source[0], &cache_dir, &build_dir)
.expect_err("non-git source with cherry_pick must be rejected");
assert!(
err.to_string()
.contains("source.cherry_pick is only supported for git sources")
);
}
#[test]
fn prepare_one_checks_out_git_tag_revision() {
let (_tmp, remote_url, tagged, _hashed) = make_remote_git_repo();
let cache_dir = tempfile::tempdir().unwrap();
let build_dir = tempfile::tempdir().unwrap();
let spec = make_git_source_spec(format!("{remote_url}#v1.0.0"), "src-tag");
let checkout_dir =
prepare_one(&spec, &spec.source[0], cache_dir.path(), build_dir.path()).unwrap();
let repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), tagged);
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"tagged\n"
);
}
#[test]
fn prepare_one_checks_out_git_commit_hash_revision() {
let (_tmp, remote_url, _tagged, hashed) = make_remote_git_repo();
let cache_dir = tempfile::tempdir().unwrap();
let build_dir = tempfile::tempdir().unwrap();
let spec = make_git_source_spec(format!("{remote_url}#{hashed}"), "src-hash");
let checkout_dir =
prepare_one(&spec, &spec.source[0], cache_dir.path(), build_dir.path()).unwrap();
let repo = Repository::open(&checkout_dir).unwrap();
assert_eq!(repo.head().unwrap().target().unwrap(), hashed);
assert_eq!(
std::fs::read_to_string(checkout_dir.join("README")).unwrap(),
"hashed\n"
);
}
#[test]
fn verify_file_hash_accepts_multiple_algorithms() {
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"abc").unwrap();
let sha256_hex = {
let mut h = Sha256::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
let sha512_hex = {
let mut h = Sha512::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
let sha1_hex = {
let mut h = Sha1::new();
h.update(b"abc");
crate::hex::encode_lower(h.finalize())
};
let md5_hex = format!("{:x}", md5::compute(b"abc"));
let b2_hex = b2sum_rust::Blake2bSum::new(64)
.read(tmp.path())
.to_ascii_lowercase();
assert!(verify_file_hash(tmp.path(), &sha256_hex).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("sha256:{}", sha256_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("sha512:{}", sha512_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("sha1:{}", sha1_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("md5:{}", md5_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("b2:{}", b2_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!("b2sum:{}", b2_hex)).unwrap());
assert!(verify_file_hash(tmp.path(), &format!(":{}", sha256_hex)).unwrap());
assert!(!verify_file_hash(tmp.path(), "md5:deadbeef").unwrap());
}
#[test]
fn build_blocking_client_with_and_without_timeout() {
use std::time::Duration;
let ua = "depot/test";
let c1 = build_blocking_client(ua, None).expect("client build failed");
assert!(c1.get("https://example.com").build().is_ok());
let c2 = build_blocking_client(ua, Some(Duration::from_secs(5))).expect("client build failed");
assert!(c2.get("https://example.com").build().is_ok());
}
#[test]
fn copy_manual_sources_local_file_mode() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::write(spec_dir.join("manual.patch"), "patch-data").unwrap();
let spec = mk_spec_with_manuals(
spec_dir.clone(),
vec![ManualSource {
file: Some("manual.patch".into()),
files: Vec::new(),
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("manual.patch")).unwrap(),
"patch-data"
);
}
#[test]
fn copy_manual_sources_url_mode_file_scheme() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&spec_dir).unwrap();
let remote_file = tmp.path().join("remote-resource.txt");
std::fs::write(&remote_file, "remote-data").unwrap();
let url = format!("file://{}", remote_file.display());
let spec = mk_spec_with_manuals(
spec_dir,
vec![ManualSource {
file: None,
files: Vec::new(),
url: Some(url),
urls: Vec::new(),
sha256: Some("skip".into()),
dest: Some("assets/manual.txt".into()),
}],
);
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("assets/manual.txt")).unwrap(),
"remote-data"
);
}
#[test]
fn preflight_manual_sources_accepts_git_url() {
let (_tmp, remote_url, _tagged, hashed) = make_remote_git_repo();
let spec = mk_spec_with_manuals(
PathBuf::from("."),
vec![ManualSource {
file: None,
files: Vec::new(),
url: Some(format!("{remote_url}#{hashed}")),
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
let cache_dir = tempfile::tempdir().unwrap();
preflight_manual_sources(&spec, cache_dir.path()).unwrap();
}
#[test]
fn copy_manual_sources_git_url_mode_checks_out_repository() {
let (_tmp, remote_url, _tagged, hashed) = make_remote_git_repo();
let spec = mk_spec_with_manuals(
PathBuf::from("."),
vec![ManualSource {
file: None,
files: Vec::new(),
url: Some(format!("{remote_url}#{hashed}")),
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
let cache_dir = tempfile::tempdir().unwrap();
let build_dir = tempfile::tempdir().unwrap();
copy_manual_sources(&spec, cache_dir.path(), build_dir.path()).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.path().join("origin/README")).unwrap(),
"hashed\n"
);
}
#[test]
fn copy_manual_sources_multi_files_in_one_block() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(spec_dir.join("pam")).unwrap();
std::fs::write(spec_dir.join("pam/other"), "other").unwrap();
std::fs::write(spec_dir.join("pam/system-auth"), "auth").unwrap();
let spec = mk_spec_with_manuals(
spec_dir.clone(),
vec![ManualSource {
file: None,
files: vec!["pam/other".into(), "pam/system-auth".into()],
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("pam/other")).unwrap(),
"other"
);
assert_eq!(
std::fs::read_to_string(build_dir.join("pam/system-auth")).unwrap(),
"auth"
);
}
#[test]
fn copy_manual_sources_expands_carch_in_files_entries() {
let tmp = tempfile::tempdir().unwrap();
let spec_dir = tmp.path().join("spec");
let cache_dir = tmp.path().join("cache");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&spec_dir).unwrap();
std::fs::write(spec_dir.join("build.sh"), "#!/bin/sh\necho hi\n").unwrap();
std::fs::write(spec_dir.join("config.armv7"), "armv7-config").unwrap();
let mut spec = mk_spec_with_manuals(
spec_dir.clone(),
vec![ManualSource {
file: None,
files: vec!["build.sh".into(), "config.$CARCH".into()],
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
}],
);
spec.build.flags.carch = "armv7".into();
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
assert_eq!(
std::fs::read_to_string(build_dir.join("build.sh")).unwrap(),
"#!/bin/sh\necho hi\n"
);
assert_eq!(
std::fs::read_to_string(build_dir.join("config.armv7")).unwrap(),
"armv7-config"
);
}
+45
View File
@@ -0,0 +1,45 @@
use super::*;
/// Manifest containing files and directories for a package
#[derive(Debug, Clone)]
pub struct Manifest {
pub files: Vec<String>,
pub directories: Vec<String>,
}
/// Generate manifest with both files and directories
pub fn generate_manifest_with_dirs(destdir: &Path) -> Result<Manifest> {
let mut files = Vec::new();
let mut directories = Vec::new();
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
let rel_path = entry
.path()
.strip_prefix(destdir)?
.to_string_lossy()
.to_string();
// Skip the root (empty path)
if rel_path.is_empty() {
continue;
}
if is_skipped_install_path(&rel_path) {
continue;
}
let file_type = entry.file_type();
// Check for symlink first
if file_type.is_symlink() {
// Track symlinks as files (they get removed the same way)
files.push(rel_path);
} else if file_type.is_file() {
files.push(rel_path);
} else if file_type.is_dir() {
directories.push(rel_path);
}
}
Ok(Manifest { files, directories })
}
Executable → Regular
+5 -1799
View File
File diff suppressed because it is too large Load Diff
+1037
View File
File diff suppressed because it is too large Load Diff
+717
View File
@@ -0,0 +1,717 @@
use super::*;
#[derive(Debug)]
pub struct FsTransaction {
rootfs: PathBuf,
pub(super) tx_dir: PathBuf,
backed_up: Vec<String>,
created: Vec<String>,
relocated: Vec<String>,
removed: Vec<String>,
}
pub(super) fn is_directory_empty(path: &Path) -> Result<bool> {
let mut entries = fs::read_dir(path)
.with_context(|| format!("Failed to read directory {}", path.display()))?;
Ok(entries.next().transpose()?.is_none())
}
pub(super) fn backup_existing_path(src: &Path, backup_path: &Path, rel: &str) -> Result<()> {
let metadata = src
.symlink_metadata()
.with_context(|| format!("Failed to inspect existing path {}", rel))?;
if let Some(parent) = backup_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create backup dir {}", parent.display()))?;
}
if metadata.file_type().is_symlink() {
let target = fs::read_link(src)
.with_context(|| format!("Failed to read existing symlink target {}", rel))?;
std::os::unix::fs::symlink(&target, backup_path)
.with_context(|| format!("Failed to backup symlink {}", rel))?;
} else if metadata.file_type().is_dir() {
fs::create_dir_all(backup_path)
.with_context(|| format!("Failed to backup directory {}", rel))?;
apply_unix_mode(backup_path, &metadata)?;
} else {
fs::copy(src, backup_path).with_context(|| format!("Failed to backup file {}", rel))?;
}
Ok(())
}
pub(super) fn move_directory_contents(src_dir: &Path, dst_dir: &Path) -> Result<()> {
fs::create_dir_all(dst_dir)
.with_context(|| format!("Failed to create directory {}", dst_dir.display()))?;
for entry in
fs::read_dir(src_dir).with_context(|| format!("Failed to read {}", src_dir.display()))?
{
let entry = entry?;
move_tree_preserving_layout(&entry.path(), &dst_dir.join(entry.file_name()))?;
}
fs::remove_dir(src_dir).with_context(|| format!("Failed to remove {}", src_dir.display()))?;
Ok(())
}
pub(super) fn copy_tree_preserving_layout_no_overwrite(
src_root: &Path,
dst_root: &Path,
logical_root: &str,
created: &mut Vec<String>,
) -> Result<()> {
for entry in WalkDir::new(src_root).follow_links(false) {
let entry = entry
.with_context(|| format!("Failed to walk relocation tree {}", src_root.display()))?;
let src_path = entry.path();
let rel = src_path
.strip_prefix(src_root)
.with_context(|| format!("Failed to strip relocation root {}", src_root.display()))?;
if rel.as_os_str().is_empty() {
continue;
}
let dst_path = dst_root.join(rel);
let metadata = src_path
.symlink_metadata()
.with_context(|| format!("Failed to inspect {}", src_path.display()))?;
let file_type = metadata.file_type();
if file_type.is_dir() {
match dst_path.symlink_metadata() {
Ok(dst_meta) => {
if !dst_meta.file_type().is_dir() {
anyhow::bail!(
"Failed to replay relocated directory into {}: destination exists and is not a directory",
dst_path.display()
);
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(&dst_path).with_context(|| {
format!("Failed to create directory {}", dst_path.display())
})?;
apply_unix_mode(&dst_path, &metadata)?;
}
Err(err) => {
return Err(err)
.with_context(|| format!("Failed to inspect {}", dst_path.display()));
}
}
continue;
}
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory {}", parent.display()))?;
}
if let Ok(dst_metadata) = dst_path.symlink_metadata() {
if duplicate_staged_path_is_equivalent(src_path, &metadata, &dst_path, &dst_metadata)? {
continue;
}
anyhow::bail!(
"Failed to replay relocated path into {}: destination already exists",
dst_path.display()
);
}
if file_type.is_symlink() {
let target = fs::read_link(src_path)
.with_context(|| format!("Failed to read symlink {}", src_path.display()))?;
std::os::unix::fs::symlink(&target, &dst_path).with_context(|| {
format!(
"Failed to create relocated symlink {} -> {}",
dst_path.display(),
target.display()
)
})?;
} else {
fs::copy(src_path, &dst_path).with_context(|| {
format!(
"Failed to copy relocated path {} to {}",
src_path.display(),
dst_path.display()
)
})?;
apply_unix_mode(&dst_path, &metadata)?;
}
let logical = Path::new(logical_root).join(rel);
let logical = logical
.to_str()
.context("Relocated install paths must be valid UTF-8")?
.to_string();
created.push(logical);
}
Ok(())
}
pub(super) fn symlink_target_path_inside_rootfs(
rootfs: &Path,
link_rel: &str,
target: &Path,
) -> Result<Option<PathBuf>> {
let mut normalized = PathBuf::new();
if target.is_absolute() {
for component in target.components() {
match component {
Component::RootDir => {}
Component::CurDir => {}
Component::Normal(segment) => normalized.push(segment),
Component::ParentDir | Component::Prefix(_) => return Ok(None),
}
}
} else {
if let Some(parent) = Path::new(link_rel).parent() {
normalized.push(parent);
}
for component in target.components() {
match component {
Component::CurDir => {}
Component::Normal(segment) => normalized.push(segment),
Component::ParentDir => {
if !normalized.pop() {
return Ok(None);
}
}
Component::RootDir | Component::Prefix(_) => return Ok(None),
}
}
}
if normalized.as_os_str().is_empty() {
return Ok(None);
}
Ok(Some(rootfs.join(normalized)))
}
pub(super) fn can_relocate_directory_for_symlink_swap(
rootfs: &Path,
link_rel: &str,
symlink_path: &Path,
) -> Result<bool> {
let target = fs::read_link(symlink_path)
.with_context(|| format!("Failed to read symlink {}", symlink_path.display()))?;
let Some(target_path) = symlink_target_path_inside_rootfs(rootfs, link_rel, &target)? else {
return Ok(false);
};
match target_path.symlink_metadata() {
Ok(metadata) => Ok(metadata.file_type().is_dir()),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
Err(err) => Err(err)
.with_context(|| format!("Failed to inspect symlink target {}", target_path.display())),
}
}
pub(super) fn remove_path_in_place(path: &Path, rel: &str) -> Result<()> {
let metadata = path
.symlink_metadata()
.with_context(|| format!("Failed to inspect existing path {}", rel))?;
if metadata.file_type().is_dir() {
fs::remove_dir(path)
.with_context(|| format!("Failed to remove obsolete directory {}", rel))?;
} else {
fs::remove_file(path)
.with_context(|| format!("Failed to remove obsolete file/symlink {}", rel))?;
}
Ok(())
}
pub(super) fn backup_and_remove_obsolete_path(
tx: &mut FsTransaction,
rootfs: &Path,
rel: &str,
require_empty_dir: bool,
) -> Result<bool> {
let dest_path = rootfs.join(rel);
let metadata = match dest_path.symlink_metadata() {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(err) => {
return Err(err).with_context(|| {
format!("Failed to inspect obsolete path before removal: {}", rel)
});
}
};
if metadata.file_type().is_dir() {
let empty = is_directory_empty(&dest_path)?;
if !empty {
if require_empty_dir {
anyhow::bail!(
"Refusing to replace existing non-empty directory with packaged file/symlink: {}",
rel
);
}
return Ok(false);
}
}
let backup_path = tx.removed_backup_path(rel);
backup_existing_path(&dest_path, &backup_path, rel)?;
remove_path_in_place(&dest_path, rel)?;
tx.removed.push(rel.to_string());
Ok(true)
}
pub(super) fn remove_obsolete_children_for_dir(
tx: &mut FsTransaction,
rootfs: &Path,
dir_rel: &str,
remove_paths: &[String],
) -> Result<()> {
let prefix = format!("{dir_rel}/");
let mut nested_paths: Vec<&str> = remove_paths
.iter()
.filter_map(|path| path.strip_prefix(&prefix).map(|_| path.as_str()))
.collect();
nested_paths.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count()));
for rel in nested_paths {
let _ = backup_and_remove_obsolete_path(tx, rootfs, rel, false)?;
}
Ok(())
}
impl FsTransaction {
fn backup_path(&self, rel: &str) -> PathBuf {
self.tx_dir.join("backup").join(rel)
}
fn removed_backup_path(&self, rel: &str) -> PathBuf {
self.tx_dir.join("removed").join(rel)
}
fn relocated_path(&self, rel: &str) -> PathBuf {
self.tx_dir.join("relocated").join(rel)
}
fn relocate_directory_for_symlink_swap(&mut self, rel: &str) -> Result<()> {
let src = self.rootfs.join(rel);
let relocated = self.relocated_path(rel);
move_directory_contents(&src, &relocated)?;
self.relocated.push(rel.to_string());
Ok(())
}
fn replay_relocated_dir_if_present(&mut self, rel: &str) -> Result<()> {
let relocated = self.relocated_path(rel);
match relocated.symlink_metadata() {
Ok(meta) if meta.file_type().is_dir() => {}
Ok(_) => {
anyhow::bail!(
"Relocation staging path is not a directory: {}",
relocated.display()
);
}
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => {
return Err(err)
.with_context(|| format!("Failed to inspect {}", relocated.display()));
}
}
copy_tree_preserving_layout_no_overwrite(
&relocated,
&self.rootfs.join(rel),
rel,
&mut self.created,
)
}
fn restore_relocated_dir(&self, rel: &str) -> Result<()> {
let relocated = self.relocated_path(rel);
match relocated.symlink_metadata() {
Ok(meta) if meta.file_type().is_dir() => {}
Ok(_) => {
anyhow::bail!(
"Relocation staging path is not a directory: {}",
relocated.display()
);
}
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => {
return Err(err)
.with_context(|| format!("Failed to inspect {}", relocated.display()));
}
}
let dst = self.rootfs.join(rel);
match dst.symlink_metadata() {
Ok(meta) if meta.file_type().is_dir() => {}
Ok(_) => {
anyhow::bail!(
"Failed to restore relocated directory contents into {}: destination is not a directory",
dst.display()
);
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(&dst)
.with_context(|| format!("Failed to create directory {}", dst.display()))?;
}
Err(err) => {
return Err(err).with_context(|| format!("Failed to inspect {}", dst.display()));
}
}
move_directory_contents(&relocated, &dst)?;
cleanup_empty_parent_dirs(&self.tx_dir, &relocated)?;
Ok(())
}
fn restore_backup_entry(&self, src: &Path, dst: &Path) -> Result<()> {
let metadata = src
.symlink_metadata()
.with_context(|| format!("Failed to inspect backup entry {}", src.display()))?;
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create restore dir {}", parent.display()))?;
}
if metadata.file_type().is_dir() {
match dst.symlink_metadata() {
Ok(dst_meta) if dst_meta.file_type().is_dir() => {}
Ok(dst_meta) if dst_meta.file_type().is_symlink() => {
fs::remove_file(dst)
.with_context(|| format!("Failed to remove {}", dst.display()))?;
}
Ok(_) => {
fs::remove_file(dst)
.with_context(|| format!("Failed to remove {}", dst.display()))?;
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err)
.with_context(|| format!("Failed to inspect {}", dst.display()));
}
}
fs::create_dir_all(dst)
.with_context(|| format!("Failed to restore directory {}", dst.display()))?;
apply_unix_mode(dst, &metadata)?;
return Ok(());
}
let _ = fs::remove_file(dst);
match fs::rename(src, dst) {
Ok(()) => Ok(()),
Err(_) if metadata.file_type().is_symlink() => {
let target = fs::read_link(src)
.with_context(|| format!("Failed to read backup symlink {}", src.display()))?;
std::os::unix::fs::symlink(&target, dst)
.with_context(|| format!("Failed to restore symlink {}", dst.display()))
}
Err(_) => {
fs::copy(src, dst).with_context(|| {
format!(
"Failed to restore file {} from {}",
dst.display(),
src.display()
)
})?;
Ok(())
}
}
}
/// Roll back file operations performed by `install_atomic`.
pub fn rollback(&self) -> Result<()> {
// Remove files that were newly created
for rel in &self.created {
let dst = self.rootfs.join(rel);
let _ = fs::remove_file(dst);
}
// Restore overwritten paths first so relocated and removed children have their parent layout.
for rel in &self.backed_up {
let src = self.backup_path(rel);
let dst = self.rootfs.join(rel);
if src.symlink_metadata().is_ok() {
self.restore_backup_entry(&src, &dst)?;
}
}
for rel in &self.relocated {
self.restore_relocated_dir(rel)?;
}
// Restore removed files/directories.
for rel in &self.removed {
let src = self.removed_backup_path(rel);
let dst = self.rootfs.join(rel);
if src.symlink_metadata().is_ok() {
self.restore_backup_entry(&src, &dst)?;
}
}
Ok(())
}
/// Commit the transaction (delete backup directory).
pub fn commit(self) -> Result<()> {
let tx_base_dir = self.tx_dir.parent().map(Path::to_path_buf);
if self.tx_dir.exists() {
fs::remove_dir_all(&self.tx_dir)?;
}
if let Some(tx_base_dir) = tx_base_dir {
match fs::remove_dir(&tx_base_dir) {
Ok(()) => {}
Err(err)
if matches!(
err.kind(),
io::ErrorKind::DirectoryNotEmpty | io::ErrorKind::NotFound
) => {}
Err(err) => {
return Err(err).with_context(|| {
format!("Failed to remove tx dir {}", tx_base_dir.display())
});
}
}
}
Ok(())
}
}
/// Install staged files using a rollback-capable transaction.
///
/// This is used for both first-time installs and updates. For updates, pass a
/// list of relative paths to remove (old manifest minus new manifest).
pub fn install_atomic(
destdir: &Path,
rootfs: &Path,
tx_base_dir: &Path,
remove_paths: &[String],
keep_paths: &[String],
) -> Result<FsTransaction> {
let tx_base_dir = if rootfs != Path::new("/") && tx_base_dir.starts_with(rootfs) {
rootfs.join(".depot-tx")
} else {
tx_base_dir.to_path_buf()
};
let keep_rules: Vec<KeepMatcher> = keep_paths
.iter()
.map(|p| KeepMatcher::from_spec(p))
.collect::<Result<Vec<_>>>()?;
let keep_set: HashSet<String> = keep_rules
.iter()
.filter_map(|m| match m {
KeepMatcher::Exact(p) => Some(p.clone()),
KeepMatcher::Pattern(_) => None,
})
.collect();
let remove_set: HashSet<&str> = remove_paths.iter().map(String::as_str).collect();
fs::create_dir_all(&tx_base_dir)
.with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?;
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let pid = std::process::id();
let tx_dir = tx_base_dir.join(format!("tx-{}-{}", ts, pid));
let backup_dir = tx_dir.join("backup");
let removed_dir = tx_dir.join("removed");
fs::create_dir_all(&backup_dir)?;
fs::create_dir_all(&removed_dir)?;
let mut tx = FsTransaction {
rootfs: rootfs.to_path_buf(),
tx_dir,
backed_up: Vec::new(),
created: Vec::new(),
relocated: Vec::new(),
removed: Vec::new(),
};
let mut staged_paths = HashSet::new();
let mut installed_hardlinks: HashMap<HardlinkKey, String> = HashMap::new();
let result: Result<()> = (|| {
// First, create all directories from destdir (for packages with only directories)
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
let src_path = entry.path();
let file_type = entry.file_type();
if !file_type.is_dir() || src_path == destdir {
continue;
}
let rel_path = src_path
.strip_prefix(destdir)
.context("Failed to strip destdir prefix")?;
let rel_path_str = rel_path.to_string_lossy().to_string();
if is_skipped_install_path(&rel_path_str) {
continue;
}
let dest_path = rootfs.join(rel_path);
if !dest_path.exists() {
fs::create_dir_all(&dest_path)?;
apply_unix_mode(&dest_path, &src_path.symlink_metadata()?)?;
}
staged_paths.insert(rel_path_str);
}
// Copy in new files.
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
let src_path = entry.path();
let metadata = src_path
.symlink_metadata()
.context("Failed to get metadata")?;
let file_type = metadata.file_type();
// We want to install files AND symlinks (to anything)
if !file_type.is_file() && !file_type.is_symlink() {
continue;
}
let rel_path = src_path
.strip_prefix(destdir)
.context("Failed to strip destdir prefix")?
.to_string_lossy()
.to_string();
if is_skipped_install_path(&rel_path) {
continue;
}
let keep_match = if keep_set.contains(&rel_path) {
true
} else {
keep_rules.iter().any(|m| m.matches(&rel_path))
};
let keep_as_depotnew = keep_match && rootfs.join(&rel_path).exists();
let install_rel_path = if keep_as_depotnew {
format!("{}.depotnew", rel_path)
} else {
rel_path.clone()
};
let dest_path = rootfs.join(&install_rel_path);
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
if let Ok(dest_meta) = dest_path.symlink_metadata() {
let backup_path = tx.backup_path(&install_rel_path);
if dest_meta.file_type().is_dir() {
let can_relocate = file_type.is_symlink()
&& can_relocate_directory_for_symlink_swap(
rootfs,
&install_rel_path,
src_path,
)?;
if !remove_set.contains(install_rel_path.as_str()) && !can_relocate {
anyhow::bail!(
"Refusing to replace existing directory with packaged file/symlink: {}",
install_rel_path
);
}
backup_existing_path(&dest_path, &backup_path, &install_rel_path)?;
tx.backed_up.push(install_rel_path.clone());
remove_obsolete_children_for_dir(
&mut tx,
rootfs,
&install_rel_path,
remove_paths,
)?;
if file_type.is_symlink() {
tx.relocate_directory_for_symlink_swap(&install_rel_path)?;
}
} else {
backup_existing_path(&dest_path, &backup_path, &install_rel_path)?;
tx.backed_up.push(install_rel_path.clone());
}
} else {
tx.created.push(install_rel_path.clone());
}
// Install new file/symlink
// Remove destination if it exists (we backed it up) so we can overwrite
if let Ok(dest_meta) = dest_path.symlink_metadata() {
if dest_meta.file_type().is_dir() {
let relocated_for_symlink_swap =
tx.relocated.iter().any(|rel| rel == &install_rel_path);
if !remove_set.contains(install_rel_path.as_str())
&& !relocated_for_symlink_swap
{
anyhow::bail!(
"Refusing to replace existing directory with packaged file/symlink: {}",
install_rel_path
);
}
if !is_directory_empty(&dest_path)? {
anyhow::bail!(
"Refusing to replace existing non-empty directory with packaged file/symlink: {}",
install_rel_path
);
}
fs::remove_dir(&dest_path)?;
} else {
fs::remove_file(&dest_path)?;
}
}
if file_type.is_symlink() {
let target = fs::read_link(src_path)
.with_context(|| format!("Failed to read staged symlink {}", rel_path))?;
std::os::unix::fs::symlink(target, &dest_path)
.with_context(|| format!("Failed to create symlink: {}", install_rel_path))?;
tx.replay_relocated_dir_if_present(&install_rel_path)?;
} else {
let hardlink_key = if keep_as_depotnew {
None
} else {
hardlink_key(&metadata)
};
if let Some(first_rel_path) =
hardlink_key.and_then(|key| installed_hardlinks.get(&key))
{
let first_path = rootfs.join(first_rel_path);
fs::hard_link(&first_path, &dest_path).with_context(|| {
format!(
"Failed to install hardlink: {} -> {}",
install_rel_path, first_rel_path
)
})?;
} else {
fs::copy(src_path, &dest_path)
.with_context(|| format!("Failed to install: {}", install_rel_path))?;
apply_unix_mode(&dest_path, &metadata)?;
if let Some(key) = hardlink_key {
installed_hardlinks.insert(key, install_rel_path.clone());
}
}
}
staged_paths.insert(install_rel_path);
}
// Remove obsolete files/directories left behind by the previous version.
for rel in remove_paths {
if staged_paths.contains(rel) || tx.removed.iter().any(|removed| removed == rel) {
continue;
}
let _ = backup_and_remove_obsolete_path(&mut tx, rootfs, rel, false)?;
}
Ok(())
})();
if let Err(e) = result {
let _ = tx.rollback();
return Err(e);
}
Ok(tx)
}