diff --git a/Cargo.lock b/Cargo.lock index 9143d4c..a3d3857 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,15 @@ dependencies = [ "roff", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "cmov" version = "0.5.3" @@ -417,9 +426,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -934,7 +943,7 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -1385,9 +1394,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.4+1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7" dependencies = [ "cc", "libc", @@ -1403,6 +1412,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -1428,6 +1438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", + "cmake", "libc", "pkg-config", "vcpkg", @@ -1581,14 +1592,14 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.2.1", "openssl-sys", "schannel", "security-framework", @@ -1693,6 +1704,21 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.111" @@ -1701,6 +1727,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -1964,9 +1991,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2132,9 +2159,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2440,9 +2467,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", diff --git a/Cargo.toml b/Cargo.toml index 067efad..7258d8d 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,18 @@ edition = "2024" [lints.rust] warnings = "deny" +[features] +default = [] +# Prefer vendored/static native libraries while keeping libc dynamically linked. +static-except-libc = [ + "git2/vendored-libgit2", + "git2/vendored-openssl", + "git2/zlib-ng-compat", + "reqwest/native-tls-vendored", + "rusqlite/bundled", + "xz2/static", +] + [dependencies] anyhow = "1.0.102" ar = "0.9.0" @@ -20,7 +32,7 @@ semver = "1.0.28" serde = { version = "1.0.228", features = ["derive"] } yaml-rust2 = "0.11.0" sha2 = "0.11.0" -tar = "0.4.45" +tar = "0.4.46" tempfile = "=3.27.0" thiserror = "2.0.18" toml = "1.1.2" @@ -35,7 +47,7 @@ suppaftp = "8.0.2" minisign = "0.9.1" petgraph = "0.8.3" fd-lock = "4.0.4" -reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] } +reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "native-tls"] } filetime = "0.2.29" clap_complete = "4.6.5" clap_mangen = "0.3.0" diff --git a/build.rs b/build.rs index 8578bd9..ea7fa38 100644 --- a/build.rs +++ b/build.rs @@ -11,10 +11,41 @@ const BUILD_OPTION_KEYS: &[&str] = &[ ]; fn main() { + println!("cargo:rerun-if-env-changed=CC"); for key in BUILD_OPTION_KEYS { println!("cargo:rerun-if-env-changed={key}"); if let Ok(value) = std::env::var(key) { println!("cargo:rustc-env={key}={value}"); } } + + if std::env::var_os("CARGO_FEATURE_STATIC_EXCEPT_LIBC").is_some() + && std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") + { + let libgcc_eh = gcc_runtime_archive("libgcc_eh.a").unwrap_or_else(|| { + panic!("static-except-libc requires libgcc_eh.a for static libgcc linkage") + }); + println!("cargo:rustc-link-arg-bin=depot=-Wl,--whole-archive"); + println!("cargo:rustc-link-arg-bin=depot={libgcc_eh}"); + println!("cargo:rustc-link-arg-bin=depot=-Wl,--no-whole-archive"); + println!("cargo:rustc-link-arg-bin=depot=-static-libgcc"); + } +} + +fn gcc_runtime_archive(name: &str) -> Option { + let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into()); + let output = std::process::Command::new(compiler) + .arg(format!("-print-file-name={name}")) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let path = String::from_utf8(output.stdout).ok()?; + let path = path.trim(); + if path.is_empty() || path == name || !std::path::Path::new(path).exists() { + None + } else { + Some(path.to_string()) + } } diff --git a/contrib/depot.d/build.toml b/contrib/depot.d/build.toml index 6a055d9..7f66259 100644 --- a/contrib/depot.d/build.toml +++ b/contrib/depot.d/build.toml @@ -5,10 +5,17 @@ # apply everywhere without crowding the main /etc/depot.toml. # Toolchain selection. +# tool_dir = "/usr/bin" cc = "clang" -# cxx = "clang++" -# ar = "ar" -# ld = "ld.lld" +# cxx = "$TOOL_DIR/clang++" +# ar = "$TOOL_DIR/ar" +# ranlib = "$TOOL_DIR/ranlib" +# strip = "$TOOL_DIR/strip" +# ld = "$TOOL_DIR/ld.lld" +# nm = "$TOOL_DIR/nm" +# objcopy = "$TOOL_DIR/objcopy" +# objdump = "$TOOL_DIR/objdump" +# readelf = "$TOOL_DIR/readelf" # Compiler defaults. These are merged with package-level flags unless a package # explicitly replaces them. @@ -18,6 +25,8 @@ cflags += ["-pipe"] # Linker defaults. ldflags = ["-Wl,-O1"] +# Select a linker through compiler drivers such as clang/gcc. +# fuse_ld = "lld" # Link-time optimization policy. ltoflags = ["-flto=auto"] diff --git a/contrib/depot.toml b/contrib/depot.toml index f144296..a69dacc 100644 --- a/contrib/depot.toml +++ b/contrib/depot.toml @@ -16,15 +16,20 @@ prefix = "/usr" # Default toolchain commands. These are passed through to builders that honor # CC/CXX/AR and related variables. +# tool_dir = "/usr/bin" cc = "clang" ar = "ar" # Optional examples: -# cxx = "clang++" -# ld = "ld.lld" -# cpp = "cpp" -# strip = "strip" -# ranlib = "ranlib" +# cxx = "$TOOL_DIR/clang++" +# ld = "$TOOL_DIR/ld.lld" +# cpp = "$TOOL_DIR/cpp" +# strip = "$TOOL_DIR/strip" +# ranlib = "$TOOL_DIR/ranlib" +# nm = "$TOOL_DIR/nm" +# objcopy = "$TOOL_DIR/objcopy" +# objdump = "$TOOL_DIR/objdump" +# readelf = "$TOOL_DIR/readelf" [build.flags] # Baseline C compiler flags for packages that do not supply their own. @@ -39,6 +44,8 @@ cflags += ["-g"] # Optional C++ and linker defaults: # cxxflags = ["-O2"] ldflags = ["-Wl,-O1"] +# Select a linker through compiler drivers such as clang/gcc. +# fuse_ld = "lld" # Link-time optimization defaults. When use_lto is true, Depot will inject # ltoflags into the appropriate compiler and linker flag sets automatically. diff --git a/contrib/user.depot.toml.example b/contrib/user.depot.toml.example index 58988a5..d2becaf 100644 --- a/contrib/user.depot.toml.example +++ b/contrib/user.depot.toml.example @@ -10,8 +10,11 @@ prefix = "/usr/local" # Personal toolchain preference. +# tool_dir = "/usr/bin" cc = "clang" -# cxx = "clang++" +# cxx = "$TOOL_DIR/clang++" +# ranlib = "$TOOL_DIR/ranlib" +# strip = "$TOOL_DIR/strip" [build.flags] # User-local compiler preferences. @@ -22,6 +25,7 @@ carch = "x86_64" # Examples: # cxxflags = ["-O2", "-g"] # ldflags = ["-Wl,-O1"] +# fuse_ld = "lld" # makeflags = "-j4" # Optional install directory overrides for supported builders. diff --git a/src/bootstrap.rs b/src/bootstrap.rs index a27c0a2..7746a01 100644 --- a/src/bootstrap.rs +++ b/src/bootstrap.rs @@ -335,6 +335,7 @@ fn bootstrap_package_is_complete( fn bootstrap_required_payload_paths(package: &str) -> &'static [&'static str] { match package { "bmake" => &["system/binaries/bmake", "system/share/mk/sys.mk"], + "byacc" => &["system/binaries/yacc"], "llvm-clang-pass1" => &[ "system/tools/bin/llvm-config", "system/tools/bin/llvm-tblgen", @@ -348,6 +349,7 @@ fn bootstrap_required_payload_paths(package: &str) -> &'static [&'static str] { fn bootstrap_recipe_revision(package: &str) -> Option { match package { "bmake" => Some(2), + "byacc" => Some(1), "llvm" => Some(3), "ubase" => Some(1), _ => None, @@ -1584,7 +1586,7 @@ fn write_generated_recipe( } spec.push_str("no_strip = true\n"); spec.push_str( - "passthrough_env = [\"DEPOT_LBI_CHROOT\", \"DEPOT_LBI_CHROOT_ROOT\", \"DEPOT_LBI_DEPOT_EXE\", \"DEPOT_LBI_SYSROOT\", \"LWI_MAKE_FLAGS\", \"LWI_MAKE_JOBS\"]\n\n", + "passthrough_env = [\"DEPOT_LBI_CHROOT\", \"DEPOT_LBI_CHROOT_ROOT\", \"DEPOT_LBI_DEPOT_EXE\", \"DEPOT_LBI_SYSROOT\", \"LBI_CCACHE\", \"LWI_MAKE_FLAGS\", \"LWI_MAKE_JOBS\"]\n\n", ); if let Some(primary) = primary_source { @@ -1753,6 +1755,19 @@ fi script.push_str("export LWI_CFLAGS=\"${LWI_CFLAGS:-${CFLAGS:-}}\"\n"); script.push_str("export LWI_CXXFLAGS=\"${LWI_CXXFLAGS:-$LWI_CFLAGS}\"\n"); script.push_str("export LBI_CUSTOM_LDFLAGS=\"${LBI_CUSTOM_LDFLAGS:-${LDFLAGS:-}}\"\n\n"); + if uses_llvm_cmake_ccache(package) { + script.push_str( + r#"if [ -z "${LBI_CCACHE:-}" ]; then + LBI_CCACHE="$(command -v ccache)" || { + echo "depot: LLVM bootstrap requires ccache in PATH for the CMake compiler launcher" >&2 + exit 1 + } +fi +export LBI_CCACHE + +"#, + ); + } let uses_cross_toolchain = use_cross_toolchain_by_default(package); if uses_cross_toolchain { script.push_str( @@ -2112,6 +2127,8 @@ fn apply_bootstrap_package_overrides(package: &BookPackage, command: &str) -> St ensure_mandoc_postinstall_uses_staged_man(command) } else if package.name == "bmake" { ensure_bmake_install_avoids_double_destdir(command) + } else if package.name == "byacc" { + ensure_byacc_exposes_unprefixed_yacc(command) } else if package.name == "ubase" { ensure_ubase_moves_bin_payload(command) } else if package.name == "sqlite" { @@ -2141,6 +2158,23 @@ fi"# ) } +fn ensure_byacc_exposes_unprefixed_yacc(command: &str) -> String { + if !command_runs_install_step(command) { + return command.to_string(); + } + + format!( + "{command}\n{}", + r#"if [ ! -e "$DESTDIR/system/binaries/yacc" ] && [ -e "$DESTDIR/system/binaries/$LBI_TARGET-yacc" ]; then + ln -sf "$LBI_TARGET-yacc" "$DESTDIR/system/binaries/yacc" +fi +if [ ! -e "$DESTDIR/system/documentation/man-pages/man1/yacc.1" ] \ + && [ -e "$DESTDIR/system/documentation/man-pages/man1/$LBI_TARGET-yacc.1" ]; then + ln -sf "$LBI_TARGET-yacc.1" "$DESTDIR/system/documentation/man-pages/man1/yacc.1" +fi"# + ) +} + fn command_runs_install_step(command: &str) -> bool { command.lines().any(|line| { let trimmed = line.trim_start(); @@ -2479,25 +2513,46 @@ fn ensure_llvm_runtime_links_skip_missing_crt(command: &str) -> String { } fn ensure_llvm_cmake_uses_ccache(command: &str) -> String { - if command.contains("-DLLVM_CCACHE_BUILD=ON") { - return command.to_string(); + let command = command + .replace(" -DLLVM_CCACHE_BUILD=ON \\\n", "") + .replace(" -DLLVM_CCACHE_BUILD=ON \\", "") + .replace("-DLLVM_CCACHE_BUILD=ON", ""); + + if command.contains("-DCMAKE_C_COMPILER_LAUNCHER=") + && command.contains("-DCMAKE_CXX_COMPILER_LAUNCHER=") + && command.contains("-DCMAKE_ASM_COMPILER_LAUNCHER=") + { + return command; } + let launcher_flags = concat!( + " -DCMAKE_C_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\\n", + " -DCMAKE_CXX_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\\n", + " -DCMAKE_ASM_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\" + ); + if command.contains("lbi_cmake build-llvm") { return command.replace( "lbi_cmake build-llvm \\", - "lbi_cmake build-llvm \\\n -DLLVM_CCACHE_BUILD=ON \\", + &format!("lbi_cmake build-llvm \\\n{launcher_flags}"), ); } if command.contains("cmake -G Ninja \"../llvm\"") { return command.replace( "cmake -G Ninja \"../llvm\" \\", - "cmake -G Ninja \"../llvm\" \\\n -DLLVM_CCACHE_BUILD=ON \\", + &format!("cmake -G Ninja \"../llvm\" \\\n{launcher_flags}"), ); } - command.to_string() + command +} + +fn uses_llvm_cmake_ccache(package: &BookPackage) -> bool { + matches!( + package.name.as_str(), + "llvm-clang-pass1" | "llvm-clang-pass2" + ) } fn ensure_llvm_pass2_uses_pass1_native_tools(command: &str) -> String { @@ -4688,10 +4743,13 @@ EOF"# let build_script = generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - assert!(build_script.contains("-DLLVM_CCACHE_BUILD=ON")); - assert!( - build_script.contains("cmake -G Ninja \"../llvm\" \\\n -DLLVM_CCACHE_BUILD=ON \\") - ); + assert!(build_script.contains("export LBI_CCACHE")); + assert!(build_script.contains( + "cmake -G Ninja \"../llvm\" \\\n -DCMAKE_C_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\" + )); + assert!(build_script.contains("-DCMAKE_CXX_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); + assert!(build_script.contains("-DCMAKE_ASM_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); + assert!(!build_script.contains("-DLLVM_CCACHE_BUILD=ON")); assert!(build_script.contains("-DLLVM_NATIVE_TOOL_DIR=\"$LBI_SYSROOT/system/tools/bin\"")); assert!( build_script @@ -6231,6 +6289,43 @@ sed -i \ )); } + #[test] + fn byacc_generated_script_exposes_unprefixed_yacc() { + let mut package = test_recipe(8).package; + package.name = "byacc".to_string(); + package.title = "byacc stage 2 20260126".to_string(); + package.version = "20260126".to_string(); + package.recipe_id = "8-12-byacc".to_string(); + let recipe = PageRecipe { + input_files: vec!["byacc-20260126.tgz".to_string()], + source_urls: vec!["https://example.invalid/byacc-20260126.tgz".to_string()], + extract_dir: Some("byacc-20260126".to_string()), + commands: vec![ + "lbi_configure --with-manpage-format=normal".to_string(), + "make $LWI_MAKE_FLAGS".to_string(), + "make ${LWI_MAKE_FLAGS:-} install".to_string(), + ], + dependencies: Vec::new(), + license: "BSD-3-Clause".to_string(), + description: "byacc".to_string(), + }; + + let build_script = + generated_build_script(&package, &recipe, "x86_64-lbi-linux-musl", "x86_64"); + + assert!( + build_script.contains(r#"ln -sf "$LBI_TARGET-yacc" "$DESTDIR/system/binaries/yacc""#) + ); + assert!(build_script.contains( + r#"ln -sf "$LBI_TARGET-yacc.1" "$DESTDIR/system/documentation/man-pages/man1/yacc.1""# + )); + assert_eq!( + bootstrap_required_payload_paths("byacc"), + &["system/binaries/yacc"] + ); + assert_eq!(bootstrap_recipe_revision("byacc"), Some(1)); + } + #[test] fn generated_python_bootstrap_specs_use_custom_chroot_script() { let tmp = tempfile::tempdir().unwrap(); @@ -6329,10 +6424,13 @@ sed -i \ assert!(build_script.contains("export LBI_SYSROOT=\"${DEPOT_LBI_SYSROOT:-$LBI_ROOT}\"")); assert!(build_script.contains("export LWI_CXXFLAGS=\"${LWI_CXXFLAGS:-$LWI_CFLAGS}\"")); assert!(build_script.contains("rm -rf build-llvm")); - assert!(build_script.contains("-DLLVM_CCACHE_BUILD=ON")); - assert!( - build_script.contains("cmake -G Ninja \"../llvm\" \\\n -DLLVM_CCACHE_BUILD=ON \\") - ); + assert!(build_script.contains("LBI_CCACHE=\"$(command -v ccache)\"")); + assert!(build_script.contains( + "cmake -G Ninja \"../llvm\" \\\n -DCMAKE_C_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\" + )); + assert!(build_script.contains("-DCMAKE_CXX_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); + assert!(build_script.contains("-DCMAKE_ASM_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); + assert!(!build_script.contains("-DLLVM_CCACHE_BUILD=ON")); assert!(build_script.contains("LLVM_ENABLE_RUNTIMES=\"compiler-rt\"")); assert!(build_script.contains("COMPILER_RT_BUILD_BUILTINS=ON")); assert!(build_script.contains("COMPILER_RT_DEFAULT_TARGET_ONLY=ON")); @@ -6456,6 +6554,7 @@ sed -i \ assert!(build_script.contains(" --target=\"$LBI_TARGET\" \\\n")); assert!(build_script.contains(" --host=\"$LBI_TARGET\" \\\n")); + assert!(!build_script.contains(" --build=\"$LBI_TARGET\" \\\n")); assert!(build_script.contains("-DCMAKE_C_COMPILER_TARGET=\"$LBI_TARGET\"")); assert!(build_script.contains("-DCMAKE_CXX_COMPILER_TARGET=\"$LBI_TARGET\"")); assert!( @@ -6470,6 +6569,18 @@ sed -i \ ); } + #[test] + fn copied_build_profile_does_not_default_build_tuple() { + let tmp = tempfile::tempdir().unwrap(); + + copy_build_profile(tmp.path(), "x86_64-unknown-linux-musl", "x86_64").unwrap(); + + let profile = fs::read_to_string(tmp.path().join("etc/profile.d/lbi-build.sh")).unwrap(); + assert!(profile.contains(" --target=\"$LBI_TARGET\" \\\n")); + assert!(profile.contains(" --host=\"$LBI_TARGET\" \\\n")); + assert!(!profile.contains(" --build=\"$LBI_TARGET\" \\\n")); + } + #[test] fn rewrite_make_flags_does_not_duplicate_existing_parallel_flags() { assert_eq!( diff --git a/src/builder/meson.rs b/src/builder/meson.rs index 0a217fd..6e91801 100755 --- a/src/builder/meson.rs +++ b/src/builder/meson.rs @@ -491,6 +491,18 @@ fn generate_lib32_meson_cross_file( let ld = meson_binary_value(&command_words(&flags.ld), "linker")?; content.push_str(&format!("ld = {ld}\n")); } + for (name, command, label) in [ + ("strip", flags.strip.as_str(), "strip"), + ("nm", flags.nm.as_str(), "nm"), + ("objcopy", flags.objcopy.as_str(), "objcopy"), + ("objdump", flags.objdump.as_str(), "objdump"), + ("readelf", flags.readelf.as_str(), "readelf"), + ] { + if !command.trim().is_empty() { + let value = meson_binary_value(&command_words(command), label)?; + content.push_str(&format!("{name} = {value}\n")); + } + } content.push_str(&format!( "\n[host_machine]\nsystem = 'linux'\ncpu_family = '{cpu_family}'\ncpu = '{arch}'\nendian = 'little'\n" )); @@ -961,7 +973,12 @@ mod tests { 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() }; @@ -970,6 +987,12 @@ mod tests { 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(()) diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 3330f34..1810d59 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -205,9 +205,36 @@ pub fn set_env_var(env_vars: &mut EnvVars, key: &str, value: impl Into) } } +fn set_expanded_env_var(env_vars: &mut EnvVars, key: &str, value: impl AsRef) { + let expanded = expand_with_envs(value.as_ref(), env_vars); + set_env_var(env_vars, key, expanded); +} + +fn configured_tool_or_default(configured: &str, default: &str) -> String { + let configured = configured.trim(); + if configured.is_empty() { + default.to_string() + } else { + configured.to_string() + } +} + +fn configured_defaulted_tool_or_default( + configured: &str, + implicit_default: &str, + default: &str, +) -> String { + let configured = configured.trim(); + if configured.is_empty() || configured == implicit_default { + default.to_string() + } else { + configured.to_string() + } +} + fn apply_declared_env_vars(spec: &PackageSpec, env_vars: &mut EnvVars) { for raw in &spec.build.flags.env_vars { - let expanded = spec.expand_vars(raw); + let expanded = expand_with_envs(&spec.expand_vars(raw), env_vars); let entry = expanded.trim(); if entry.is_empty() { continue; @@ -234,6 +261,21 @@ fn apply_declared_env_vars(spec: &PackageSpec, env_vars: &mut EnvVars) { } } +/// Expand environment variables in a string using Depot's build env first, +/// falling back to the parent process environment. +pub(crate) fn expand_with_envs(input: &str, envs: &[(String, String)]) -> String { + let mut result = input.to_string(); + for (key, value) in envs { + result = result.replace(&format!("${key}"), value); + result = result.replace(&format!("${{{key}}}"), value); + } + for (key, value) in std::env::vars() { + result = result.replace(&format!("${key}"), &value); + result = result.replace(&format!("${{{key}}}"), &value); + } + result +} + fn default_libdir_for_variant(lib32_variant: bool) -> &'static str { if lib32_variant { "/usr/lib32" @@ -513,6 +555,23 @@ pub(crate) fn install_dirs(flags: &crate::package::BuildFlags) -> InstallDirs { } } +fn apply_install_dir_env_vars(env_vars: &mut EnvVars, flags: &crate::package::BuildFlags) { + let dirs = install_dirs(flags); + set_env_var(env_vars, "PREFIX", flags.prefix.clone()); + set_env_var(env_vars, "BINDIR", dirs.bindir); + set_env_var(env_vars, "SBINDIR", dirs.sbindir); + set_env_var(env_vars, "LIBDIR", dirs.libdir); + set_env_var(env_vars, "LIBEXECDIR", dirs.libexecdir); + set_env_var(env_vars, "SYSCONFDIR", dirs.sysconfdir); + set_env_var(env_vars, "LOCALSTATEDIR", dirs.localstatedir); + set_env_var(env_vars, "SHAREDSTATEDIR", dirs.sharedstatedir); + set_env_var(env_vars, "INCLUDEDIR", dirs.includedir); + set_env_var(env_vars, "DATAROOTDIR", dirs.datarootdir); + set_env_var(env_vars, "DATADIR", dirs.datadir); + set_env_var(env_vars, "MANDIR", dirs.mandir); + set_env_var(env_vars, "INFODIR", dirs.infodir); +} + pub(crate) fn install_destdir_path( build_dir: &Path, destdir: &Path, @@ -628,6 +687,10 @@ fn compiler_flag_sets( "build.flags.ltoflags", ); + if let Some(fuse_ld) = fuse_ld_flag(&flags.fuse_ld) { + ldflags.insert(0, fuse_ld); + } + if flags.use_lto && !ltoflags.is_empty() { cflags.extend(ltoflags.iter().cloned()); cxxflags.extend(ltoflags.iter().cloned()); @@ -637,6 +700,26 @@ fn compiler_flag_sets( (cflags, cxxflags, ldflags, ltoflags) } +fn fuse_ld_flag(fuse_ld: &str) -> Option { + let fuse_ld = fuse_ld.trim(); + if fuse_ld.is_empty() { + None + } else if fuse_ld.starts_with("-fuse-ld=") || fuse_ld.starts_with("--ld-path=") { + Some(fuse_ld.to_string()) + } else if fuse_ld.contains('/') { + Some(format!("-fuse-ld={fuse_ld}")) + } else { + let driver_name = match fuse_ld { + "ld.lld" => "lld", + "ld.mold" => "mold", + "ld.gold" => "gold", + "ld.bfd" => "bfd", + _ => fuse_ld, + }; + Some(format!("-fuse-ld={driver_name}")) + } +} + fn rust_ltoflags(flags: &crate::package::BuildFlags) -> Vec { sanitize_flag_list(flags.rustltoflags.clone(), "build.flags.rustltoflags") } @@ -664,21 +747,25 @@ pub fn standard_build_env( let mut env_vars: EnvVars = Vec::new(); let export_compiler_flags = export_compiler_flags && !flags.no_flags; + if !flags.tool_dir.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "TOOL_DIR", flags.tool_dir.trim()); + } + if include_compiler_env && export_compiler_flags { let (cflags, cxxflags, ldflags, ltoflags) = compiler_flag_sets(flags); if !cflags.is_empty() { - set_env_var(&mut env_vars, "CFLAGS", cflags.join(" ")); + set_expanded_env_var(&mut env_vars, "CFLAGS", cflags.join(" ")); } if !cxxflags.is_empty() { - set_env_var(&mut env_vars, "CXXFLAGS", cxxflags.join(" ")); + set_expanded_env_var(&mut env_vars, "CXXFLAGS", cxxflags.join(" ")); } if !ltoflags.is_empty() { - set_env_var(&mut env_vars, "LTOFLAGS", ltoflags.join(" ")); + set_expanded_env_var(&mut env_vars, "LTOFLAGS", ltoflags.join(" ")); } let rust_ltoflags = rust_ltoflags(flags); if !rust_ltoflags.is_empty() { - set_env_var(&mut env_vars, "RUSTLTOFLAGS", rust_ltoflags.join(" ")); + set_expanded_env_var(&mut env_vars, "RUSTLTOFLAGS", rust_ltoflags.join(" ")); } let ldflags = if !ldflags.is_empty() || !flags.libc.is_empty() { @@ -693,7 +780,7 @@ pub fn standard_build_env( String::new() }; if !ldflags.is_empty() { - set_env_var(&mut env_vars, "LDFLAGS", ldflags); + set_expanded_env_var(&mut env_vars, "LDFLAGS", ldflags); } } @@ -712,15 +799,9 @@ pub fn standard_build_env( if !effective_carch.is_empty() { set_env_var(&mut env_vars, "CARCH", effective_carch); } - if !flags.prefix.is_empty() { - set_env_var(&mut env_vars, "PREFIX", flags.prefix.clone()); - } + apply_install_dir_env_vars(&mut env_vars, flags); if !flags.makeflags.trim().is_empty() { - set_env_var( - &mut env_vars, - "MAKEFLAGS", - flags.makeflags.trim().to_string(), - ); + set_expanded_env_var(&mut env_vars, "MAKEFLAGS", flags.makeflags.trim()); } set_env_var(&mut env_vars, "DEPOT_ROOTFS", flags.rootfs.clone()); @@ -732,13 +813,60 @@ pub fn standard_build_env( if include_compiler_env { if let Some(cc_cfg) = cross { - set_env_var(&mut env_vars, "CC", cc_cfg.cc.clone()); - set_env_var(&mut env_vars, "CXX", cc_cfg.cxx.clone()); - set_env_var(&mut env_vars, "AR", cc_cfg.ar.clone()); - set_env_var(&mut env_vars, "RANLIB", cc_cfg.ranlib.clone()); - set_env_var(&mut env_vars, "STRIP", cc_cfg.strip.clone()); - set_env_var(&mut env_vars, "LD", cc_cfg.ld.clone()); - set_env_var(&mut env_vars, "NM", cc_cfg.nm.clone()); + let default_flags = BuildFlags::default(); + set_expanded_env_var( + &mut env_vars, + "CC", + configured_defaulted_tool_or_default(&flags.cc, &default_flags.cc, &cc_cfg.cc), + ); + set_expanded_env_var( + &mut env_vars, + "CXX", + configured_defaulted_tool_or_default(&flags.cxx, &default_flags.cxx, &cc_cfg.cxx), + ); + set_expanded_env_var( + &mut env_vars, + "AR", + configured_defaulted_tool_or_default(&flags.ar, &default_flags.ar, &cc_cfg.ar), + ); + set_expanded_env_var( + &mut env_vars, + "RANLIB", + configured_tool_or_default(&flags.ranlib, &cc_cfg.ranlib), + ); + set_expanded_env_var( + &mut env_vars, + "STRIP", + configured_tool_or_default(&flags.strip, &cc_cfg.strip), + ); + set_expanded_env_var( + &mut env_vars, + "LD", + configured_tool_or_default(&flags.ld, &cc_cfg.ld), + ); + set_expanded_env_var( + &mut env_vars, + "NM", + configured_tool_or_default(&flags.nm, &cc_cfg.nm), + ); + set_expanded_env_var( + &mut env_vars, + "OBJCOPY", + configured_tool_or_default(&flags.objcopy, &cc_cfg.objcopy), + ); + set_expanded_env_var( + &mut env_vars, + "OBJDUMP", + configured_tool_or_default(&flags.objdump, &cc_cfg.objdump), + ); + set_expanded_env_var( + &mut env_vars, + "READELF", + configured_tool_or_default(&flags.readelf, &cc_cfg.readelf), + ); + if !flags.cpp.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "CPP", flags.cpp.trim()); + } set_env_var(&mut env_vars, "CROSS_PREFIX", cc_cfg.prefix.clone()); set_env_var( &mut env_vars, @@ -746,14 +874,32 @@ pub fn standard_build_env( format!("{}-", cc_cfg.prefix), ); } else { - set_env_var(&mut env_vars, "CC", flags.cc.clone()); - set_env_var(&mut env_vars, "CXX", flags.cxx.clone()); - set_env_var(&mut env_vars, "AR", flags.ar.clone()); + set_expanded_env_var(&mut env_vars, "CC", flags.cc.trim()); + set_expanded_env_var(&mut env_vars, "CXX", flags.cxx.trim()); + set_expanded_env_var(&mut env_vars, "AR", flags.ar.trim()); + if !flags.ranlib.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "RANLIB", flags.ranlib.trim()); + } + if !flags.strip.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "STRIP", flags.strip.trim()); + } if !flags.ld.trim().is_empty() { - set_env_var(&mut env_vars, "LD", flags.ld.clone()); + set_expanded_env_var(&mut env_vars, "LD", flags.ld.trim()); + } + if !flags.nm.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "NM", flags.nm.trim()); + } + if !flags.objcopy.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "OBJCOPY", flags.objcopy.trim()); + } + if !flags.objdump.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "OBJDUMP", flags.objdump.trim()); + } + if !flags.readelf.trim().is_empty() { + set_expanded_env_var(&mut env_vars, "READELF", flags.readelf.trim()); } if !flags.cpp.trim().is_empty() { - set_env_var(&mut env_vars, "CPP", flags.cpp.clone()); + set_expanded_env_var(&mut env_vars, "CPP", flags.cpp.trim()); } } } @@ -1026,6 +1172,12 @@ mod tests { } } + 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"); @@ -1143,14 +1295,121 @@ mod tests { #[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()); @@ -1175,6 +1434,24 @@ mod tests { .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; @@ -1271,6 +1548,29 @@ mod tests { ); } + #[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"]); @@ -1383,6 +1683,62 @@ mod tests { ); } + #[test] + fn test_standard_build_env_exports_install_dir_vars() { + let mut spec = mk_spec(Vec::new(), Vec::new()); + spec.build.flags.prefix = "/system".into(); + spec.build.flags.bindir = "/system/binaries".into(); + spec.build.flags.sbindir = "/system/systembinaries".into(); + spec.build.flags.libdir = "/system/libraries".into(); + spec.build.flags.libexecdir = "/system/libexec".into(); + spec.build.flags.sysconfdir = "/system/configuration".into(); + spec.build.flags.localstatedir = "/system/variable".into(); + spec.build.flags.sharedstatedir = "/system/variable/lib".into(); + spec.build.flags.includedir = "/system/headers".into(); + spec.build.flags.datarootdir = "/system/share".into(); + spec.build.flags.datadir = "/system/data".into(); + spec.build.flags.mandir = "/system/documentation/man-pages".into(); + spec.build.flags.infodir = "/system/documentation/info".into(); + + let env = standard_build_env(&spec, None, false, true); + + assert_eq!(env_value(&env, "PREFIX"), Some("/system")); + assert_eq!(env_value(&env, "BINDIR"), Some("/system/binaries")); + assert_eq!(env_value(&env, "SBINDIR"), Some("/system/systembinaries")); + assert_eq!(env_value(&env, "LIBDIR"), Some("/system/libraries")); + assert_eq!(env_value(&env, "LIBEXECDIR"), Some("/system/libexec")); + assert_eq!(env_value(&env, "SYSCONFDIR"), Some("/system/configuration")); + assert_eq!(env_value(&env, "LOCALSTATEDIR"), Some("/system/variable")); + assert_eq!( + env_value(&env, "SHAREDSTATEDIR"), + Some("/system/variable/lib") + ); + assert_eq!(env_value(&env, "INCLUDEDIR"), Some("/system/headers")); + assert_eq!(env_value(&env, "DATAROOTDIR"), Some("/system/share")); + assert_eq!(env_value(&env, "DATADIR"), Some("/system/data")); + assert_eq!( + env_value(&env, "MANDIR"), + Some("/system/documentation/man-pages") + ); + assert_eq!( + env_value(&env, "INFODIR"), + Some("/system/documentation/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()); @@ -1407,6 +1763,7 @@ mod tests { 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); @@ -1420,6 +1777,11 @@ mod tests { .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] diff --git a/src/builder/perl.rs b/src/builder/perl.rs index 4ce797c..81786bc 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -58,7 +58,10 @@ pub fn build( configure_cmd.arg("INSTALLDIRS=vendor"); } for arg in &flags.configure { - configure_cmd.arg(spec.expand_vars(arg)); + configure_cmd.arg(crate::builder::expand_with_envs( + &spec.expand_vars(arg), + &env_vars, + )); } for arg in crate::builder::static_build_args_for(crate::package::BuildType::Perl, flags)? { configure_cmd.arg(arg); @@ -291,13 +294,19 @@ pub(crate) fn run_helper_configure( configure_cmd.arg("INSTALLDIRS=vendor"); } for arg in &flags.configure { - configure_cmd.arg(context.expand_vars(arg)); + configure_cmd.arg(crate::builder::expand_with_envs( + &context.expand_vars(arg), + &helper_env, + )); } for arg in crate::builder::static_build_args_for(crate::package::BuildType::Perl, &flags)? { configure_cmd.arg(arg); } for arg in extra_args { - configure_cmd.arg(context.expand_vars(arg)); + configure_cmd.arg(crate::builder::expand_with_envs( + &context.expand_vars(arg), + &helper_env, + )); } crate::builder::prepare_tool_command(&mut configure_cmd, &helper_env); @@ -350,12 +359,15 @@ pub(crate) fn run_helper_install( for target in &install_targets { install_cmd.arg(target); } - for arg in extra_args { - install_cmd.arg(context.expand_vars(arg)); - } - let mut install_env = env_vars.to_vec(); crate::builder::set_env_var(&mut install_env, "DESTDIR", destdir.clone()); + for arg in extra_args { + install_cmd.arg(crate::builder::expand_with_envs( + &context.expand_vars(arg), + &install_env, + )); + } + crate::builder::prepare_tool_command(&mut install_cmd, &install_env); let status = command_status_with_sh_fallback(&mut install_cmd).with_context(|| { diff --git a/src/builder/python.rs b/src/builder/python.rs index e7c556c..eb30444 100644 --- a/src/builder/python.rs +++ b/src/builder/python.rs @@ -36,7 +36,6 @@ pub fn build( ) -> Result<()> { let flags = &spec.build.flags; let actual_src = resolve_actual_src(spec, src_dir)?; - let config_settings = normalize_pep517_config_settings(spec)?; fs::create_dir_all(destdir) .with_context(|| format!("Failed to create DESTDIR: {}", destdir.display()))?; @@ -44,6 +43,7 @@ pub fn build( crate::builder::set_env_var(&mut env_vars, "PYTHONNOUSERSITE", "1"); crate::builder::set_env_var(&mut env_vars, "PYTHONDONTWRITEBYTECODE", "1"); crate::builder::set_env_var(&mut env_vars, "SETUPTOOLS_USE_DISTUTILS", "local"); + let config_settings = normalize_pep517_config_settings(spec, &env_vars)?; let mut state = StateTracker::new_with_namespace( &actual_src, @@ -84,10 +84,13 @@ pub fn build( Ok(()) } -fn normalize_pep517_config_settings(spec: &PackageSpec) -> Result> { +fn normalize_pep517_config_settings( + spec: &PackageSpec, + env_vars: &[(String, String)], +) -> Result> { let mut out = Vec::new(); for raw in &spec.build.flags.config_settings { - let expanded = spec.expand_vars(raw); + let expanded = crate::builder::expand_with_envs(&spec.expand_vars(raw), env_vars); let trimmed = expanded.trim(); if trimmed.is_empty() { continue; @@ -821,9 +824,11 @@ mod tests { "setup-args=--plat-name=x86_64".into(), "builddir".into(), "version=$version".into(), + "libdir=$LIBDIR".into(), ]; - let normalized = normalize_pep517_config_settings(&spec)?; + let env_vars = crate::builder::standard_build_env(&spec, None, false, true); + let normalized = normalize_pep517_config_settings(&spec, &env_vars)?; assert_eq!( normalized, vec![ @@ -831,6 +836,7 @@ mod tests { "setup-args=--plat-name=x86_64".to_string(), "builddir=".to_string(), "version=1.2.3".to_string(), + "libdir=/usr/lib".to_string(), ] ); Ok(()) @@ -840,7 +846,8 @@ mod tests { fn normalize_pep517_config_settings_rejects_invalid_key() { let mut spec = mk_spec(); spec.build.flags.config_settings = vec!["bad key=value".into()]; - let err = normalize_pep517_config_settings(&spec) + let env_vars = crate::builder::standard_build_env(&spec, None, false, true); + let err = normalize_pep517_config_settings(&spec, &env_vars) .expect_err("invalid config setting key should fail"); assert!(err.to_string().contains("expected KEY=VALUE or KEY")); } diff --git a/src/cli.rs b/src/cli.rs index 7179405..b4c6d6a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -250,6 +250,32 @@ pub struct ConfigArgs { pub rootfs_args: RootfsArgs, } +/// Arguments for selecting canonical tool aliases in the configured tool directory. +#[derive(Debug, Clone, Args)] +pub struct SetArgs { + #[command(flatten)] + pub rootfs_args: RootfsArgs, + + /// Tool role to configure, such as compiler, linker, or shell. + #[arg(value_enum)] + pub role: ToolRoleArg, + + /// Literal separator used by commands like `depot set compiler to clang`. + #[arg(value_name = "to")] + pub connector: String, + + /// Tool implementation to select for the requested role. + pub implementation: String, +} + +/// Tool alias role configurable through `depot set`. +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] +pub enum ToolRoleArg { + Compiler, + Linker, + Shell, +} + /// Arguments for Depot system-build state management commands. #[derive(Debug, Clone, Args)] pub struct SystemArgs { @@ -373,6 +399,8 @@ pub enum Commands { Repo(RepoArgs), /// Show current configuration Config(ConfigArgs), + /// Select canonical tool aliases, e.g. `depot set compiler to clang` + Set(SetArgs), /// Manage system build stage, package layers, and book-style layout state System(SystemArgs), /// Generate shell completion scripts and a man page into an output directory. @@ -591,7 +619,7 @@ pub enum RepoKindArg { mod tests { use super::{ BuildArgs, Cli, Commands, ConvertArgs, InstallArgs, RepoArgs, RepoCommands, SearchArgs, - UpdateArgs, + SetArgs, ToolRoleArg, UpdateArgs, }; use clap::{CommandFactory, Parser}; use std::path::PathBuf; @@ -631,6 +659,52 @@ mod tests { } } + #[test] + fn set_accepts_role_to_implementation_form() { + let cli = Cli::try_parse_from([ + "depot", + "set", + "--rootfs", + "/tmp/rootfs", + "compiler", + "to", + "clang", + ]) + .unwrap(); + match cli.command { + Commands::Set(SetArgs { + rootfs_args, + role, + connector, + implementation, + }) => { + assert_eq!(rootfs_args.rootfs, PathBuf::from("/tmp/rootfs")); + assert_eq!(role, ToolRoleArg::Compiler); + assert_eq!(connector, "to"); + assert_eq!(implementation, "clang"); + } + _ => panic!("expected set command"), + } + } + + #[test] + fn set_accepts_shell_role() { + let cli = Cli::try_parse_from(["depot", "set", "shell", "to", "dash"]).unwrap(); + match cli.command { + Commands::Set(SetArgs { + role, + connector, + implementation, + .. + }) => { + assert_eq!(role, ToolRoleArg::Shell); + assert_eq!(connector, "to"); + assert_eq!(implementation, "dash"); + } + _ => panic!("expected set command"), + } + } + #[test] fn update_accepts_no_package_names() { let cli = Cli::try_parse_from(["depot", "update"]).unwrap(); diff --git a/src/commands.rs b/src/commands.rs index d003c7d..0cd6f0e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,7 +1,7 @@ use crate::cli::{ BuildArgs, Cli, Commands, ConfigArgs, ConvertArgs, InfoArgs, InstallArgs, InternalCommands, - ListArgs, OwnsArgs, RemoveArgs, RepoCommands, RepoKindArg, SearchArgs, SignArgs, SystemArgs, - UpdateArgs, + ListArgs, OwnsArgs, RemoveArgs, RepoCommands, RepoKindArg, SearchArgs, SetArgs, SignArgs, + SystemArgs, UpdateArgs, }; use crate::{ bootstrap, builder, cli_assets, config, cross, db, deps, index, install, locking, package, @@ -66,7 +66,7 @@ fn rootfs_is_system_root(rootfs: &Path) -> bool { fn command_requires_live_root(command: &Commands) -> bool { matches!( command, - Commands::Install(_) | Commands::Remove(_) | Commands::Update(_) + Commands::Install(_) | Commands::Remove(_) | Commands::Update(_) | Commands::Set(_) ) } @@ -100,6 +100,7 @@ fn command_rootfs(command: &Commands) -> Option<&Path> { Commands::Sign(args) => Some(&args.rootfs_args.rootfs), Commands::Repo(args) => Some(repo_command_rootfs(&args.command)), Commands::Config(args) => Some(&args.rootfs_args.rootfs), + Commands::Set(args) => Some(&args.rootfs_args.rootfs), Commands::System(args) => Some(&args.rootfs_args.rootfs), Commands::Check(_) | Commands::Convert(_) @@ -125,6 +126,7 @@ fn command_assume_yes(command: &Commands) -> bool { | Commands::Sign(_) | Commands::Repo(_) | Commands::Config(_) + | Commands::Set(_) | Commands::System(_) | Commands::GenerateArtifacts(_) | Commands::MakeSpec(_) @@ -2167,6 +2169,7 @@ fn run_direct_install_request( // Install from spec (normal build) 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(); @@ -2567,6 +2570,7 @@ mod check; mod install_cmd; mod misc; mod repo; +mod set; mod update; pub fn run(cli: Cli) -> Result<()> { @@ -2591,6 +2595,7 @@ pub fn run(cli: Cli) -> Result<()> { | Commands::Sign(_) | Commands::Repo(_) | Commands::Config(_) + | Commands::Set(_) | Commands::System(_) | Commands::GenerateArtifacts(_) | Commands::Convert(_) @@ -2613,6 +2618,7 @@ pub fn run(cli: Cli) -> Result<()> { Commands::Repo(args) => repo::run_repo(args.command)?, Commands::GenerateArtifacts(args) => misc::run_generate_artifacts(args)?, Commands::Config(args) => misc::run_config(args)?, + Commands::Set(args) => set::run_set(args)?, Commands::System(args) => misc::run_system(args)?, Commands::MakeSpec(args) => misc::run_make_spec(args)?, Commands::Convert(args) => misc::run_convert(args)?, diff --git a/src/commands/build_cmd.rs b/src/commands/build_cmd.rs index 8d80b9a..e2ff772 100644 --- a/src/commands/build_cmd.rs +++ b/src/commands/build_cmd.rs @@ -11,6 +11,17 @@ use self::support::{ requested_outputs, should_install_test_deps, warn_if_running_as_root_for_build, }; +pub(crate) fn build_env_rootfs(rootfs: &Path) -> String { + if rootfs == Path::new("/") { + return "/".to_string(); + } + rootfs + .canonicalize() + .unwrap_or_else(|_| rootfs.to_path_buf()) + .to_string_lossy() + .into_owned() +} + pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> { let BuildArgs { rootfs_args, @@ -46,6 +57,7 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> { let mut auto_installed_deps = AutoInstalledDependencyTracker::default(); let build_result: Result<()> = (|| { pkg_spec.apply_config(&config); + pkg_spec.build.flags.rootfs = build_env_rootfs(&rootfs); let lib32_only = effective_lib32_only(&pkg_spec, cli_lib32_only); let requested_outputs = requested_outputs(&pkg_spec, lib32_only); let db_path = config.installed_db_path(&rootfs); diff --git a/src/commands/install_cmd/archive.rs b/src/commands/install_cmd/archive.rs index 6361371..3adfec7 100644 --- a/src/commands/install_cmd/archive.rs +++ b/src/commands/install_cmd/archive.rs @@ -138,7 +138,7 @@ pub(crate) fn package_spec_from_repo_record( runtime: record.runtime_dependencies.clone(), test: Vec::new(), optional: record.optional_dependencies.clone(), - groups: Vec::new(), + groups: record.groups.clone(), lib32: None, }, package_alternatives: Default::default(), diff --git a/src/commands/misc/internal.rs b/src/commands/misc/internal.rs index aff15ef..55450dd 100644 --- a/src/commands/misc/internal.rs +++ b/src/commands/misc/internal.rs @@ -35,6 +35,7 @@ fn current_process_env_vars() -> Vec<(String, String)> { "RUSTLTOFLAGS", "SETUPTOOLS_USE_DISTUTILS", "STRIP", + "TOOL_DIR", ]; ALLOWED_ENV_VARS diff --git a/src/commands/set.rs b/src/commands/set.rs new file mode 100644 index 0000000..22da194 --- /dev/null +++ b/src/commands/set.rs @@ -0,0 +1,434 @@ +use super::*; +use crate::cli::ToolRoleArg; +use std::io::ErrorKind; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ToolAlias { + alias: &'static str, + target: &'static str, +} + +pub(super) fn run_set(args: SetArgs) -> Result<()> { + let SetArgs { + rootfs_args, + role, + connector, + implementation, + } = args; + + if connector != "to" { + anyhow::bail!("Expected `to` in set command, for example: depot set compiler to clang"); + } + + let implementation = implementation.trim().to_ascii_lowercase(); + let aliases = aliases_for_selection(role, &implementation)?; + let rootfs = rootfs_args.rootfs; + let config = config::Config::for_rootfs(&rootfs); + let mut set_lock = locking::open_lock(&config)?; + let set_lock_path = locking::lock_path(&config); + let _set_lock_guard = locking::try_write(&mut set_lock, &set_lock_path, "set")?; + let alias_dir = configured_alias_dir(&config); + let host_alias_dir = dir_in_rootfs(&rootfs, &alias_dir); + + configure_tool_aliases(&host_alias_dir, &aliases).with_context(|| { + format!( + "Failed to set {} to {} in {}", + role_name(role), + implementation, + host_alias_dir.display() + ) + })?; + + ui::success(format!( + "Set {} to {} in {}", + role_name(role), + implementation, + host_alias_dir.display() + )); + Ok(()) +} + +fn role_name(role: ToolRoleArg) -> &'static str { + match role { + ToolRoleArg::Compiler => "compiler", + ToolRoleArg::Linker => "linker", + ToolRoleArg::Shell => "shell", + } +} + +fn configured_alias_dir(config: &config::Config) -> PathBuf { + let configured = config + .build_overrides + .get("flags") + .and_then(|flags| flags.get("bindir")) + .or_else(|| config.build_overrides.get("bindir")) + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("/system/binaries"); + PathBuf::from(configured) +} + +fn dir_in_rootfs(rootfs: &Path, dir: &Path) -> PathBuf { + let rootfs = resolve_rootfs_base(rootfs); + if dir.is_absolute() && dir.starts_with(&rootfs) { + dir.to_path_buf() + } else if dir.is_absolute() { + rootfs.join(dir.strip_prefix("/").unwrap_or(dir)) + } else { + rootfs.join(dir) + } +} + +fn resolve_rootfs_base(rootfs: &Path) -> PathBuf { + if rootfs.exists() { + rootfs.canonicalize().unwrap_or_else(|_| { + std::env::current_dir() + .map(|cwd| cwd.join(rootfs)) + .unwrap_or_else(|_| rootfs.to_path_buf()) + }) + } else { + std::env::current_dir() + .map(|cwd| cwd.join(rootfs)) + .unwrap_or_else(|_| rootfs.to_path_buf()) + } +} + +fn aliases_for_selection(role: ToolRoleArg, implementation: &str) -> Result> { + let aliases = match (role, implementation) { + (ToolRoleArg::Compiler, "clang") => vec![ + ToolAlias { + alias: "cc", + target: "clang", + }, + ToolAlias { + alias: "c++", + target: "clang++", + }, + ], + (ToolRoleArg::Compiler, "gcc") => vec![ + ToolAlias { + alias: "cc", + target: "gcc", + }, + ToolAlias { + alias: "c++", + target: "g++", + }, + ], + (ToolRoleArg::Linker, "lld" | "ld.lld") => vec![ToolAlias { + alias: "ld", + target: "ld.lld", + }], + (ToolRoleArg::Linker, "mold") => vec![ToolAlias { + alias: "ld", + target: "mold", + }], + (ToolRoleArg::Shell, "bash") => vec![ToolAlias { + alias: "sh", + target: "bash", + }], + (ToolRoleArg::Shell, "dash") => vec![ToolAlias { + alias: "sh", + target: "dash", + }], + (ToolRoleArg::Shell, "zsh") => vec![ToolAlias { + alias: "sh", + target: "zsh", + }], + (ToolRoleArg::Compiler, _) => { + anyhow::bail!( + "Unsupported compiler selection `{implementation}`; supported: clang, gcc" + ) + } + (ToolRoleArg::Linker, _) => { + anyhow::bail!("Unsupported linker selection `{implementation}`; supported: lld, mold") + } + (ToolRoleArg::Shell, _) => { + anyhow::bail!( + "Unsupported shell selection `{implementation}`; supported: bash, dash, zsh" + ) + } + }; + Ok(aliases) +} + +#[cfg(unix)] +fn configure_tool_aliases(tool_dir: &Path, aliases: &[ToolAlias]) -> Result<()> { + use std::os::unix::fs as unix_fs; + + for alias in aliases { + validate_tool_name("alias", alias.alias)?; + validate_tool_name("target", alias.target)?; + let target_path = tool_dir.join(alias.target); + match fs::metadata(&target_path) { + Ok(_) => {} + Err(err) if err.kind() == ErrorKind::NotFound => { + anyhow::bail!( + "Cannot set {} to {}; target tool is missing: {}", + alias.alias, + alias.target, + target_path.display() + ); + } + Err(err) => { + return Err(err) + .with_context(|| format!("Failed to inspect {}", target_path.display())); + } + } + + let alias_path = tool_dir.join(alias.alias); + match fs::symlink_metadata(&alias_path) { + Ok(metadata) if metadata.file_type().is_symlink() => { + let current = fs::read_link(&alias_path) + .with_context(|| format!("Failed to read symlink {}", alias_path.display()))?; + if current == Path::new(alias.target) { + continue; + } + fs::remove_file(&alias_path) + .with_context(|| format!("Failed to replace {}", alias_path.display()))?; + } + Ok(_) => { + anyhow::bail!( + "Refusing to replace non-symlink tool alias: {}", + alias_path.display() + ); + } + Err(err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => { + return Err(err) + .with_context(|| format!("Failed to inspect {}", alias_path.display())); + } + } + + unix_fs::symlink(alias.target, &alias_path) + .with_context(|| format!("Failed to create symlink {}", alias_path.display()))?; + } + Ok(()) +} + +#[cfg(not(unix))] +fn configure_tool_aliases(_tool_dir: &Path, _aliases: &[ToolAlias]) -> Result<()> { + anyhow::bail!("depot set requires Unix symlink support") +} + +fn validate_tool_name(kind: &str, name: &str) -> Result<()> { + if name.is_empty() || name.contains('/') || name.contains('\0') { + anyhow::bail!("Invalid tool {kind}: {name:?}"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::{RootfsArgs, ToolRoleArg}; + + fn set_args(rootfs: &Path, role: ToolRoleArg, implementation: &str) -> SetArgs { + SetArgs { + rootfs_args: RootfsArgs { + rootfs: rootfs.to_path_buf(), + }, + role, + connector: "to".into(), + implementation: implementation.into(), + } + } + + fn make_tool_dir(rootfs: &Path) -> PathBuf { + let etc = rootfs.join("etc/depot.d"); + fs::create_dir_all(&etc).unwrap(); + fs::write( + etc.join("build.toml"), + r#" +[flags] +tool_dir = "/system/tools/bin" +bindir = "/system/binaries" +"#, + ) + .unwrap(); + let tool_dir = rootfs.join("system/binaries"); + fs::create_dir_all(&tool_dir).unwrap(); + tool_dir + } + + #[test] + fn configured_alias_dir_prefers_bindir_over_tool_dir() { + let tmp = tempfile::tempdir().unwrap(); + make_tool_dir(tmp.path()); + let config = config::Config::for_rootfs(tmp.path()); + + assert_eq!( + configured_alias_dir(&config), + PathBuf::from("/system/binaries") + ); + } + + #[test] + fn dir_in_rootfs_does_not_duplicate_host_absolute_rootfs_paths() { + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path().canonicalize().unwrap(); + let host_dir = rootfs.join("system/binaries"); + + assert_eq!(dir_in_rootfs(&rootfs, &host_dir), host_dir); + } + + #[test] + fn compiler_selection_maps_aliases() { + assert_eq!( + aliases_for_selection(ToolRoleArg::Compiler, "clang").unwrap(), + vec![ + ToolAlias { + alias: "cc", + target: "clang" + }, + ToolAlias { + alias: "c++", + target: "clang++" + } + ] + ); + assert_eq!( + aliases_for_selection(ToolRoleArg::Compiler, "gcc").unwrap(), + vec![ + ToolAlias { + alias: "cc", + target: "gcc" + }, + ToolAlias { + alias: "c++", + target: "g++" + } + ] + ); + } + + #[test] + fn linker_selection_maps_aliases() { + assert_eq!( + aliases_for_selection(ToolRoleArg::Linker, "lld").unwrap(), + vec![ToolAlias { + alias: "ld", + target: "ld.lld" + }] + ); + assert_eq!( + aliases_for_selection(ToolRoleArg::Linker, "mold").unwrap(), + vec![ToolAlias { + alias: "ld", + target: "mold" + }] + ); + } + + #[test] + fn shell_selection_maps_aliases() { + for shell in ["bash", "dash", "zsh"] { + assert_eq!( + aliases_for_selection(ToolRoleArg::Shell, shell).unwrap(), + vec![ToolAlias { + alias: "sh", + target: shell + }] + ); + } + } + + #[cfg(unix)] + #[test] + fn run_set_switches_compiler_aliases() { + let tmp = tempfile::tempdir().unwrap(); + let tool_dir = make_tool_dir(tmp.path()); + fs::write(tool_dir.join("clang"), "").unwrap(); + fs::write(tool_dir.join("clang++"), "").unwrap(); + fs::write(tool_dir.join("gcc"), "").unwrap(); + fs::write(tool_dir.join("g++"), "").unwrap(); + + run_set(set_args(tmp.path(), ToolRoleArg::Compiler, "clang")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("cc")).unwrap(), + PathBuf::from("clang") + ); + assert_eq!( + fs::read_link(tool_dir.join("c++")).unwrap(), + PathBuf::from("clang++") + ); + + run_set(set_args(tmp.path(), ToolRoleArg::Compiler, "gcc")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("cc")).unwrap(), + PathBuf::from("gcc") + ); + assert_eq!( + fs::read_link(tool_dir.join("c++")).unwrap(), + PathBuf::from("g++") + ); + } + + #[cfg(unix)] + #[test] + fn run_set_switches_linker_alias() { + let tmp = tempfile::tempdir().unwrap(); + let tool_dir = make_tool_dir(tmp.path()); + fs::write(tool_dir.join("ld.lld"), "").unwrap(); + fs::write(tool_dir.join("mold"), "").unwrap(); + + run_set(set_args(tmp.path(), ToolRoleArg::Linker, "lld")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("ld")).unwrap(), + PathBuf::from("ld.lld") + ); + + run_set(set_args(tmp.path(), ToolRoleArg::Linker, "mold")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("ld")).unwrap(), + PathBuf::from("mold") + ); + } + + #[cfg(unix)] + #[test] + fn run_set_switches_shell_alias() { + let tmp = tempfile::tempdir().unwrap(); + let tool_dir = make_tool_dir(tmp.path()); + fs::write(tool_dir.join("dash"), "").unwrap(); + fs::write(tool_dir.join("zsh"), "").unwrap(); + fs::write(tool_dir.join("bash"), "").unwrap(); + + run_set(set_args(tmp.path(), ToolRoleArg::Shell, "dash")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("sh")).unwrap(), + PathBuf::from("dash") + ); + + run_set(set_args(tmp.path(), ToolRoleArg::Shell, "zsh")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("sh")).unwrap(), + PathBuf::from("zsh") + ); + + run_set(set_args(tmp.path(), ToolRoleArg::Shell, "bash")).unwrap(); + assert_eq!( + fs::read_link(tool_dir.join("sh")).unwrap(), + PathBuf::from("bash") + ); + } + + #[cfg(unix)] + #[test] + fn run_set_refuses_to_replace_real_tool_alias() { + let tmp = tempfile::tempdir().unwrap(); + let tool_dir = make_tool_dir(tmp.path()); + fs::write(tool_dir.join("clang"), "").unwrap(); + fs::write(tool_dir.join("clang++"), "").unwrap(); + fs::write(tool_dir.join("cc"), "real binary").unwrap(); + + let err = run_set(set_args(tmp.path(), ToolRoleArg::Compiler, "clang")).unwrap_err(); + assert!(err.to_string().contains("Failed to set compiler to clang")); + assert!(err.chain().any(|cause| { + cause + .to_string() + .contains("Refusing to replace non-symlink") + })); + } +} diff --git a/src/commands/tests.rs b/src/commands/tests.rs index 1931d01..9130bcb 100644 --- a/src/commands/tests.rs +++ b/src/commands/tests.rs @@ -41,6 +41,90 @@ fn lib32_args() -> Lib32Args { Lib32Args { lib32_only: false } } +#[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 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(()) +} + fn test_binary_repo_record(name: &str, filename: &str) -> db::repo::BinaryRepoPackageRecord { db::repo::BinaryRepoPackageRecord { repo_name: "core".into(), @@ -378,7 +462,7 @@ fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> R assert_eq!(installed[0].package.name, "pkg"); assert!(rootfs.path().join("usr/bin/hello").exists()); - let db_path = cfg.db_dir.join("packages.db"); + let db_path = cfg.installed_db_path(rootfs.path()); assert_eq!( db::get_package_version(&db_path, "pkg")?, Some("1.0".into()) diff --git a/src/config.rs b/src/config.rs index 6eae5ed..3f24451 100755 --- a/src/config.rs +++ b/src/config.rs @@ -269,11 +269,16 @@ impl Config { home.join(".local/share/depot"), ) } else { + let variable_root = if is_system_root { + abs_rootfs.join("var") + } else { + abs_rootfs.join("system/variable") + }; ( - abs_rootfs.join("var/cache/depot/sources"), - abs_rootfs.join("var/cache/depot/packages"), - abs_rootfs.join("var/cache/depot/build"), - abs_rootfs.join("var/lib/depot"), + variable_root.join("cache/depot/sources"), + variable_root.join("cache/depot/packages"), + variable_root.join("cache/depot/build"), + variable_root.join("lib/depot"), ) }; @@ -307,7 +312,11 @@ impl Config { /// to per-user directories under `$HOME`. pub fn installed_db_path(&self, rootfs: &Path) -> PathBuf { let abs_rootfs = resolve_rootfs_base(rootfs); - abs_rootfs.join("var/lib/depot/packages.db") + if abs_rootfs == Path::new("/") || abs_rootfs.as_os_str() == "/" { + abs_rootfs.join("var/lib/depot/packages.db") + } else { + abs_rootfs.join("system/variable/lib/depot/packages.db") + } } /// Load system-level and user-level overrides @@ -592,15 +601,20 @@ mod tests { config .cache_dir .to_string_lossy() - .contains("var/cache/depot/sources") + .contains("system/variable/cache/depot/sources") ); assert!( config .build_dir .to_string_lossy() - .contains("var/cache/depot/build") + .contains("system/variable/cache/depot/build") + ); + assert!( + config + .db_dir + .to_string_lossy() + .contains("system/variable/lib/depot") ); - assert!(config.db_dir.to_string_lossy().contains("var/lib/depot")); } #[test] @@ -713,7 +727,7 @@ cflags += ["-g"] let config = Config::for_rootfs(&root); assert_eq!( config.installed_db_path(&root), - PathBuf::from("/tmp/test_root/var/lib/depot/packages.db") + PathBuf::from("/tmp/test_root/system/variable/lib/depot/packages.db") ); } @@ -738,6 +752,7 @@ cflags += ["-g"] r#" [build] prefix = "/opt/depot" +tool_dir = "/opt/toolchain/bin" cc = "clang" [build.flags] @@ -763,6 +778,13 @@ test_deps = true config.build_overrides.get("cc").and_then(|v| v.as_str()), Some("clang") ); + assert_eq!( + config + .build_overrides + .get("tool_dir") + .and_then(|v| v.as_str()), + Some("/opt/toolchain/bin") + ); assert_eq!( config .build_overrides diff --git a/src/package/interactive.rs b/src/package/interactive.rs index 588f223..5d3fb0b 100644 --- a/src/package/interactive.rs +++ b/src/package/interactive.rs @@ -1014,6 +1014,12 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { ), ); } + if !spec.build.flags.fuse_ld.is_empty() { + flags_tbl.insert( + "fuse_ld".into(), + Value::String(spec.build.flags.fuse_ld.clone()), + ); + } if !spec.build.flags.ltoflags.is_empty() { flags_tbl.insert( "ltoflags".into(), @@ -1114,6 +1120,45 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { if spec.build.flags.ar != defaults.ar { flags_tbl.insert("ar".into(), Value::String(spec.build.flags.ar.clone())); } + if !spec.build.flags.ranlib.is_empty() { + flags_tbl.insert( + "ranlib".into(), + Value::String(spec.build.flags.ranlib.clone()), + ); + } + if !spec.build.flags.strip.is_empty() { + flags_tbl.insert( + "strip".into(), + Value::String(spec.build.flags.strip.clone()), + ); + } + if !spec.build.flags.ld.is_empty() { + flags_tbl.insert("ld".into(), Value::String(spec.build.flags.ld.clone())); + } + if !spec.build.flags.nm.is_empty() { + flags_tbl.insert("nm".into(), Value::String(spec.build.flags.nm.clone())); + } + if !spec.build.flags.objcopy.is_empty() { + flags_tbl.insert( + "objcopy".into(), + Value::String(spec.build.flags.objcopy.clone()), + ); + } + if !spec.build.flags.objdump.is_empty() { + flags_tbl.insert( + "objdump".into(), + Value::String(spec.build.flags.objdump.clone()), + ); + } + if !spec.build.flags.readelf.is_empty() { + flags_tbl.insert( + "readelf".into(), + Value::String(spec.build.flags.readelf.clone()), + ); + } + if !spec.build.flags.cpp.is_empty() { + flags_tbl.insert("cpp".into(), Value::String(spec.build.flags.cpp.clone())); + } if !spec.build.flags.libc.is_empty() { flags_tbl.insert("libc".into(), Value::String(spec.build.flags.libc.clone())); } @@ -1849,6 +1894,7 @@ mod tests { 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()], @@ -1928,6 +1974,7 @@ mod tests { 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 = [")); diff --git a/src/package/spec.rs b/src/package/spec.rs index 5551ca2..880a935 100755 --- a/src/package/spec.rs +++ b/src/package/spec.rs @@ -368,7 +368,16 @@ impl PackageSpec { } } + fn apply_default_string(target: &mut String, default: &str, value: &toml::Value) { + if let Some(s) = value.as_str() + && (target.trim().is_empty() || target == default) + { + *target = s.to_string(); + } + } + fn apply_flags_table(&mut self, table: &toml::map::Map) { + let default_flags = BuildFlags::default(); for (k, v) in table { // match case-insensitively for common keys (allow CXX/Cc etc.) match k.to_lowercase().as_str() { @@ -471,6 +480,20 @@ impl PackageSpec { self.build.flags.ldflags = vec![s.to_string()]; } } + "fuse_ld" | "fuse-ld" => { + Self::apply_default_string( + &mut self.build.flags.fuse_ld, + &default_flags.fuse_ld, + v, + ); + } + "tool_dir" | "tool-dir" | "tools_dir" | "tools-dir" => { + Self::apply_default_string( + &mut self.build.flags.tool_dir, + &default_flags.tool_dir, + v, + ); + } "replace_ldflags" | "replace-ldflags" => { if let Some(arr) = v.as_array() { self.build.flags.replace_ldflags = arr @@ -565,109 +588,169 @@ impl PackageSpec { } } "cc" => { - if let Some(s) = v.as_str() { - self.build.flags.cc = s.to_string(); - } + Self::apply_default_string(&mut self.build.flags.cc, &default_flags.cc, v); } "cxx" => { - if let Some(s) = v.as_str() { - self.build.flags.cxx = s.to_string(); - } + Self::apply_default_string(&mut self.build.flags.cxx, &default_flags.cxx, v); } "ar" => { - if let Some(s) = v.as_str() { - self.build.flags.ar = s.to_string(); - } + Self::apply_default_string(&mut self.build.flags.ar, &default_flags.ar, v); + } + "ranlib" => { + Self::apply_default_string( + &mut self.build.flags.ranlib, + &default_flags.ranlib, + v, + ); + } + "strip" => { + Self::apply_default_string( + &mut self.build.flags.strip, + &default_flags.strip, + v, + ); } "ld" => { - if let Some(s) = v.as_str() { - self.build.flags.ld = s.to_string(); - } + Self::apply_default_string(&mut self.build.flags.ld, &default_flags.ld, v); + } + "nm" => { + Self::apply_default_string(&mut self.build.flags.nm, &default_flags.nm, v); + } + "objcopy" => { + Self::apply_default_string( + &mut self.build.flags.objcopy, + &default_flags.objcopy, + v, + ); + } + "objdump" => { + Self::apply_default_string( + &mut self.build.flags.objdump, + &default_flags.objdump, + v, + ); + } + "readelf" => { + Self::apply_default_string( + &mut self.build.flags.readelf, + &default_flags.readelf, + v, + ); } "cpp" => { - if let Some(s) = v.as_str() { - self.build.flags.cpp = s.to_string(); - } + Self::apply_default_string(&mut self.build.flags.cpp, &default_flags.cpp, v); } "prefix" => { - if let Some(s) = v.as_str() { - self.build.flags.prefix = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.prefix, + &default_flags.prefix, + v, + ); } "bindir" => { - if let Some(s) = v.as_str() { - self.build.flags.bindir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.bindir, + &default_flags.bindir, + v, + ); } "sbindir" => { - if let Some(s) = v.as_str() { - self.build.flags.sbindir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.sbindir, + &default_flags.sbindir, + v, + ); } "libdir" => { - if let Some(s) = v.as_str() { - self.build.flags.libdir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.libdir, + &default_flags.libdir, + v, + ); } "libexecdir" => { - if let Some(s) = v.as_str() { - self.build.flags.libexecdir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.libexecdir, + &default_flags.libexecdir, + v, + ); } "sysconfdir" => { - if let Some(s) = v.as_str() { - self.build.flags.sysconfdir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.sysconfdir, + &default_flags.sysconfdir, + v, + ); } "localstatedir" => { - if let Some(s) = v.as_str() { - self.build.flags.localstatedir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.localstatedir, + &default_flags.localstatedir, + v, + ); } "sharedstatedir" => { - if let Some(s) = v.as_str() { - self.build.flags.sharedstatedir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.sharedstatedir, + &default_flags.sharedstatedir, + v, + ); } "includedir" => { - if let Some(s) = v.as_str() { - self.build.flags.includedir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.includedir, + &default_flags.includedir, + v, + ); } "datarootdir" => { - if let Some(s) = v.as_str() { - self.build.flags.datarootdir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.datarootdir, + &default_flags.datarootdir, + v, + ); } "datadir" => { - if let Some(s) = v.as_str() { - self.build.flags.datadir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.datadir, + &default_flags.datadir, + v, + ); } "mandir" => { - if let Some(s) = v.as_str() { - self.build.flags.mandir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.mandir, + &default_flags.mandir, + v, + ); } "infodir" => { - if let Some(s) = v.as_str() { - self.build.flags.infodir = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.infodir, + &default_flags.infodir, + v, + ); } "chost" => { - if let Some(s) = v.as_str() { - self.build.flags.chost = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.chost, + &default_flags.chost, + v, + ); } "cbuild" => { - if let Some(s) = v.as_str() { - self.build.flags.cbuild = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.cbuild, + &default_flags.cbuild, + v, + ); } "carch" => { - if let Some(s) = v.as_str() { - self.build.flags.carch = s.to_string(); - } + Self::apply_default_string( + &mut self.build.flags.carch, + &default_flags.carch, + v, + ); } "makeflags" | "make_flags" | "make-flags" => { if let Some(s) = v.as_str() { @@ -1318,16 +1401,51 @@ impl PackageSpec { self.build.flags.cc = s.to_string(); } } + "cxx" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.cxx = s.to_string(); + } + } "ar" => { if let Some(s) = values.last().and_then(|v| v.as_str()) { self.build.flags.ar = s.to_string(); } } + "ranlib" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.ranlib = s.to_string(); + } + } + "strip" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.strip = s.to_string(); + } + } "ld" => { if let Some(s) = values.last().and_then(|v| v.as_str()) { self.build.flags.ld = s.to_string(); } } + "nm" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.nm = s.to_string(); + } + } + "objcopy" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.objcopy = s.to_string(); + } + } + "objdump" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.objdump = s.to_string(); + } + } + "readelf" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.readelf = s.to_string(); + } + } "cpp" => { if let Some(s) = values.last().and_then(|v| v.as_str()) { self.build.flags.cpp = s.to_string(); @@ -2511,8 +2629,17 @@ type = "custom" r#" [flags] cc = "my-cc" +cxx = "my-cxx" +ar = "my-ar" +ranlib = "my-ranlib" +strip = "my-strip" ld = "ld.lld" +nm = "my-nm" +objcopy = "my-objcopy" +objdump = "my-objdump" +readelf = "my-readelf" CPP = "clang-cpp" +tool_dir = "/opt/toolchain/bin" cflags = ["-O2"] replace_cflags = ["-O2=>-O3"] cxxflags = ["-O2", "-pipe"] @@ -2672,8 +2799,17 @@ post_configure = ["echo configured"] spec.apply_config(&config); assert_eq!(spec.build.flags.cc, "my-cc"); + assert_eq!(spec.build.flags.cxx, "my-cxx"); + assert_eq!(spec.build.flags.ar, "my-ar"); + assert_eq!(spec.build.flags.ranlib, "my-ranlib"); + assert_eq!(spec.build.flags.strip, "my-strip"); assert_eq!(spec.build.flags.ld, "ld.lld"); + assert_eq!(spec.build.flags.nm, "my-nm"); + assert_eq!(spec.build.flags.objcopy, "my-objcopy"); + assert_eq!(spec.build.flags.objdump, "my-objdump"); + assert_eq!(spec.build.flags.readelf, "my-readelf"); assert_eq!(spec.build.flags.cpp, "clang-cpp"); + assert_eq!(spec.build.flags.tool_dir, "/opt/toolchain/bin"); assert!(spec.build.flags.cflags.contains(&"-O2".to_string())); assert!(spec.build.flags.cflags.contains(&"-g".to_string())); assert!( @@ -2873,6 +3009,33 @@ post_configure = ["echo configured"] ); } + #[test] + fn test_apply_config_preserves_package_scalar_tool_and_layout_overrides() { + let mut spec = mk_spec("foo", "1.0"); + spec.build.flags.ld = "ld.lld".to_string(); + spec.build.flags.libdir = "/package/lib".to_string(); + spec.build.flags.sysconfdir = "/package/etc".to_string(); + let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); + config.build_overrides = toml::from_str( + r#" +ld = "/config/bin/ld" +fuse_ld = "/config/bin/ld.lld" +ranlib = "/config/bin/ranlib" +libdir = "/config/lib" +sysconfdir = "/config/etc" +"#, + ) + .unwrap(); + + spec.apply_config(&config); + + assert_eq!(spec.build.flags.ld, "ld.lld"); + assert_eq!(spec.build.flags.libdir, "/package/lib"); + assert_eq!(spec.build.flags.sysconfdir, "/package/etc"); + assert_eq!(spec.build.flags.ranlib, "/config/bin/ranlib"); + assert_eq!(spec.build.flags.fuse_ld, "/config/bin/ld.lld"); + } + #[test] fn parse_no_flags_from_spec() { let tmp = tempfile::tempdir().unwrap(); @@ -2906,6 +3069,61 @@ no_flags = true assert!(spec.build.flags.no_flags); } + #[test] + fn parse_tool_commands_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] +cc = "/tools/bin/cc" +cxx = "/tools/bin/c++" +ar = "/tools/bin/ar" +ranlib = "/tools/bin/ranlib" +strip = "/tools/bin/strip" +ld = "/tools/bin/ld" +fuse_ld = "/usr/bin/ld.lld" +nm = "/tools/bin/nm" +objcopy = "/tools/bin/objcopy" +objdump = "/tools/bin/objdump" +readelf = "/tools/bin/readelf" +cpp = "/tools/bin/cpp" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.cc, "/tools/bin/cc"); + assert_eq!(spec.build.flags.cxx, "/tools/bin/c++"); + assert_eq!(spec.build.flags.ar, "/tools/bin/ar"); + assert_eq!(spec.build.flags.ranlib, "/tools/bin/ranlib"); + assert_eq!(spec.build.flags.strip, "/tools/bin/strip"); + assert_eq!(spec.build.flags.ld, "/tools/bin/ld"); + assert_eq!(spec.build.flags.fuse_ld, "/usr/bin/ld.lld"); + assert_eq!(spec.build.flags.nm, "/tools/bin/nm"); + assert_eq!(spec.build.flags.objcopy, "/tools/bin/objcopy"); + assert_eq!(spec.build.flags.objdump, "/tools/bin/objdump"); + assert_eq!(spec.build.flags.readelf, "/tools/bin/readelf"); + assert_eq!(spec.build.flags.cpp, "/tools/bin/cpp"); + } + #[test] fn parse_ltoflags_and_use_lto_from_spec() { let tmp = tempfile::tempdir().unwrap(); @@ -4282,6 +4500,9 @@ pub struct BuildFlags { /// Extra flags exported to `LDFLAGS`. #[serde(default, deserialize_with = "deserialize_string_or_array")] pub ldflags: Vec, + /// Linker selected through compiler drivers with `-fuse-ld=`. + #[serde(default, alias = "fuse-ld")] + pub fuse_ld: String, /// Ordered replacement rules applied to `ldflags` before export. #[serde( default, @@ -4416,6 +4637,9 @@ pub struct BuildFlags { /// Autotools configure script path, relative to source root or absolute. #[serde(default, alias = "configure-file")] pub configure_file: String, + /// Directory containing the configured compiler, linker, and binutils tools. + #[serde(default, alias = "tool-dir", alias = "tools_dir", alias = "tools-dir")] + pub tool_dir: String, /// C compiler #[serde(default = "default_cc")] pub cc: String, @@ -4425,9 +4649,27 @@ pub struct BuildFlags { /// Archiver #[serde(default = "default_ar")] pub ar: String, + /// Archive indexer exported as `RANLIB` when configured. + #[serde(default)] + pub ranlib: String, + /// Strip executable exported as `STRIP` when configured. + #[serde(default)] + pub strip: String, /// Linker executable or linker flavor override for supported builders. #[serde(default)] pub ld: String, + /// Symbol table dumper exported as `NM` when configured. + #[serde(default)] + pub nm: String, + /// Object copy tool exported as `OBJCOPY` when configured. + #[serde(default)] + pub objcopy: String, + /// Object dump tool exported as `OBJDUMP` when configured. + #[serde(default)] + pub objdump: String, + /// ELF reader exported as `READELF` when configured. + #[serde(default)] + pub readelf: String, /// C preprocessor executable exported as `CPP` when configured. #[serde(default, alias = "CPP")] pub cpp: String, @@ -4700,6 +4942,7 @@ impl Default for BuildFlags { cxxflags_lib32: Vec::new(), replace_cxxflags_lib32: Vec::new(), ldflags: Vec::new(), + fuse_ld: String::new(), replace_ldflags: Vec::new(), ltoflags: Vec::new(), rustltoflags: Vec::new(), @@ -4720,10 +4963,17 @@ impl Default for BuildFlags { config_settings: Vec::new(), configure_lib32: Vec::new(), configure_file: String::new(), + tool_dir: String::new(), cc: default_cc(), cxx: default_cxx(), ar: default_ar(), + ranlib: String::new(), + strip: String::new(), ld: String::new(), + nm: String::new(), + objcopy: String::new(), + objdump: String::new(), + readelf: String::new(), cpp: String::new(), libc: String::new(), rootfs: default_rootfs(), diff --git a/src/staging/mod.rs b/src/staging/mod.rs index 640f189..7eff484 100755 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -531,8 +531,14 @@ fn is_elf_file(path: &Path) -> Result { Ok(n == 4 && magic == [0x7F, b'E', b'L', b'F']) } -fn auto_strip_elf_files(destdir: &Path) -> Result { +fn auto_strip_elf_files(destdir: &Path, strip_command: &str) -> Result { let mut stripped = 0usize; + let strip_command = strip_command.trim(); + let strip_command = if strip_command.is_empty() { + "strip" + } else { + strip_command + }; for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) { if !entry.file_type().is_file() { continue; @@ -542,19 +548,21 @@ fn auto_strip_elf_files(destdir: &Path) -> Result { continue; } - let status = Command::new("strip") + let status = Command::new(strip_command) .arg("--strip-debug") .arg(path) .status() .with_context(|| { format!( - "Failed to execute strip for {} (disable with build.flags.no_strip = true)", + "Failed to execute {} for {} (disable with build.flags.no_strip = true)", + strip_command, path.display() ) })?; if !status.success() { anyhow::bail!( - "strip failed for {} with status {} (disable with build.flags.no_strip = true)", + "{} failed for {} with status {} (disable with build.flags.no_strip = true)", + strip_command, path.display(), status ); @@ -878,7 +886,7 @@ pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> { if spec.build.flags.no_strip { crate::log_info!("Skipping auto-strip: disabled by build.flags.no_strip"); } else { - let stripped = auto_strip_elf_files(destdir)?; + let stripped = auto_strip_elf_files(destdir, &spec.build.flags.strip)?; if stripped > 0 { crate::log_info!("Stripped {} ELF file(s)", stripped); } @@ -968,7 +976,14 @@ fn normalize_lbi_tree_paths(root: &Path) -> Result { let mut changed = 0usize; for (from, to) in mappings { let src = root.join(from); - if !src.exists() { + let src_meta = match fs::symlink_metadata(&src) { + Ok(metadata) => metadata, + Err(err) if err.kind() == io::ErrorKind::NotFound => continue, + Err(err) => { + return Err(err).with_context(|| format!("Failed to inspect {}", src.display())); + } + }; + if src_meta.file_type().is_symlink() { continue; } let dst = root.join(to); @@ -1242,7 +1257,10 @@ fn copy_tree_preserving_layout_no_overwrite( .with_context(|| format!("Failed to create directory {}", parent.display()))?; } - if dst_path.symlink_metadata().is_ok() { + 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() @@ -1281,6 +1299,65 @@ fn copy_tree_preserving_layout_no_overwrite( Ok(()) } +fn symlink_target_path_inside_rootfs( + rootfs: &Path, + link_rel: &str, + target: &Path, +) -> Result> { + 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))) +} + +fn can_relocate_directory_for_symlink_swap( + rootfs: &Path, + link_rel: &str, + symlink_path: &Path, +) -> Result { + 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())), + } +} + fn remove_path_in_place(path: &Path, rel: &str) -> Result<()> { let metadata = path .symlink_metadata() @@ -1530,9 +1607,25 @@ impl FsTransaction { /// 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(()) } } @@ -1548,6 +1641,11 @@ pub fn install_atomic( remove_paths: &[String], keep_paths: &[String], ) -> Result { + 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 = keep_paths .iter() .map(|p| KeepMatcher::from_spec(p)) @@ -1561,7 +1659,7 @@ pub fn install_atomic( .collect(); let remove_set: HashSet<&str> = remove_paths.iter().map(String::as_str).collect(); - fs::create_dir_all(tx_base_dir) + fs::create_dir_all(&tx_base_dir) .with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?; let ts = SystemTime::now() @@ -1655,7 +1753,13 @@ pub fn install_atomic( 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() { - if !remove_set.contains(install_rel_path.as_str()) { + 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 @@ -1684,7 +1788,11 @@ pub fn install_atomic( // 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() { - if !remove_set.contains(install_rel_path.as_str()) { + 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 @@ -1703,7 +1811,8 @@ pub fn install_atomic( } if file_type.is_symlink() { - let target = fs::read_link(src_path)?; + 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)?; @@ -1949,6 +2058,49 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn process_preserves_lbi_compatibility_symlinks() { + use std::os::unix::fs as unix_fs; + + let tmp = tempfile::tempdir().unwrap(); + let destdir = tmp.path().join("dest"); + std::fs::create_dir_all(destdir.join("system/binaries")).unwrap(); + std::fs::create_dir_all(destdir.join("system/documentation/man-pages")).unwrap(); + std::fs::create_dir_all(destdir.join("system/share")).unwrap(); + std::fs::create_dir_all(destdir.join("usr")).unwrap(); + + unix_fs::symlink("system/binaries", destdir.join("bin")).unwrap(); + unix_fs::symlink("../system/binaries", destdir.join("usr/bin")).unwrap(); + unix_fs::symlink("../system/share", destdir.join("usr/share")).unwrap(); + unix_fs::symlink( + "../documentation/man-pages", + destdir.join("system/share/man"), + ) + .unwrap(); + + let spec = mk_spec_for_stage_processing(); + process(&destdir, &spec).unwrap(); + + assert_eq!( + std::fs::read_link(destdir.join("bin")).unwrap(), + PathBuf::from("system/binaries") + ); + assert_eq!( + std::fs::read_link(destdir.join("usr/bin")).unwrap(), + PathBuf::from("../system/binaries") + ); + assert_eq!( + std::fs::read_link(destdir.join("usr/share")).unwrap(), + PathBuf::from("../system/share") + ); + assert_eq!( + std::fs::read_link(destdir.join("system/share/man")).unwrap(), + PathBuf::from("../documentation/man-pages") + ); + assert!(destdir.join("system/documentation/man-pages").is_dir()); + } + #[test] fn process_rejects_conflicting_lbi_normalized_paths() { let tmp = tempfile::tempdir().unwrap(); @@ -2256,6 +2408,43 @@ mod tests { ); } + #[test] + #[cfg(unix)] + fn install_atomic_relocates_existing_directory_into_packaged_symlink_target() { + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path().join("root"); + let destdir = tmp.path().join("dest"); + let tx_base = rootfs.join("var/cache/depot/build/tx"); + std::fs::create_dir_all(rootfs.join("var/lib/depot")).unwrap(); + std::fs::write(rootfs.join("var/lib/depot/lock"), "state").unwrap(); + std::fs::create_dir_all(rootfs.join("system/variable/lib/depot")).unwrap(); + std::fs::write(rootfs.join("system/variable/lib/depot/lock"), "state").unwrap(); + std::fs::create_dir_all(destdir.join("system/variable/lib/misc")).unwrap(); + std::os::unix::fs::symlink("system/variable", destdir.join("var")).unwrap(); + + let tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap(); + + let var_meta = rootfs.join("var").symlink_metadata().unwrap(); + assert!(var_meta.file_type().is_symlink()); + assert_eq!( + std::fs::read_link(rootfs.join("var")).unwrap(), + PathBuf::from("system/variable") + ); + assert_eq!( + std::fs::read_to_string(rootfs.join("system/variable/lib/depot/lock")).unwrap(), + "state" + ); + assert!(rootfs.join("system/variable/lib/misc").is_dir()); + + tx.rollback().unwrap(); + let restored = rootfs.join("var").symlink_metadata().unwrap(); + assert!(restored.file_type().is_dir()); + assert_eq!( + std::fs::read_to_string(rootfs.join("var/lib/depot/lock")).unwrap(), + "state" + ); + } + #[test] #[cfg(unix)] fn install_atomic_replaces_obsolete_directory_with_symlink() { diff --git a/src/system_state.rs b/src/system_state.rs index 1efd1ad..3655cea 100644 --- a/src/system_state.rs +++ b/src/system_state.rs @@ -348,10 +348,11 @@ datarootdir = "/system/share" datadir = "/system/share" mandir = "/system/documentation/man-pages" infodir = "/system/documentation/info" -cc = "clang" -cxx = "clang++" -ar = "llvm-ar" -ld = "ld.lld" +tool_dir = "/system/tools/bin" +cc = "$TOOL_DIR/clang" +cxx = "$TOOL_DIR/clang++" +ar = "$TOOL_DIR/llvm-ar" +ld = "$TOOL_DIR/ld.lld" carch = "{arch}" chost = "{target}" target = "{target}"