Compare commits
7 Commits
d65115241f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b396a88a4b | |||
| 9bae0f8642 | |||
| fa6ef2ddc9 | |||
| 176738e931 | |||
| 9855b1e080 | |||
| 5223149372 | |||
| e9d369ddf3 |
Generated
+13
-13
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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}");
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user