Simplify Depot staging and preserve filesystem topology
Remove the old LBI bootstrap/system-state path and make Depot stage and install packages against standard FHS paths instead of rewriting payloads into the custom /system layout. This drops the bootstrap command surface, system-state storage, /system path normalization, and bootstrap chroot helper while moving rootfs state defaults back under /var and aliases under /usr/bin. Preserve hardlink topology throughout the package lifecycle. Add shared fs_copy helpers for symlink/hardlink-aware tree copies, use them for source preparation, binary packages, Rust installs, and lib32 staging, and keep hardlinks intact during stripping, atomic installation, and package archive creation. Replace the external fakeroot wrapper with an internal user-namespace based install command wrapper. Install phases now run as UID/GID 0 inside a private namespace while mapping ownership back to the invoking user on the host. Split the package spec parser into loading, model, config, and tests modules without changing the public PackageSpec surface. Keep strict unknown-key handling, manual source validation, config/appends support, lib32 overrides, and generated output helpers in the new layout. Tighten build and dependency behavior: - emit explicit shared-library selectors when static builds are disabled - apply configure_arch entries for the effective target/lib32 architecture - add CMake sysroot defaults for non-live DEPOT_ROOTFS builds - resolve Rust source_subdir before build hooks and binary installation - satisfy dependencies and local sibling plans through package real_name aliases - accept bare git:// source URLs as HEAD checkouts Update tests around FHS path expectations, namespace fakeroot installs, hardlink-preserving copies/archives/installs/stripping, real_name dependency resolution, configure_arch expansion, CMake sysroot defaults, Rust source subdirs, and git:// URL parsing. Bump suppaftp to 8.0.4 and lzma-rust2 to 0.16.4. Bump Depot to 0.50.0
This commit is contained in:
Generated
+6
-6
@@ -542,7 +542,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
||||
|
||||
[[package]]
|
||||
name = "depot"
|
||||
version = "0.36.0"
|
||||
version = "0.50.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
@@ -1516,11 +1516,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.16.2"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
|
||||
checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02"
|
||||
dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"sha2 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2410,9 +2410,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "suppaftp"
|
||||
version = "8.0.3"
|
||||
version = "8.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4275c142b5be3af2eeadd70dd368caf3b65546c8af1035839372dd7a1436127d"
|
||||
checksum = "4cf00e4d8418c477a8cb3c13ae5396a68d31658e760c74280bdbd34926e3b94b"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures-lite",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "depot"
|
||||
version = "0.36.0"
|
||||
version = "0.50.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints.rust]
|
||||
@@ -43,7 +43,7 @@ zip = "8.5.0"
|
||||
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
||||
inquire = "0.9.4"
|
||||
md5 = "0.8.0"
|
||||
suppaftp = "8.0.2"
|
||||
suppaftp = "8.0.4"
|
||||
minisign = "0.9.1"
|
||||
petgraph = "0.8.3"
|
||||
fd-lock = "4.0.4"
|
||||
@@ -56,7 +56,7 @@ time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||
b2sum-rust = "0.3.0"
|
||||
serde_ignored = "0.1.14"
|
||||
lz4_flex = "0.13.1"
|
||||
lzma-rust2 = "0.16.2"
|
||||
lzma-rust2 = "0.16.4"
|
||||
signal-hook = "0.4.4"
|
||||
sha1 = "0.11.0"
|
||||
pdf-extract = "0.10.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.36.0',
|
||||
version: '0.50.0',
|
||||
meson_version: '>=0.60.0',
|
||||
default_options: ['buildtype=release'],
|
||||
)
|
||||
|
||||
-6811
File diff suppressed because it is too large
Load Diff
@@ -293,7 +293,7 @@ pub fn build(
|
||||
}
|
||||
|
||||
if !state.is_done(BuildStep::PostInstallDone) {
|
||||
// Run make install with fakeroot if not root
|
||||
// Run make install with internal fakeroot if not root
|
||||
crate::log_info!(
|
||||
"Running {} {}{}...",
|
||||
make_exec,
|
||||
|
||||
+1
-25
@@ -4,9 +4,7 @@ use crate::cross::CrossConfig;
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::os::unix::fs as unix_fs;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// For binary packages we simply copy the extracted files into DESTDIR (preserving
|
||||
/// directory structure). This is useful for .deb packages where extract step
|
||||
@@ -28,29 +26,7 @@ pub fn build(
|
||||
fs::create_dir_all(destdir)
|
||||
.with_context(|| format!("Failed to create destdir: {}", destdir.display()))?;
|
||||
|
||||
for entry in WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let rel = entry.path().strip_prefix(src_dir).unwrap();
|
||||
let target = destdir.join(rel);
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target)?;
|
||||
} else if entry.file_type().is_symlink() {
|
||||
let link_target = fs::read_link(entry.path())?;
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
// overwrite existing links/files
|
||||
if target.exists() {
|
||||
let _ = fs::remove_file(&target);
|
||||
}
|
||||
unix_fs::symlink(link_target, &target)?;
|
||||
} else {
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(entry.path(), &target)?;
|
||||
}
|
||||
}
|
||||
crate::fs_copy::copy_tree_preserving_links(src_dir, destdir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+87
-2
@@ -74,6 +74,9 @@ pub fn build(
|
||||
for arg in cmake_lib32_target_args(flags, cross) {
|
||||
cmake_cmd.arg(arg);
|
||||
}
|
||||
for arg in cmake_depot_sysroot_args(flags, depot_rootfs_from_env(&env_vars)) {
|
||||
cmake_cmd.arg(arg);
|
||||
}
|
||||
|
||||
// Add toolchain file for cross-compilation
|
||||
if let Some(ref tf) = toolchain_file {
|
||||
@@ -193,13 +196,13 @@ pub fn build(
|
||||
}
|
||||
|
||||
if !state.is_done(BuildStep::PostInstallDone) {
|
||||
// Run cmake install with fakeroot if not root
|
||||
// Run cmake install with internal fakeroot if not root
|
||||
crate::log_info!(
|
||||
"Running cmake install{}...",
|
||||
if fakeroot::is_root() {
|
||||
""
|
||||
} else {
|
||||
" (with fakeroot)"
|
||||
" (with internal fakeroot)"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -402,6 +405,9 @@ pub(crate) fn run_helper_configure(
|
||||
for arg in cmake_lib32_target_args(&flags, cross) {
|
||||
cmake_cmd.arg(arg);
|
||||
}
|
||||
for arg in cmake_depot_sysroot_args(&flags, depot_rootfs_from_env(env_vars)) {
|
||||
cmake_cmd.arg(arg);
|
||||
}
|
||||
if let Some(toolchain_file) = &toolchain_file {
|
||||
cmake_cmd.arg(format!(
|
||||
"-DCMAKE_TOOLCHAIN_FILE={}",
|
||||
@@ -686,6 +692,34 @@ fn cmake_lib32_target_args(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn cmake_depot_sysroot_args(flags: &crate::package::BuildFlags, depot_rootfs: &str) -> Vec<String> {
|
||||
let depot_rootfs = depot_rootfs.trim();
|
||||
if depot_rootfs.is_empty() || depot_rootfs == "/" {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let defaults = [
|
||||
("CMAKE_SYSROOT", depot_rootfs.to_string()),
|
||||
("CMAKE_FIND_ROOT_PATH_MODE_PROGRAM", "NEVER".to_string()),
|
||||
("CMAKE_FIND_ROOT_PATH_MODE_LIBRARY", "ONLY".to_string()),
|
||||
("CMAKE_FIND_ROOT_PATH_MODE_INCLUDE", "ONLY".to_string()),
|
||||
("CMAKE_FIND_ROOT_PATH_MODE_PACKAGE", "ONLY".to_string()),
|
||||
];
|
||||
|
||||
defaults
|
||||
.into_iter()
|
||||
.filter(|(variable, _)| cmake_cache_entry_value(&flags.configure, variable).is_none())
|
||||
.map(|(variable, value)| format!("-D{variable}={value}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn depot_rootfs_from_env(env_vars: &[(String, String)]) -> &str {
|
||||
env_vars
|
||||
.iter()
|
||||
.find_map(|(key, value)| (key == "DEPOT_ROOTFS").then_some(value.as_str()))
|
||||
.unwrap_or("/")
|
||||
}
|
||||
|
||||
fn lib32_target_triple(
|
||||
flags: &crate::package::BuildFlags,
|
||||
cross: Option<&CrossConfig>,
|
||||
@@ -1030,6 +1064,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmake_depot_sysroot_args_skip_live_rootfs() {
|
||||
let args = cmake_depot_sysroot_args(&BuildFlags::default(), "/");
|
||||
assert!(args.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmake_depot_sysroot_args_include_non_live_rootfs_defaults() {
|
||||
let args = cmake_depot_sysroot_args(&BuildFlags::default(), "/tmp/depot-root");
|
||||
assert!(args.iter().any(|a| a == "-DCMAKE_SYSROOT=/tmp/depot-root"));
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY")
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY")
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmake_depot_sysroot_args_respect_explicit_configure_overrides() {
|
||||
let flags = BuildFlags {
|
||||
configure: vec![
|
||||
"-DCMAKE_SYSROOT=/opt/custom-root".into(),
|
||||
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY:STRING=BOTH".into(),
|
||||
],
|
||||
..BuildFlags::default()
|
||||
};
|
||||
|
||||
let args = cmake_depot_sysroot_args(&flags, "/tmp/depot-root");
|
||||
assert!(!args.iter().any(|a| a.starts_with("-DCMAKE_SYSROOT=")));
|
||||
assert!(
|
||||
!args
|
||||
.iter()
|
||||
.any(|a| a.starts_with("-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY="))
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmake_install_dir_args_respect_explicit_user_overrides() {
|
||||
let flags = BuildFlags {
|
||||
|
||||
+9
-44
@@ -134,7 +134,7 @@ pub fn build(
|
||||
let function_mode = custom_function_mode_enabled(&abs_build_script)?;
|
||||
if function_mode {
|
||||
crate::log_info!(
|
||||
"Running custom build script (function mode; fakeroot only during install)..."
|
||||
"Running custom build script (function mode; internal fakeroot only during install)..."
|
||||
);
|
||||
crate::log_info!(
|
||||
"Using custom build.sh function mode (per-output install functions enabled)"
|
||||
@@ -152,7 +152,7 @@ pub fn build(
|
||||
if fakeroot::is_root() {
|
||||
""
|
||||
} else {
|
||||
" (with fakeroot)"
|
||||
" (with internal fakeroot)"
|
||||
}
|
||||
);
|
||||
// Use POSIX `sh` (doing something wrong if your system doesn't have it...)
|
||||
@@ -580,15 +580,11 @@ depot_install_dev_pkg() {
|
||||
&build_sh,
|
||||
r#"#!/bin/sh
|
||||
depot_build() {
|
||||
if [ "${FAKEROOT_ACTIVE:-0}" = 1 ]; then
|
||||
echo yes > build-fakeroot.txt
|
||||
else
|
||||
echo no > build-fakeroot.txt
|
||||
fi
|
||||
id -u > build-uid.txt
|
||||
}
|
||||
depot_install() {
|
||||
mkdir -p "$DESTDIR/usr/share"
|
||||
echo installed > "$DESTDIR/usr/share/install-fakeroot.txt"
|
||||
id -u > "$DESTDIR/usr/share/install-uid.txt"
|
||||
}
|
||||
"#,
|
||||
)?;
|
||||
@@ -604,12 +600,12 @@ depot_install() {
|
||||
build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None)?;
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tmp_src.path().join("build-fakeroot.txt"))?,
|
||||
"no\n"
|
||||
std::fs::read_to_string(tmp_src.path().join("build-uid.txt"))?,
|
||||
format!("{}\n", nix::unistd::geteuid().as_raw())
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tmp_dest.path().join("usr/share/install-fakeroot.txt"))?,
|
||||
"installed\n"
|
||||
std::fs::read_to_string(tmp_dest.path().join("usr/share/install-uid.txt"))?,
|
||||
"0\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -626,12 +622,7 @@ depot_install() {
|
||||
let spec = mk_spec("custom-function-fakeroot-split", "1.0");
|
||||
let install_cmd =
|
||||
build_function_mode_install_command(&spec, install_destdir, build_script, true);
|
||||
let expected = if crate::fakeroot::is_root() {
|
||||
std::ffi::OsStr::new("sh")
|
||||
} else {
|
||||
std::ffi::OsStr::new("fakeroot")
|
||||
};
|
||||
assert_eq!(install_cmd.get_program(), expected);
|
||||
assert_eq!(install_cmd.get_program(), std::ffi::OsStr::new("sh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -710,7 +701,6 @@ exit 0
|
||||
fn test_build_lib32_stages_only_usr_lib_payload() -> Result<()> {
|
||||
let tmp_src = tempdir()?;
|
||||
let tmp_dest = tempdir()?;
|
||||
let tmp_tools = tempdir()?;
|
||||
|
||||
let build_sh = tmp_src.path().join("build.sh");
|
||||
std::fs::write(
|
||||
@@ -729,31 +719,6 @@ printf 'manpage' > "$DESTDIR/usr/share/man/man1/foo.1"
|
||||
std::fs::set_permissions(&build_sh, perms)?;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let fakeroot = tmp_tools.path().join("fakeroot");
|
||||
std::fs::write(
|
||||
&fakeroot,
|
||||
r#"#!/bin/sh
|
||||
if [ "$1" = "--" ]; then
|
||||
shift
|
||||
fi
|
||||
exec "$@"
|
||||
"#,
|
||||
)?;
|
||||
let mut perms = std::fs::metadata(&fakeroot)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&fakeroot, perms)?;
|
||||
}
|
||||
|
||||
let mut env = crate::test_support::TestEnv::new();
|
||||
let old_path = std::env::var("PATH").unwrap_or_default();
|
||||
env.set_var(
|
||||
"PATH",
|
||||
format!("{}:{}", tmp_tools.path().display(), old_path),
|
||||
);
|
||||
|
||||
let mut spec = mk_spec("custom-lib32", "1.0");
|
||||
spec.build.flags.lib32_variant = true;
|
||||
|
||||
|
||||
+29
-47
@@ -66,13 +66,13 @@ pub fn build(
|
||||
}
|
||||
|
||||
if !state.is_done(BuildStep::PostInstallDone) {
|
||||
// Run install commands with fakeroot
|
||||
// Run install commands with internal fakeroot
|
||||
crate::log_info!(
|
||||
"Running makefile install commands{}...",
|
||||
if crate::fakeroot::is_root() {
|
||||
""
|
||||
} else {
|
||||
" (with fakeroot)"
|
||||
" (with internal fakeroot)"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ pub fn build(
|
||||
let cmd_str = spec.expand_vars(cmd_str);
|
||||
crate::log_info!(" Executing: {}", cmd_str);
|
||||
|
||||
// We need to run each command under fakeroot
|
||||
// We need to run each command under internal fakeroot
|
||||
let mut cmd = crate::fakeroot::wrap_install_command("sh", &install_destdir);
|
||||
cmd.arg("-c").arg(&cmd_str);
|
||||
cmd.current_dir(src_dir);
|
||||
@@ -136,21 +136,10 @@ pub fn build(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo};
|
||||
use crate::test_support::TestEnv;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn write_executable(path: &std::path::Path, contents: &str) -> Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
fs::write(path, contents)?;
|
||||
let mut perms = fs::metadata(path)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(path, perms)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mk_spec(name: &str, version: &str) -> PackageSpec {
|
||||
PackageSpec {
|
||||
package: PackageInfo {
|
||||
@@ -189,24 +178,8 @@ mod tests {
|
||||
fn test_makefile_build_runs_commands() -> Result<()> {
|
||||
let tmp_src = tempdir()?;
|
||||
let tmp_dest = tempdir()?;
|
||||
let tmp_tools = tempdir()?;
|
||||
let src_path = tmp_src.path();
|
||||
let dest_path = tmp_dest.path();
|
||||
let tools_path = tmp_tools.path();
|
||||
|
||||
write_executable(
|
||||
&tools_path.join("fakeroot"),
|
||||
r#"#!/bin/sh
|
||||
if [ "$1" = "--" ]; then
|
||||
shift
|
||||
fi
|
||||
exec "$@"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut env = TestEnv::new();
|
||||
let old_path = std::env::var("PATH").unwrap_or_default();
|
||||
env.set_var("PATH", format!("{}:{}", tools_path.display(), old_path));
|
||||
|
||||
let mut spec = mk_spec("test-make", "1.0");
|
||||
spec.build.flags.makefile_commands = vec![
|
||||
@@ -241,28 +214,37 @@ exec "$@"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_makefile_install_preserves_staged_hardlinks() -> Result<()> {
|
||||
let tmp_src = tempdir()?;
|
||||
let tmp_dest = tempdir()?;
|
||||
let src_path = tmp_src.path();
|
||||
let dest_path = tmp_dest.path();
|
||||
|
||||
let mut spec = mk_spec("uutils-like", "1.0");
|
||||
spec.build.flags.makefile_install_commands = vec![
|
||||
"mkdir -p \"$DESTDIR/usr/bin\"".into(),
|
||||
"printf 'multicall' > \"$DESTDIR/usr/bin/uutils\"".into(),
|
||||
"ln \"$DESTDIR/usr/bin/uutils\" \"$DESTDIR/usr/bin/ls\"".into(),
|
||||
];
|
||||
|
||||
build(&spec, src_path, dest_path, None, true, None)?;
|
||||
|
||||
let uutils = dest_path.join("usr/bin/uutils").metadata()?;
|
||||
let ls = dest_path.join("usr/bin/ls").metadata()?;
|
||||
assert_eq!(uutils.ino(), ls.ino());
|
||||
assert_eq!(uutils.nlink(), 2);
|
||||
assert_eq!(ls.nlink(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_makefile_lib32_install_relocates_usr_lib_without_copying_other_paths() -> Result<()> {
|
||||
let tmp_src = tempdir()?;
|
||||
let tmp_dest = tempdir()?;
|
||||
let tmp_tools = tempdir()?;
|
||||
let src_path = tmp_src.path();
|
||||
let dest_path = tmp_dest.path();
|
||||
let tools_path = tmp_tools.path();
|
||||
|
||||
write_executable(
|
||||
&tools_path.join("fakeroot"),
|
||||
r#"#!/bin/sh
|
||||
if [ "$1" = "--" ]; then
|
||||
shift
|
||||
fi
|
||||
exec "$@"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut env = TestEnv::new();
|
||||
let old_path = std::env::var("PATH").unwrap_or_default();
|
||||
env.set_var("PATH", format!("{}:{}", tools_path.display(), old_path));
|
||||
|
||||
let mut spec = mk_spec("lib32-test-make", "1.0");
|
||||
spec.build.flags.lib32_variant = true;
|
||||
|
||||
@@ -141,13 +141,13 @@ pub fn build(
|
||||
}
|
||||
|
||||
if !state.is_done(BuildStep::PostInstallDone) {
|
||||
// Run meson install with fakeroot if not root
|
||||
// Run meson install with internal fakeroot if not root
|
||||
crate::log_info!(
|
||||
"Running meson install{}...",
|
||||
if fakeroot::is_root() {
|
||||
""
|
||||
} else {
|
||||
" (with fakeroot)"
|
||||
" (with internal fakeroot)"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -750,11 +750,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_meson_setup_args_include_configure_flags() {
|
||||
let mut flags = BuildFlags {
|
||||
let flags = BuildFlags {
|
||||
prefix: "/usr".to_string(),
|
||||
configure: vec!["-Dmanpages=false".to_string()],
|
||||
..BuildFlags::default()
|
||||
};
|
||||
flags.configure = vec!["-Dmanpages=false".to_string()];
|
||||
|
||||
let args = meson_setup_args(&flags, None, &[]);
|
||||
assert!(args.iter().any(|a| a == "-Dmanpages=false"));
|
||||
@@ -764,8 +764,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_meson_setup_args_expand_host_build_dir() {
|
||||
let mut flags = BuildFlags::default();
|
||||
flags.configure = vec!["-Dtools_dir=$DEPOT_BUILD_HOST_DIR/bin".into()];
|
||||
let flags = BuildFlags {
|
||||
configure: vec!["-Dtools_dir=$DEPOT_BUILD_HOST_DIR/bin".into()],
|
||||
..BuildFlags::default()
|
||||
};
|
||||
|
||||
let args = meson_setup_args(
|
||||
&flags,
|
||||
|
||||
+184
-99
@@ -15,10 +15,8 @@ use crate::cross::CrossConfig;
|
||||
use crate::package::{BuildFlags, BuildType, PackageSpec};
|
||||
use anyhow::{Context, Result};
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub type EnvVars = Vec<(String, String)>;
|
||||
pub(crate) const DEPOT_BUILD_HOST_DIR_ENV: &str = "DEPOT_BUILD_HOST_DIR";
|
||||
@@ -292,6 +290,10 @@ fn normalized_arch(arch: &str) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_arch_key(arch: &str) -> String {
|
||||
normalized_arch(arch).to_ascii_lowercase().replace('-', "_")
|
||||
}
|
||||
|
||||
fn lib32_arch_for(arch: &str) -> String {
|
||||
match normalized_arch(arch) {
|
||||
"x86_64" => "i686".to_string(),
|
||||
@@ -374,9 +376,48 @@ pub(crate) fn host_build_spec(spec: &PackageSpec) -> PackageSpec {
|
||||
host_spec.build.flags.carch = host_arch().to_string();
|
||||
host_spec.build.flags.host_build_dir = None;
|
||||
host_spec.build.flags.build_dir = Some(default_host_build_dir_name(&spec.build.flags));
|
||||
append_configure_for_arch(&mut host_spec.build.flags, host_arch());
|
||||
host_spec
|
||||
}
|
||||
|
||||
fn append_configure_for_target_arch(
|
||||
flags: &mut crate::package::BuildFlags,
|
||||
cross: Option<&CrossConfig>,
|
||||
kind: TargetBuildKind,
|
||||
) {
|
||||
let arch = effective_target_arch(flags, cross, kind);
|
||||
append_configure_for_arch(flags, &arch);
|
||||
}
|
||||
|
||||
fn append_configure_for_arch(flags: &mut crate::package::BuildFlags, arch: &str) {
|
||||
if flags.configure_arch.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let target_arch = normalized_arch_key(arch);
|
||||
let matching_args: Vec<String> = flags
|
||||
.configure_arch
|
||||
.iter()
|
||||
.filter(|(key, _)| normalized_arch_key(key) == target_arch)
|
||||
.flat_map(|(_, values)| values.iter().cloned())
|
||||
.collect();
|
||||
flags.configure.extend(matching_args);
|
||||
}
|
||||
|
||||
fn spec_with_target_configure(
|
||||
spec: &PackageSpec,
|
||||
cross: Option<&CrossConfig>,
|
||||
kind: TargetBuildKind,
|
||||
) -> Option<PackageSpec> {
|
||||
if spec.build.flags.configure_arch.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut spec = spec.clone();
|
||||
append_configure_for_target_arch(&mut spec.build.flags, cross, kind);
|
||||
Some(spec)
|
||||
}
|
||||
|
||||
pub(crate) fn requested_static_build() -> Result<Option<bool>> {
|
||||
crate::build_options::requested_static_build()
|
||||
}
|
||||
@@ -395,11 +436,16 @@ fn static_build_args_for_request(
|
||||
}
|
||||
|
||||
match build_type {
|
||||
BuildType::Autotools => vec![if enabled {
|
||||
"--enable-static".to_string()
|
||||
BuildType::Autotools => {
|
||||
if enabled {
|
||||
vec!["--enable-static".to_string()]
|
||||
} else {
|
||||
"--disable-static".to_string()
|
||||
}],
|
||||
vec![
|
||||
"--enable-shared".to_string(),
|
||||
"--disable-static".to_string(),
|
||||
]
|
||||
}
|
||||
}
|
||||
BuildType::CMake => vec![format!(
|
||||
"-DBUILD_SHARED_LIBS={}",
|
||||
if enabled { "OFF" } else { "ON" }
|
||||
@@ -586,64 +632,10 @@ pub(crate) fn install_destdir_path(
|
||||
|
||||
pub(crate) fn stage_lib32_install_tree(staging_destdir: &Path, destdir: &Path) -> Result<()> {
|
||||
let lib_rel = lib32_stage_source_rel(staging_destdir)?;
|
||||
copy_tree_preserving_links(&staging_destdir.join(&lib_rel), &destdir.join("usr/lib32"))
|
||||
}
|
||||
|
||||
fn copy_tree_preserving_links(src: &Path, dst: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dst)
|
||||
.with_context(|| format!("Failed to create destination dir: {}", dst.display()))?;
|
||||
|
||||
for entry in WalkDir::new(src) {
|
||||
let entry = entry?;
|
||||
let rel = entry
|
||||
.path()
|
||||
.strip_prefix(src)
|
||||
.with_context(|| format!("Failed to strip prefix: {}", src.display()))?;
|
||||
let target = dst.join(rel);
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target)
|
||||
.with_context(|| format!("Failed to create dir: {}", target.display()))?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create dir: {}", parent.display()))?;
|
||||
}
|
||||
|
||||
if entry.file_type().is_symlink() {
|
||||
let link_target = fs::read_link(entry.path())
|
||||
.with_context(|| format!("Failed to read symlink: {}", entry.path().display()))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs as unix_fs;
|
||||
unix_fs::symlink(&link_target, &target).with_context(|| {
|
||||
format!(
|
||||
"Failed to create symlink {} -> {}",
|
||||
target.display(),
|
||||
link_target.display()
|
||||
crate::fs_copy::copy_tree_preserving_links(
|
||||
&staging_destdir.join(&lib_rel),
|
||||
&destdir.join("usr/lib32"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"Symlink-preserving lib32 staging copy is only supported on unix hosts"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fs::copy(entry.path(), &target).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy {} to {}",
|
||||
entry.path().display(),
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn lib32_stage_source_rel(staging_destdir: &Path) -> Result<PathBuf> {
|
||||
let staged_lib32 = PathBuf::from("usr/lib32");
|
||||
@@ -1028,6 +1020,14 @@ pub fn build(
|
||||
export_compiler_flags: bool,
|
||||
host_build_dir: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
let target_kind = if spec.build.flags.lib32_variant {
|
||||
TargetBuildKind::Lib32
|
||||
} else {
|
||||
TargetBuildKind::Primary
|
||||
};
|
||||
let target_configured_spec = spec_with_target_configure(spec, cross, target_kind);
|
||||
let spec = target_configured_spec.as_ref().unwrap_or(spec);
|
||||
|
||||
if let Some(cc) = cross {
|
||||
crate::log_info!(
|
||||
"Cross-compiling for {} with {:?}...",
|
||||
@@ -1131,6 +1131,7 @@ mod tests {
|
||||
use crate::test_support::TestEnv;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn mk_spec(cflags: Vec<&str>, ldflags: Vec<&str>) -> PackageSpec {
|
||||
@@ -1284,7 +1285,22 @@ mod tests {
|
||||
fn test_static_build_args_keep_other_requested_modes() {
|
||||
assert_eq!(
|
||||
static_build_args_for_request(BuildType::Autotools, Some(false), false),
|
||||
vec!["--disable-static".to_string()]
|
||||
vec![
|
||||
"--enable-shared".to_string(),
|
||||
"--disable-static".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
static_build_args_for_request(BuildType::CMake, Some(false), false),
|
||||
vec!["-DBUILD_SHARED_LIBS=ON".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
static_build_args_for_request(BuildType::Meson, Some(false), false),
|
||||
vec!["-Ddefault_library=shared".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
static_build_args_for_request(BuildType::Perl, Some(false), false),
|
||||
vec!["LINKTYPE=dynamic".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
static_build_args_for_request(BuildType::Meson, Some(true), true),
|
||||
@@ -1463,6 +1479,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spec_with_target_configure_appends_matching_arch_args() {
|
||||
let mut spec = mk_spec(Vec::new(), Vec::new());
|
||||
spec.build.flags.configure = vec!["--base".to_string()];
|
||||
spec.build
|
||||
.flags
|
||||
.configure_arch
|
||||
.insert("aarch64".to_string(), vec!["--for-aarch64".to_string()]);
|
||||
spec.build
|
||||
.flags
|
||||
.configure_arch
|
||||
.insert("x86_64".to_string(), vec!["--for-x86".to_string()]);
|
||||
let cross = CrossConfig {
|
||||
prefix: "aarch64-linux-gnu".into(),
|
||||
cc: "aarch64-linux-gnu-gcc".into(),
|
||||
cxx: "aarch64-linux-gnu-g++".into(),
|
||||
ar: "aarch64-linux-gnu-ar".into(),
|
||||
ranlib: "aarch64-linux-gnu-ranlib".into(),
|
||||
strip: "aarch64-linux-gnu-strip".into(),
|
||||
ld: "aarch64-linux-gnu-ld".into(),
|
||||
nm: "aarch64-linux-gnu-nm".into(),
|
||||
objcopy: "aarch64-linux-gnu-objcopy".into(),
|
||||
objdump: "aarch64-linux-gnu-objdump".into(),
|
||||
readelf: "aarch64-linux-gnu-readelf".into(),
|
||||
};
|
||||
|
||||
let adjusted = spec_with_target_configure(&spec, Some(&cross), TargetBuildKind::Primary)
|
||||
.expect("expected arch-specific configure args");
|
||||
|
||||
assert_eq!(
|
||||
adjusted.build.flags.configure,
|
||||
vec!["--base".to_string(), "--for-aarch64".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spec_with_target_configure_uses_lib32_arch() {
|
||||
let mut spec = mk_spec(Vec::new(), Vec::new());
|
||||
spec.build.flags.lib32_variant = true;
|
||||
spec.build.flags.carch = "x86_64".to_string();
|
||||
spec.build
|
||||
.flags
|
||||
.configure_arch
|
||||
.insert("i686".to_string(), vec!["--for-lib32".to_string()]);
|
||||
|
||||
let adjusted = spec_with_target_configure(&spec, None, TargetBuildKind::Lib32)
|
||||
.expect("expected lib32 configure args");
|
||||
|
||||
assert_eq!(
|
||||
adjusted.build.flags.configure,
|
||||
vec!["--for-lib32".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_standard_build_env_respects_export_compiler_flags_toggle() {
|
||||
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
|
||||
@@ -1686,44 +1756,35 @@ 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();
|
||||
spec.build.flags.prefix = "/opt/vertex".into();
|
||||
spec.build.flags.bindir = "/opt/vertex/bin".into();
|
||||
spec.build.flags.sbindir = "/opt/vertex/sbin".into();
|
||||
spec.build.flags.libdir = "/opt/vertex/lib".into();
|
||||
spec.build.flags.libexecdir = "/opt/vertex/libexec".into();
|
||||
spec.build.flags.sysconfdir = "/etc/vertex".into();
|
||||
spec.build.flags.localstatedir = "/var".into();
|
||||
spec.build.flags.sharedstatedir = "/var/lib".into();
|
||||
spec.build.flags.includedir = "/opt/vertex/include".into();
|
||||
spec.build.flags.datarootdir = "/opt/vertex/share".into();
|
||||
spec.build.flags.datadir = "/opt/vertex/share/data".into();
|
||||
spec.build.flags.mandir = "/opt/vertex/share/man".into();
|
||||
spec.build.flags.infodir = "/opt/vertex/share/info".into();
|
||||
|
||||
let env = standard_build_env(&spec, None, false, true);
|
||||
|
||||
assert_eq!(env_value(&env, "PREFIX"), Some("/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")
|
||||
);
|
||||
assert_eq!(env_value(&env, "PREFIX"), Some("/opt/vertex"));
|
||||
assert_eq!(env_value(&env, "BINDIR"), Some("/opt/vertex/bin"));
|
||||
assert_eq!(env_value(&env, "SBINDIR"), Some("/opt/vertex/sbin"));
|
||||
assert_eq!(env_value(&env, "LIBDIR"), Some("/opt/vertex/lib"));
|
||||
assert_eq!(env_value(&env, "LIBEXECDIR"), Some("/opt/vertex/libexec"));
|
||||
assert_eq!(env_value(&env, "SYSCONFDIR"), Some("/etc/vertex"));
|
||||
assert_eq!(env_value(&env, "LOCALSTATEDIR"), Some("/var"));
|
||||
assert_eq!(env_value(&env, "SHAREDSTATEDIR"), Some("/var/lib"));
|
||||
assert_eq!(env_value(&env, "INCLUDEDIR"), Some("/opt/vertex/include"));
|
||||
assert_eq!(env_value(&env, "DATAROOTDIR"), Some("/opt/vertex/share"));
|
||||
assert_eq!(env_value(&env, "DATADIR"), Some("/opt/vertex/share/data"));
|
||||
assert_eq!(env_value(&env, "MANDIR"), Some("/opt/vertex/share/man"));
|
||||
assert_eq!(env_value(&env, "INFODIR"), Some("/opt/vertex/share/info"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1952,4 +2013,28 @@ mod tests {
|
||||
assert!(!dest.join("usr/lib").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stage_lib32_install_tree_preserves_hardlinks() -> Result<()> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let temp = tempfile::tempdir()?;
|
||||
let staging = temp.path().join("staging");
|
||||
let dest = temp.path().join("dest");
|
||||
fs::create_dir_all(staging.join("usr/lib32"))?;
|
||||
fs::write(staging.join("usr/lib32/libfoo.so.1"), "lib32")?;
|
||||
fs::hard_link(
|
||||
staging.join("usr/lib32/libfoo.so.1"),
|
||||
staging.join("usr/lib32/libfoo-current.so"),
|
||||
)?;
|
||||
|
||||
stage_lib32_install_tree(&staging, &dest)?;
|
||||
|
||||
let first = dest.join("usr/lib32/libfoo.so.1").metadata()?;
|
||||
let second = dest.join("usr/lib32/libfoo-current.so").metadata()?;
|
||||
assert_eq!(first.ino(), second.ino());
|
||||
assert_eq!(first.nlink(), 2);
|
||||
assert_eq!(second.nlink(), 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,16 +598,6 @@ case "$target" in
|
||||
esac
|
||||
"#,
|
||||
)?;
|
||||
write_executable(
|
||||
&tools_path.join("fakeroot"),
|
||||
r#"#!/bin/sh
|
||||
if [ "$1" = "--" ]; then
|
||||
shift
|
||||
fi
|
||||
exec "$@"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut env = TestEnv::new();
|
||||
let old_path = std::env::var("PATH").unwrap_or_default();
|
||||
env.set_var("PATH", format!("{}:{}", tools_path.display(), old_path));
|
||||
|
||||
+111
-12
@@ -3,9 +3,9 @@
|
||||
use crate::cross::CrossConfig;
|
||||
use crate::package::PackageSpec;
|
||||
use crate::source::hooks;
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub fn build(
|
||||
@@ -17,13 +17,14 @@ pub fn build(
|
||||
_host_build_dir: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
let flags = &spec.build.flags;
|
||||
let actual_src = resolve_actual_src(spec, src_dir)?;
|
||||
|
||||
// Create destdir
|
||||
fs::create_dir_all(destdir)?;
|
||||
|
||||
// Isolate from parent workspace by adding empty [workspace] if not present
|
||||
// This prevents "believes it's in a workspace when it's not" errors
|
||||
let cargo_toml = src_dir.join("Cargo.toml");
|
||||
let cargo_toml = actual_src.join("Cargo.toml");
|
||||
if cargo_toml.exists() {
|
||||
let contents = fs::read_to_string(&cargo_toml)
|
||||
.with_context(|| format!("Failed to read {}", cargo_toml.display()))?;
|
||||
@@ -75,7 +76,7 @@ pub fn build(
|
||||
crate::builder::set_env_var(&mut env_vars, "RUSTUP_TOOLCHAIN", "stable");
|
||||
}
|
||||
|
||||
hooks::run_post_configure_commands(spec, src_dir, destdir)?;
|
||||
hooks::run_post_configure_commands(spec, &actual_src, destdir)?;
|
||||
|
||||
// Run cargo build
|
||||
crate::log_info!(
|
||||
@@ -83,7 +84,7 @@ pub fn build(
|
||||
if is_release { "release" } else { "debug" }
|
||||
);
|
||||
let mut cargo_cmd = Command::new("cargo");
|
||||
cargo_cmd.current_dir(src_dir);
|
||||
cargo_cmd.current_dir(&actual_src);
|
||||
cargo_cmd.arg("build");
|
||||
|
||||
if is_release {
|
||||
@@ -110,16 +111,16 @@ pub fn build(
|
||||
}
|
||||
|
||||
// Run post-compile hooks
|
||||
hooks::run_post_compile_commands(spec, src_dir, destdir)?;
|
||||
hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
|
||||
|
||||
// Install binaries to destdir
|
||||
crate::log_info!("Installing binaries to DESTDIR...");
|
||||
|
||||
// Determine target directory
|
||||
let target_dir = if let Some(ref t) = target {
|
||||
src_dir.join("target").join(t).join(profile_dir)
|
||||
actual_src.join("target").join(t).join(profile_dir)
|
||||
} else {
|
||||
src_dir.join("target").join(profile_dir)
|
||||
actual_src.join("target").join(profile_dir)
|
||||
};
|
||||
|
||||
// Use bindir from flags (default: /usr/bin)
|
||||
@@ -127,6 +128,7 @@ pub fn build(
|
||||
fs::create_dir_all(&bin_dir)?;
|
||||
|
||||
// Find and copy executable files
|
||||
let mut hardlink_tracker = crate::fs_copy::HardlinkCopyTracker::new();
|
||||
if target_dir.exists() {
|
||||
for entry in fs::read_dir(&target_dir)
|
||||
.with_context(|| format!("Failed to read target directory: {}", target_dir.display()))?
|
||||
@@ -164,9 +166,7 @@ pub fn build(
|
||||
{
|
||||
let dest = bin_dir.join(&*file_name);
|
||||
crate::log_info!(" Installing: {}", file_name);
|
||||
fs::copy(&path, &dest).with_context(|| {
|
||||
format!("Failed to copy {} to {}", path.display(), dest.display())
|
||||
})?;
|
||||
hardlink_tracker.copy_file(&path, &dest)?;
|
||||
|
||||
// Preserve executable permission
|
||||
let mut perms = fs::metadata(&dest)?.permissions();
|
||||
@@ -178,7 +178,106 @@ pub fn build(
|
||||
}
|
||||
|
||||
// Run post-install hooks
|
||||
hooks::run_post_install_commands(spec, src_dir, destdir)?;
|
||||
hooks::run_post_install_commands(spec, &actual_src, destdir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_actual_src(spec: &PackageSpec, src_dir: &Path) -> Result<PathBuf> {
|
||||
let source_subdir = spec.expand_vars(&spec.build.flags.source_subdir);
|
||||
if source_subdir.is_empty() {
|
||||
return Ok(src_dir.to_path_buf());
|
||||
}
|
||||
|
||||
let candidate = Path::new(&source_subdir);
|
||||
if candidate.is_absolute() {
|
||||
if candidate.exists() {
|
||||
return Ok(candidate.to_path_buf());
|
||||
}
|
||||
bail!(
|
||||
"Source directory not found: {} (source_subdir: {} -> {})",
|
||||
candidate.display(),
|
||||
spec.build.flags.source_subdir,
|
||||
source_subdir
|
||||
);
|
||||
}
|
||||
|
||||
let under_src = src_dir.join(&source_subdir);
|
||||
if under_src.exists() {
|
||||
return Ok(under_src);
|
||||
}
|
||||
|
||||
let under_spec = spec.spec_dir.join(&source_subdir);
|
||||
if under_spec.exists() {
|
||||
return Ok(under_spec);
|
||||
}
|
||||
|
||||
if candidate.exists() {
|
||||
return Ok(candidate.to_path_buf());
|
||||
}
|
||||
|
||||
bail!(
|
||||
"Source directory not found: {} (expanded from '{}'; tried src_dir, spec_dir, and absolute path)",
|
||||
source_subdir,
|
||||
spec.build.flags.source_subdir
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::package::{Build, BuildFlags, BuildType, PackageInfo};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn test_spec(spec_dir: PathBuf, source_subdir: &str) -> PackageSpec {
|
||||
PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: "red".into(),
|
||||
real_name: None,
|
||||
version: "1.0.2".into(),
|
||||
revision: 1,
|
||||
description: String::new(),
|
||||
homepage: String::new(),
|
||||
abi_breaking: false,
|
||||
license: vec!["MIT".into()],
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Default::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: Vec::new(),
|
||||
build: Build {
|
||||
build_type: BuildType::Rust,
|
||||
flags: BuildFlags {
|
||||
source_subdir: source_subdir.into(),
|
||||
..BuildFlags::default()
|
||||
},
|
||||
},
|
||||
dependencies: Default::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_actual_src_supports_source_subdir() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let src_dir = tmp.path().join("source");
|
||||
let nested = src_dir.join("red-1.0.2");
|
||||
fs::create_dir_all(&nested).unwrap();
|
||||
|
||||
let spec = test_spec(tmp.path().join("spec"), "$name-$version");
|
||||
assert_eq!(resolve_actual_src(&spec, &src_dir).unwrap(), nested);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_actual_src_rejects_missing_source_subdir() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let src_dir = tmp.path().join("source");
|
||||
fs::create_dir_all(&src_dir).unwrap();
|
||||
|
||||
let spec = test_spec(tmp.path().join("spec"), "missing");
|
||||
let error = resolve_actual_src(&spec, &src_dir).unwrap_err();
|
||||
assert!(error.to_string().contains("Source directory not found"));
|
||||
}
|
||||
}
|
||||
|
||||
-105
@@ -144,30 +144,6 @@ pub struct BuildArgs {
|
||||
pub cleanup_deps: bool,
|
||||
}
|
||||
|
||||
/// Arguments for importing the Linux by Intent book package plan into Depot layers.
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct BootstrapArgs {
|
||||
/// Target sysroot whose Depot system state should receive the parsed layers
|
||||
#[arg(value_name = "SYSROOT")]
|
||||
pub sysroot: PathBuf,
|
||||
|
||||
/// Target triple used for cross and staged bootstrap builds
|
||||
#[arg(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Target architecture component used by build defaults
|
||||
#[arg(long)]
|
||||
pub arch: Option<String>,
|
||||
|
||||
/// URL of the Linux by Intent PDF to parse
|
||||
#[arg(long, default_value = "https://www.vertexlinux.net/lbi/book.pdf")]
|
||||
pub book_url: String,
|
||||
|
||||
/// Use a local PDF instead of fetching book-url
|
||||
#[arg(long, value_name = "PDF")]
|
||||
pub book_pdf: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct UpdateArgs {
|
||||
#[command(flatten)]
|
||||
@@ -276,70 +252,6 @@ pub enum ToolRoleArg {
|
||||
Shell,
|
||||
}
|
||||
|
||||
/// Arguments for Depot system-build state management commands.
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct SystemArgs {
|
||||
/// Root filesystem whose system state should be inspected or modified.
|
||||
#[command(flatten)]
|
||||
pub rootfs_args: RootfsArgs,
|
||||
|
||||
/// Requested system state subcommand.
|
||||
#[command(subcommand)]
|
||||
pub command: SystemCommands,
|
||||
}
|
||||
|
||||
/// System-build state and Linux by Intent layout commands.
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum SystemCommands {
|
||||
/// Show tracked system build stage and package layers
|
||||
Status,
|
||||
/// Move the tracked system status to a build stage
|
||||
Stage {
|
||||
/// New stage name, such as cross-tools, minimal, chroot, stage2, or bootable
|
||||
stage: String,
|
||||
},
|
||||
/// Manage package layer membership
|
||||
Layer {
|
||||
#[command(subcommand)]
|
||||
command: SystemLayerCommands,
|
||||
},
|
||||
/// Initialize the Linux by Intent /system layout and Depot build defaults
|
||||
InitLbi {
|
||||
/// Target triple used for cross and staged builds
|
||||
#[arg(long, default_value = "x86_64-unknown-linux-musl")]
|
||||
target: String,
|
||||
/// Target architecture component used by build defaults
|
||||
#[arg(long)]
|
||||
arch: Option<String>,
|
||||
/// Replace an existing Depot build config generated for the target rootfs
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Package layer membership commands.
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum SystemLayerCommands {
|
||||
/// Add packages to a named layer
|
||||
Add {
|
||||
/// Layer name, such as base, devel, toolchain, or boot
|
||||
layer: String,
|
||||
/// Package names to add to the layer
|
||||
#[arg(required = true, num_args = 1..)]
|
||||
packages: Vec<String>,
|
||||
},
|
||||
/// Remove packages from a named layer
|
||||
Remove {
|
||||
/// Layer name
|
||||
layer: String,
|
||||
/// Package names to remove from the layer
|
||||
#[arg(required = true, num_args = 1..)]
|
||||
packages: Vec<String>,
|
||||
},
|
||||
/// List all tracked layers
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct GenerateArtifactsArgs {
|
||||
/// Output directory for generated files
|
||||
@@ -379,8 +291,6 @@ pub enum Commands {
|
||||
Remove(RemoveArgs),
|
||||
/// Build a package without installing
|
||||
Build(BuildArgs),
|
||||
/// Parse the Linux by Intent book and populate temp/base/devel layers
|
||||
Bootstrap(BootstrapArgs),
|
||||
/// Update installed packages from configured repositories
|
||||
Update(UpdateArgs),
|
||||
/// Scan package specs for upstream version updates
|
||||
@@ -401,8 +311,6 @@ pub enum Commands {
|
||||
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.
|
||||
#[command(hide = true)]
|
||||
GenerateArtifacts(GenerateArtifactsArgs),
|
||||
@@ -437,19 +345,6 @@ pub enum InternalCommands {
|
||||
#[command(hide = true)]
|
||||
Clone { repo: String, dest: Option<PathBuf> },
|
||||
#[command(hide = true)]
|
||||
BootstrapChroot {
|
||||
#[arg(long)]
|
||||
rootfs: PathBuf,
|
||||
#[arg(long)]
|
||||
sources: PathBuf,
|
||||
#[arg(long)]
|
||||
destdir: PathBuf,
|
||||
#[arg(long)]
|
||||
workdir: String,
|
||||
#[arg(long)]
|
||||
script: String,
|
||||
},
|
||||
#[command(hide = true)]
|
||||
AutotoolsConfigure {
|
||||
#[arg(value_name = "ARG", num_args = 0.., allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
|
||||
+4
-12
@@ -1,11 +1,11 @@
|
||||
use crate::cli::{
|
||||
BuildArgs, Cli, Commands, ConfigArgs, ConvertArgs, InfoArgs, InstallArgs, InternalCommands,
|
||||
ListArgs, OwnsArgs, RemoveArgs, RepoCommands, RepoKindArg, SearchArgs, SetArgs, SignArgs,
|
||||
SystemArgs, UpdateArgs,
|
||||
UpdateArgs,
|
||||
};
|
||||
use crate::{
|
||||
bootstrap, builder, cli_assets, config, cross, db, deps, index, install, locking, package,
|
||||
planner, signing, source, staging, ui,
|
||||
builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner,
|
||||
signing, source, staging, ui,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use git2::Direction;
|
||||
@@ -91,7 +91,6 @@ fn command_rootfs(command: &Commands) -> Option<&Path> {
|
||||
Commands::Install(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::Remove(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::Build(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::Bootstrap(args) => Some(&args.sysroot),
|
||||
Commands::Update(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::Info(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::Search(args) => Some(&args.rootfs_args.rootfs),
|
||||
@@ -101,7 +100,6 @@ fn command_rootfs(command: &Commands) -> Option<&Path> {
|
||||
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(_)
|
||||
| Commands::GenerateArtifacts(_)
|
||||
@@ -115,7 +113,6 @@ fn command_assume_yes(command: &Commands) -> bool {
|
||||
Commands::Install(args) => args.prompt_args.yes,
|
||||
Commands::Remove(args) => args.prompt_args.yes,
|
||||
Commands::Build(args) => args.prompt_args.yes,
|
||||
Commands::Bootstrap(_) => false,
|
||||
Commands::Update(args) => args.prompt_args.yes,
|
||||
Commands::Check(_)
|
||||
| Commands::Convert(_)
|
||||
@@ -127,7 +124,6 @@ fn command_assume_yes(command: &Commands) -> bool {
|
||||
| Commands::Repo(_)
|
||||
| Commands::Config(_)
|
||||
| Commands::Set(_)
|
||||
| Commands::System(_)
|
||||
| Commands::GenerateArtifacts(_)
|
||||
| Commands::MakeSpec(_)
|
||||
| Commands::Internal(_) => false,
|
||||
@@ -2585,8 +2581,7 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
Commands::Install(args) => args.build_exec_args.test_deps,
|
||||
Commands::Build(args) => args.build_exec_args.test_deps,
|
||||
Commands::Update(args) => args.build_exec_args.test_deps,
|
||||
Commands::Bootstrap(_)
|
||||
| Commands::Check(_)
|
||||
Commands::Check(_)
|
||||
| Commands::Remove(_)
|
||||
| Commands::Info(_)
|
||||
| Commands::Search(_)
|
||||
@@ -2596,7 +2591,6 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
| Commands::Repo(_)
|
||||
| Commands::Config(_)
|
||||
| Commands::Set(_)
|
||||
| Commands::System(_)
|
||||
| Commands::GenerateArtifacts(_)
|
||||
| Commands::Convert(_)
|
||||
| Commands::MakeSpec(_)
|
||||
@@ -2607,7 +2601,6 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
Commands::Install(args) => install_cmd::run_install(args, cli_test_deps)?,
|
||||
Commands::Remove(args) => install_cmd::run_remove(args)?,
|
||||
Commands::Build(args) => build_cmd::run_build(args, cli_test_deps)?,
|
||||
Commands::Bootstrap(args) => bootstrap::run(args)?,
|
||||
Commands::Update(args) => update::run_update(args, cli_test_deps)?,
|
||||
Commands::Check(args) => check::run_check(args)?,
|
||||
Commands::Info(args) => misc::run_info(args)?,
|
||||
@@ -2619,7 +2612,6 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
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)?,
|
||||
Commands::Internal(args) => misc::run_internal(args)?,
|
||||
|
||||
@@ -112,117 +112,6 @@ pub(super) fn run_config(args: ConfigArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn run_system(args: SystemArgs) -> Result<()> {
|
||||
let SystemArgs {
|
||||
rootfs_args,
|
||||
command,
|
||||
} = args;
|
||||
let rootfs = rootfs_args.rootfs;
|
||||
let config = config::Config::for_rootfs(&rootfs);
|
||||
let mut system_lock = locking::open_lock(&config)?;
|
||||
let system_lock_path = locking::lock_path(&config);
|
||||
|
||||
match command {
|
||||
crate::cli::SystemCommands::Status => {
|
||||
let _guard = locking::try_read(&system_lock, &system_lock_path, "system status")?;
|
||||
let state = crate::system_state::load(&config)?;
|
||||
print_system_state(&state);
|
||||
}
|
||||
crate::cli::SystemCommands::Stage { stage } => {
|
||||
let _guard = locking::try_write(&mut system_lock, &system_lock_path, "system stage")?;
|
||||
let state = crate::system_state::set_stage(&config, stage)?;
|
||||
ui::success(format!(
|
||||
"Moved system status to stage {}",
|
||||
state.stage.as_deref().unwrap_or("unknown")
|
||||
));
|
||||
}
|
||||
crate::cli::SystemCommands::Layer { command } => {
|
||||
let _guard = locking::try_write(&mut system_lock, &system_lock_path, "system layer")?;
|
||||
match command {
|
||||
crate::cli::SystemLayerCommands::Add { layer, packages } => {
|
||||
let package_count = packages.len();
|
||||
crate::system_state::add_packages_to_layer(&config, layer.clone(), &packages)?;
|
||||
ui::success(format!(
|
||||
"Added {} package(s) to layer {}",
|
||||
package_count, layer
|
||||
));
|
||||
}
|
||||
crate::cli::SystemLayerCommands::Remove { layer, packages } => {
|
||||
let package_count = packages.len();
|
||||
crate::system_state::remove_packages_from_layer(
|
||||
&config,
|
||||
layer.clone(),
|
||||
&packages,
|
||||
)?;
|
||||
ui::success(format!(
|
||||
"Removed {} package(s) from layer {}",
|
||||
package_count, layer
|
||||
));
|
||||
}
|
||||
crate::cli::SystemLayerCommands::List => {
|
||||
drop(_guard);
|
||||
let _guard =
|
||||
locking::try_read(&system_lock, &system_lock_path, "system layer list")?;
|
||||
let state = crate::system_state::load(&config)?;
|
||||
print_system_layers(&state);
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::cli::SystemCommands::InitLbi {
|
||||
target,
|
||||
arch,
|
||||
force,
|
||||
} => {
|
||||
let _guard =
|
||||
locking::try_write(&mut system_lock, &system_lock_path, "system init-lbi")?;
|
||||
let state = crate::system_state::init_lbi_layout(
|
||||
&rootfs,
|
||||
&config,
|
||||
&target,
|
||||
arch.as_deref(),
|
||||
force,
|
||||
)?;
|
||||
ui::success(format!(
|
||||
"Initialized Linux by Intent layout for {} ({})",
|
||||
state.target.as_deref().unwrap_or("unknown"),
|
||||
state.arch.as_deref().unwrap_or("unknown")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_system_state(state: &crate::system_state::SystemState) {
|
||||
println!(
|
||||
"Stage: {}",
|
||||
state.stage.as_deref().unwrap_or("uninitialized")
|
||||
);
|
||||
if let Some(target) = &state.target {
|
||||
println!("Target: {target}");
|
||||
}
|
||||
if let Some(arch) = &state.arch {
|
||||
println!("Arch: {arch}");
|
||||
}
|
||||
print_system_layers(state);
|
||||
}
|
||||
|
||||
fn print_system_layers(state: &crate::system_state::SystemState) {
|
||||
if state.layers.is_empty() {
|
||||
println!("Layers: none");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Layers:");
|
||||
for (layer, packages) in &state.layers {
|
||||
if packages.is_empty() {
|
||||
println!(" {layer}:");
|
||||
} else {
|
||||
println!(" {}: {}", layer, packages.join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn run_make_spec(args: crate::cli::MakeSpecArgs) -> Result<()> {
|
||||
let output = args.output;
|
||||
let spec = package::create_interactive()?;
|
||||
|
||||
@@ -144,13 +144,6 @@ pub(crate) fn run_internal_command(command: InternalCommands) -> Result<()> {
|
||||
&[],
|
||||
)
|
||||
}
|
||||
InternalCommands::BootstrapChroot {
|
||||
rootfs,
|
||||
sources,
|
||||
destdir,
|
||||
workdir,
|
||||
script,
|
||||
} => crate::bootstrap::run_bootstrap_chroot(&rootfs, &sources, &destdir, &workdir, &script),
|
||||
InternalCommands::AutotoolsConfigure { args } => {
|
||||
let env_vars = current_process_env_vars();
|
||||
let context = current_build_helper_context()?;
|
||||
@@ -312,27 +305,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_install_uses_fakeroot_and_destdir() -> Result<()> {
|
||||
fn cmake_install_uses_internal_fakeroot_and_destdir() -> Result<()> {
|
||||
let source = tempdir()?;
|
||||
let build_dir = source.path().join("build");
|
||||
let tools = tempdir()?;
|
||||
let fakeroot_log = tools.path().join("fakeroot.log");
|
||||
let cmake_log = tools.path().join("cmake.log");
|
||||
let destdir = source.path().join("dest");
|
||||
fs::create_dir_all(&build_dir)?;
|
||||
fs::create_dir_all(&destdir)?;
|
||||
|
||||
write_executable(
|
||||
&tools.path().join("fakeroot"),
|
||||
&format!(
|
||||
"#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\n[ \"$1\" = \"--\" ]\nshift\nexec \"$@\"\n",
|
||||
fakeroot_log.display()
|
||||
),
|
||||
)?;
|
||||
write_executable(
|
||||
&tools.path().join("cmake"),
|
||||
&format!(
|
||||
"#!/bin/sh\n{{ printf 'DESTDIR=%s\\n' \"$DESTDIR\"; printf '%s\\n' \"$@\"; }} > '{}'\n",
|
||||
"#!/bin/sh\n{{ printf 'UID=%s\\n' \"$(id -u)\"; printf 'DESTDIR=%s\\n' \"$DESTDIR\"; printf '%s\\n' \"$@\"; }} > '{}'\n",
|
||||
cmake_log.display()
|
||||
),
|
||||
)?;
|
||||
@@ -354,15 +339,8 @@ mod tests {
|
||||
args: vec!["--component".into(), "runtime".into()],
|
||||
})?;
|
||||
|
||||
if !crate::fakeroot::is_root() {
|
||||
let fakeroot_output = fs::read_to_string(&fakeroot_log)?;
|
||||
assert!(fakeroot_output.contains("--"));
|
||||
assert!(fakeroot_output.contains("cmake"));
|
||||
} else {
|
||||
assert!(!fakeroot_log.exists());
|
||||
}
|
||||
|
||||
let cmake_output = fs::read_to_string(&cmake_log)?;
|
||||
assert!(cmake_output.contains("UID=0"));
|
||||
assert!(cmake_output.contains(&format!("DESTDIR={}", destdir.display())));
|
||||
assert!(cmake_output.contains("--install"));
|
||||
assert!(cmake_output.contains(build_dir.to_string_lossy().as_ref()));
|
||||
|
||||
+7
-10
@@ -65,7 +65,7 @@ fn configured_alias_dir(config: &config::Config) -> PathBuf {
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("/system/binaries");
|
||||
.unwrap_or("/usr/bin");
|
||||
PathBuf::from(configured)
|
||||
}
|
||||
|
||||
@@ -242,12 +242,12 @@ mod tests {
|
||||
etc.join("build.toml"),
|
||||
r#"
|
||||
[flags]
|
||||
tool_dir = "/system/tools/bin"
|
||||
bindir = "/system/binaries"
|
||||
"#,
|
||||
tool_dir = "/usr/lib/depot/tools/bin"
|
||||
bindir = "/usr/bin"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let tool_dir = rootfs.join("system/binaries");
|
||||
let tool_dir = rootfs.join("usr/bin");
|
||||
fs::create_dir_all(&tool_dir).unwrap();
|
||||
tool_dir
|
||||
}
|
||||
@@ -258,17 +258,14 @@ bindir = "/system/binaries"
|
||||
make_tool_dir(tmp.path());
|
||||
let config = config::Config::for_rootfs(tmp.path());
|
||||
|
||||
assert_eq!(
|
||||
configured_alias_dir(&config),
|
||||
PathBuf::from("/system/binaries")
|
||||
);
|
||||
assert_eq!(configured_alias_dir(&config), PathBuf::from("/usr/bin"));
|
||||
}
|
||||
|
||||
#[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");
|
||||
let host_dir = rootfs.join("usr/bin");
|
||||
|
||||
assert_eq!(dir_in_rootfs(&rootfs, &host_dir), host_dir);
|
||||
}
|
||||
|
||||
+5
-33
@@ -269,11 +269,7 @@ 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")
|
||||
};
|
||||
let variable_root = abs_rootfs.join("var");
|
||||
(
|
||||
variable_root.join("cache/depot/sources"),
|
||||
variable_root.join("cache/depot/packages"),
|
||||
@@ -306,17 +302,9 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Return the package database path used to query what is installed in `rootfs`.
|
||||
///
|
||||
/// For the live system root (`/`), non-root processes still read the system DB
|
||||
/// from `/var/lib/depot/packages.db` even though writable state is redirected
|
||||
/// to per-user directories under `$HOME`.
|
||||
pub fn installed_db_path(&self, rootfs: &Path) -> PathBuf {
|
||||
let abs_rootfs = resolve_rootfs_base(rootfs);
|
||||
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
|
||||
@@ -596,25 +584,9 @@ mod tests {
|
||||
let root = PathBuf::from("/tmp/test_root");
|
||||
let config = Config::for_rootfs(&root);
|
||||
|
||||
// Canonicalization might happen, so let's just check ends_with or construct reliably
|
||||
assert!(
|
||||
config
|
||||
.cache_dir
|
||||
.to_string_lossy()
|
||||
.contains("system/variable/cache/depot/sources")
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.build_dir
|
||||
.to_string_lossy()
|
||||
.contains("system/variable/cache/depot/build")
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.db_dir
|
||||
.to_string_lossy()
|
||||
.contains("system/variable/lib/depot")
|
||||
);
|
||||
assert_eq!(config.cache_dir, root.join("var/cache/depot/sources"));
|
||||
assert_eq!(config.build_dir, root.join("var/cache/depot/build"));
|
||||
assert_eq!(config.db_dir, root.join("var/lib/depot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -727,7 +699,7 @@ cflags += ["-g"]
|
||||
let config = Config::for_rootfs(&root);
|
||||
assert_eq!(
|
||||
config.installed_db_path(&root),
|
||||
PathBuf::from("/tmp/test_root/system/variable/lib/depot/packages.db")
|
||||
PathBuf::from("/tmp/test_root/var/lib/depot/packages.db")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+85
-18
@@ -844,6 +844,30 @@ pub fn get_installed_packages(db_path: &Path) -> Result<std::collections::HashSe
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Get installed dependency names, including stable `real_name` aliases.
|
||||
pub(crate) fn get_installed_dependency_names(
|
||||
db_path: &Path,
|
||||
) -> Result<std::collections::HashSet<String>> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
if !db_path.exists() {
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
init_db(&conn)?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT name FROM packages
|
||||
UNION
|
||||
SELECT real_name FROM packages WHERE real_name IS NOT NULL",
|
||||
)?;
|
||||
let names: HashSet<String> = stmt
|
||||
.query_map([], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// List installed packages with version and revision metadata.
|
||||
pub fn list_installed_package_records(db_path: &Path) -> Result<Vec<InstalledPackageRecord>> {
|
||||
if !db_path.exists() {
|
||||
@@ -1038,6 +1062,27 @@ pub fn get_package_version(db_path: &Path, name: &str) -> Result<Option<String>>
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Get an installed package version by package name or stable `real_name`.
|
||||
pub(crate) fn get_dependency_version(db_path: &Path, name: &str) -> Result<Option<String>> {
|
||||
if !db_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
init_db(&conn)?;
|
||||
let version = conn
|
||||
.query_row(
|
||||
"SELECT version FROM packages
|
||||
WHERE name = ?1 OR real_name = ?1
|
||||
ORDER BY CASE WHEN name = ?1 THEN 0 ELSE 1 END, name
|
||||
LIMIT 1",
|
||||
params![name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()?;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Find the installed package that owns a filesystem path from the local DB.
|
||||
pub fn owns_path(db_path: &Path, path: &Path) -> Result<Option<String>> {
|
||||
if !db_path.exists() {
|
||||
@@ -1165,6 +1210,28 @@ mod tests {
|
||||
assert_eq!(version.as_deref(), Some("2.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_dependency_names_include_real_name_aliases() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(&destdir).unwrap();
|
||||
|
||||
let mut spec = mk_spec("libressl43", "4.3.2");
|
||||
spec.package.real_name = Some("libressl".into());
|
||||
register_package(&db_path, &spec, &destdir).unwrap();
|
||||
|
||||
let names = get_installed_dependency_names(&db_path).unwrap();
|
||||
assert!(names.contains("libressl43"));
|
||||
assert!(names.contains("libressl"));
|
||||
assert_eq!(
|
||||
get_dependency_version(&db_path, "libressl")
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("4.3.2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_uses_metadata_completed_at_when_present() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -1268,28 +1335,28 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let rootfs = tmp.path().join("rootfs");
|
||||
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
|
||||
std::fs::create_dir_all(rootfs.join("system/binaries")).unwrap();
|
||||
std::fs::write(rootfs.join("system/binaries/find"), "sbase find").unwrap();
|
||||
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
|
||||
std::fs::write(rootfs.join("usr/bin/find"), "sbase find").unwrap();
|
||||
|
||||
let spec_a = mk_spec("sbase", "1.0");
|
||||
let dest_a = tmp.path().join("dest_a");
|
||||
std::fs::create_dir_all(dest_a.join("system/binaries")).unwrap();
|
||||
std::fs::write(dest_a.join("system/binaries/find"), "sbase find").unwrap();
|
||||
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest_a.join("usr/bin/find"), "sbase find").unwrap();
|
||||
register_package(&db_path, &spec_a, &dest_a).unwrap();
|
||||
|
||||
let spec_b = mk_spec("bfs", "4.1");
|
||||
let dest_b = tmp.path().join("dest_b");
|
||||
std::fs::create_dir_all(dest_b.join("system/binaries")).unwrap();
|
||||
std::fs::write(dest_b.join("system/binaries/find"), "bfs find").unwrap();
|
||||
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest_b.join("usr/bin/find"), "bfs find").unwrap();
|
||||
|
||||
let mut env = TestEnv::new();
|
||||
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
|
||||
register_package(&db_path, &spec_b, &dest_b).unwrap();
|
||||
|
||||
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
|
||||
assert!(!files_sbase.contains(&"system/binaries/find".to_string()));
|
||||
assert!(!files_sbase.contains(&"usr/bin/find".to_string()));
|
||||
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
|
||||
assert!(files_bfs.contains(&"system/binaries/find".to_string()));
|
||||
assert!(files_bfs.contains(&"usr/bin/find".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1297,33 +1364,33 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let rootfs = tmp.path().join("rootfs");
|
||||
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
|
||||
std::fs::create_dir_all(rootfs.join("system/binaries")).unwrap();
|
||||
std::fs::write(rootfs.join("system/binaries/find"), "sbase find").unwrap();
|
||||
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
|
||||
std::fs::write(rootfs.join("usr/bin/find"), "sbase find").unwrap();
|
||||
|
||||
let spec_a = mk_spec("sbase", "1.0");
|
||||
let dest_a = tmp.path().join("dest_a");
|
||||
std::fs::create_dir_all(dest_a.join("system/binaries")).unwrap();
|
||||
std::fs::write(dest_a.join("system/binaries/find"), "sbase find").unwrap();
|
||||
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest_a.join("usr/bin/find"), "sbase find").unwrap();
|
||||
register_package(&db_path, &spec_a, &dest_a).unwrap();
|
||||
|
||||
std::fs::write(rootfs.join("system/binaries/find"), "bfs find").unwrap();
|
||||
std::fs::write(rootfs.join("usr/bin/find"), "bfs find").unwrap();
|
||||
let spec_b = mk_spec("bfs", "4.1");
|
||||
let dest_b = tmp.path().join("dest_b");
|
||||
std::fs::create_dir_all(dest_b.join("system/binaries")).unwrap();
|
||||
std::fs::write(dest_b.join("system/binaries/find"), "bfs find").unwrap();
|
||||
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest_b.join("usr/bin/find"), "bfs find").unwrap();
|
||||
|
||||
let mut env = TestEnv::new();
|
||||
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
|
||||
register_package(&db_path, &spec_b, &dest_b).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("system/binaries/find")).unwrap(),
|
||||
std::fs::read_to_string(rootfs.join("usr/bin/find")).unwrap(),
|
||||
"bfs find"
|
||||
);
|
||||
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
|
||||
assert!(!files_sbase.contains(&"system/binaries/find".to_string()));
|
||||
assert!(!files_sbase.contains(&"usr/bin/find".to_string()));
|
||||
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
|
||||
assert!(files_bfs.contains(&"system/binaries/find".to_string()));
|
||||
assert!(files_bfs.contains(&"usr/bin/find".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+24
-5
@@ -149,7 +149,7 @@ fn is_dep_satisfied(
|
||||
};
|
||||
|
||||
// Check version matches
|
||||
if let Some(installed_version) = db::get_package_version(db_path, parsed.name)? {
|
||||
if let Some(installed_version) = db::get_dependency_version(db_path, parsed.name)? {
|
||||
Ok(compare_versions(&installed_version, required, parsed.op))
|
||||
} else {
|
||||
// Package might be satisfied by an alternative or replacement, accept it.
|
||||
@@ -174,7 +174,7 @@ pub fn is_dep_satisfied_in_db(dep: &str, db_path: &Path) -> Result<bool> {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let installed = db::get_installed_packages(db_path)?;
|
||||
let installed = db::get_installed_dependency_names(db_path)?;
|
||||
let provides = db::get_all_provides(db_path)?;
|
||||
let replaces = db::get_all_replaces(db_path)?;
|
||||
is_dep_satisfied(dep, &installed, &provides, &replaces, db_path)
|
||||
@@ -255,7 +255,7 @@ pub(crate) fn check_build_deps_for_outputs(
|
||||
return Ok(build_deps);
|
||||
}
|
||||
|
||||
let installed = db::get_installed_packages(db_path)?;
|
||||
let installed = db::get_installed_dependency_names(db_path)?;
|
||||
let provides = db::get_all_provides(db_path)?;
|
||||
let replaces = db::get_all_replaces(db_path)?;
|
||||
|
||||
@@ -287,7 +287,7 @@ pub(crate) fn check_runtime_deps_for_outputs(
|
||||
return Ok(missing);
|
||||
}
|
||||
|
||||
let installed = db::get_installed_packages(db_path)?;
|
||||
let installed = db::get_installed_dependency_names(db_path)?;
|
||||
let provides = db::get_all_provides(db_path)?;
|
||||
let replaces = db::get_all_replaces(db_path)?;
|
||||
|
||||
@@ -316,7 +316,7 @@ pub(crate) fn check_test_deps_for_outputs(
|
||||
return Ok(test_deps);
|
||||
}
|
||||
|
||||
let installed = db::get_installed_packages(db_path)?;
|
||||
let installed = db::get_installed_dependency_names(db_path)?;
|
||||
let provides = db::get_all_provides(db_path)?;
|
||||
let replaces = db::get_all_replaces(db_path)?;
|
||||
|
||||
@@ -780,6 +780,25 @@ mod tests {
|
||||
assert!(is_dep_satisfied_in_db("grep", &db_path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_installed_real_name_satisfies_dependencies() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(destdir.join("usr/lib")).unwrap();
|
||||
std::fs::write(destdir.join("usr/lib/libssl.so"), "ssl").unwrap();
|
||||
|
||||
let mut spec = test_spec_with_build(BuildType::Custom, None, &[]);
|
||||
spec.package.name = "libressl43".into();
|
||||
spec.package.real_name = Some("libressl".into());
|
||||
spec.package.version = "4.3.2".into();
|
||||
|
||||
crate::db::register_package(&db_path, &spec, &destdir).unwrap();
|
||||
|
||||
assert!(is_dep_satisfied_in_db("libressl", &db_path).unwrap());
|
||||
assert!(is_dep_satisfied_in_db("libressl>=4.3.0", &db_path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_type_runs_automatic_tests_matches_builder_behavior() {
|
||||
assert!(build_type_runs_automatic_tests(&test_spec_with_build(
|
||||
|
||||
+128
-24
@@ -1,42 +1,41 @@
|
||||
//! Fakeroot support for running commands as pseudo-root
|
||||
//! Uses the system fakeroot command when not running as root
|
||||
//! Rootless install command support backed by Linux user namespaces.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Check if we're running as root
|
||||
const UID_MAP_PATH: &[u8] = b"/proc/self/uid_map\0";
|
||||
const GID_MAP_PATH: &[u8] = b"/proc/self/gid_map\0";
|
||||
const SETGROUPS_PATH: &[u8] = b"/proc/self/setgroups\0";
|
||||
|
||||
/// Check if the current process is running as root.
|
||||
pub fn is_root() -> bool {
|
||||
nix::unistd::geteuid().is_root()
|
||||
}
|
||||
|
||||
/// Wrap a command for fakeroot execution
|
||||
/// For make install, we use the system fakeroot command
|
||||
/// Build an install command that runs as root inside a private user namespace.
|
||||
///
|
||||
/// The namespace maps UID/GID 0 to the invoking user, allowing install scripts
|
||||
/// to perform root-owned staged installs without changing host ownership or
|
||||
/// requiring an external fakeroot implementation.
|
||||
pub fn wrap_install_command(program: &str, destdir: &Path) -> Command {
|
||||
let script_path = shell_script_path(program);
|
||||
if is_root() {
|
||||
build_command(program, script_path)
|
||||
} else {
|
||||
// Use system fakeroot command which handles LD_PRELOAD internally
|
||||
let mut cmd = Command::new("fakeroot");
|
||||
cmd.arg("--");
|
||||
if let Some(script_path) = script_path {
|
||||
cmd.arg("sh");
|
||||
cmd.arg(script_path);
|
||||
} else {
|
||||
cmd.arg(program);
|
||||
}
|
||||
// Fakeroot will ensure file ownership appears as root
|
||||
cmd.env("DESTDIR", destdir);
|
||||
cmd
|
||||
let mut command = build_command(program, shell_script_path(program));
|
||||
command.env("DESTDIR", destdir);
|
||||
|
||||
if !is_root() {
|
||||
configure_rootless_namespace(&mut command);
|
||||
}
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
fn build_command(program: &str, script_path: Option<&Path>) -> Command {
|
||||
if let Some(script_path) = script_path {
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg(script_path);
|
||||
cmd
|
||||
let mut command = Command::new("sh");
|
||||
command.arg(script_path);
|
||||
command
|
||||
} else {
|
||||
Command::new(program)
|
||||
}
|
||||
@@ -50,3 +49,108 @@ fn shell_script_path(program: &str) -> Option<&Path> {
|
||||
let bytes = fs::read(path).ok()?;
|
||||
bytes.starts_with(b"#!").then_some(path)
|
||||
}
|
||||
|
||||
fn configure_rootless_namespace(command: &mut Command) {
|
||||
let uid_map = format!("0 {} 1\n", nix::unistd::geteuid().as_raw()).into_bytes();
|
||||
let gid_map = format!("0 {} 1\n", nix::unistd::getegid().as_raw()).into_bytes();
|
||||
|
||||
// Only async-signal-safe libc calls are made in the post-fork child.
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
if nix::libc::unshare(nix::libc::CLONE_NEWUSER) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
write_proc_file(SETGROUPS_PATH, b"deny\n", true)?;
|
||||
write_proc_file(UID_MAP_PATH, &uid_map, false)?;
|
||||
write_proc_file(GID_MAP_PATH, &gid_map, false)?;
|
||||
|
||||
if nix::libc::setresgid(0, 0, 0) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if nix::libc::setresuid(0, 0, 0) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn write_proc_file(path: &[u8], contents: &[u8], allow_missing: bool) -> io::Result<()> {
|
||||
let fd = unsafe {
|
||||
nix::libc::open(
|
||||
path.as_ptr().cast(),
|
||||
nix::libc::O_WRONLY | nix::libc::O_CLOEXEC,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
if allow_missing && error.raw_os_error() == Some(nix::libc::ENOENT) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let result = write_all(fd, contents);
|
||||
let close_result = unsafe { nix::libc::close(fd) };
|
||||
if result.is_ok() && close_result != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn write_all(fd: i32, mut contents: &[u8]) -> io::Result<()> {
|
||||
while !contents.is_empty() {
|
||||
let written = unsafe { nix::libc::write(fd, contents.as_ptr().cast(), contents.len()) };
|
||||
if written < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
if error.raw_os_error() == Some(nix::libc::EINTR) {
|
||||
continue;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if written == 0 {
|
||||
return Err(io::Error::from_raw_os_error(nix::libc::EIO));
|
||||
}
|
||||
contents = &contents[written as usize..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::{Context, Result};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
#[test]
|
||||
fn rootless_command_runs_as_namespace_root() -> Result<()> {
|
||||
let destdir = tempfile::tempdir()?;
|
||||
let mut command = wrap_install_command("/bin/sh", destdir.path());
|
||||
command
|
||||
.arg("-c")
|
||||
.arg("test \"$(/usr/bin/id -u)\" = 0; test \"$(/usr/bin/id -g)\" = 0; /usr/bin/touch \"$DESTDIR/root-owned\"; /usr/bin/chown 0:0 \"$DESTDIR/root-owned\"; /usr/bin/chmod 4755 \"$DESTDIR/root-owned\"");
|
||||
|
||||
let status = command
|
||||
.status()
|
||||
.context("Failed to launch internal fakeroot command")?;
|
||||
assert!(status.success());
|
||||
|
||||
let metadata = std::fs::metadata(destdir.path().join("root-owned"))?;
|
||||
assert_eq!(metadata.uid(), nix::unistd::geteuid().as_raw());
|
||||
assert_eq!(metadata.gid(), nix::unistd::getegid().as_raw());
|
||||
assert_eq!(metadata.mode() & 0o7777, 0o4755);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_command_preserves_program_and_sets_destdir() {
|
||||
let command = wrap_install_command("make", Path::new("/tmp/package-root"));
|
||||
|
||||
assert_eq!(command.get_program(), "make");
|
||||
assert!(command.get_envs().any(|(key, value)| {
|
||||
key == "DESTDIR" && value.is_some_and(|value| value == "/tmp/package-root")
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
//! Filesystem copy helpers that preserve Unix link topology.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use filetime::FileTime;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
type HardlinkKey = (u64, u64);
|
||||
|
||||
fn hardlink_key(metadata: &fs::Metadata) -> Option<HardlinkKey> {
|
||||
(metadata.nlink() > 1).then_some((metadata.dev(), metadata.ino()))
|
||||
}
|
||||
|
||||
/// Tracks already-copied hardlink groups while copying a set of files.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct HardlinkCopyTracker {
|
||||
copied: HashMap<HardlinkKey, PathBuf>,
|
||||
}
|
||||
|
||||
impl HardlinkCopyTracker {
|
||||
/// Create an empty tracker for one logical copy operation.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Copy a regular file while preserving hardlinks seen by this tracker.
|
||||
pub(crate) fn copy_file(&mut self, src: &Path, dst: &Path) -> Result<()> {
|
||||
let metadata = src
|
||||
.symlink_metadata()
|
||||
.with_context(|| format!("Failed to inspect {}", src.display()))?;
|
||||
if !metadata.file_type().is_file() {
|
||||
anyhow::bail!("Expected regular file for copy: {}", src.display());
|
||||
}
|
||||
|
||||
if let Some(parent) = dst.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create dir: {}", parent.display()))?;
|
||||
}
|
||||
|
||||
if let Some(first_dst) = hardlink_key(&metadata).and_then(|key| self.copied.get(&key)) {
|
||||
fs::hard_link(first_dst, dst).with_context(|| {
|
||||
format!(
|
||||
"Failed to create hardlink {} -> {}",
|
||||
dst.display(),
|
||||
first_dst.display()
|
||||
)
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::copy(src, dst)
|
||||
.with_context(|| format!("Failed to copy {} to {}", src.display(), dst.display()))?;
|
||||
preserve_file_metadata(dst, &metadata)?;
|
||||
|
||||
if let Some(key) = hardlink_key(&metadata) {
|
||||
self.copied.insert(key, dst.to_path_buf());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a tree without following symlinks, preserving symlinks and hardlinks.
|
||||
pub(crate) fn copy_tree_preserving_links(src: &Path, dst: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dst)
|
||||
.with_context(|| format!("Failed to create destination dir: {}", dst.display()))?;
|
||||
|
||||
let mut hardlinks = HardlinkCopyTracker::new();
|
||||
for entry in WalkDir::new(src).follow_links(false) {
|
||||
crate::interrupts::check()?;
|
||||
let entry = entry.with_context(|| format!("Failed to walk {}", src.display()))?;
|
||||
let rel = entry
|
||||
.path()
|
||||
.strip_prefix(src)
|
||||
.with_context(|| format!("Failed to strip prefix: {}", src.display()))?;
|
||||
let target = dst.join(rel);
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target)
|
||||
.with_context(|| format!("Failed to create dir: {}", target.display()))?;
|
||||
let metadata = entry
|
||||
.path()
|
||||
.symlink_metadata()
|
||||
.with_context(|| format!("Failed to inspect {}", entry.path().display()))?;
|
||||
preserve_directory_metadata(&target, &metadata)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create dir: {}", parent.display()))?;
|
||||
}
|
||||
|
||||
if entry.file_type().is_symlink() {
|
||||
let link_target = fs::read_link(entry.path())
|
||||
.with_context(|| format!("Failed to read symlink: {}", entry.path().display()))?;
|
||||
std::os::unix::fs::symlink(&link_target, &target).with_context(|| {
|
||||
format!(
|
||||
"Failed to create symlink {} -> {}",
|
||||
target.display(),
|
||||
link_target.display()
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
hardlinks.copy_file(entry.path(), &target)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn preserve_file_metadata(dst: &Path, metadata: &fs::Metadata) -> Result<()> {
|
||||
fs::set_permissions(dst, metadata.permissions())
|
||||
.with_context(|| format!("Failed to set permissions for {}", dst.display()))?;
|
||||
let atime = FileTime::from_last_access_time(metadata);
|
||||
let mtime = FileTime::from_last_modification_time(metadata);
|
||||
filetime::set_file_times(dst, atime, mtime)
|
||||
.with_context(|| format!("Failed to preserve file timestamps for {}", dst.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn preserve_directory_metadata(dst: &Path, metadata: &fs::Metadata) -> Result<()> {
|
||||
fs::set_permissions(dst, metadata.permissions())
|
||||
.with_context(|| format!("Failed to set permissions for {}", dst.display()))?;
|
||||
let atime = FileTime::from_last_access_time(metadata);
|
||||
let mtime = FileTime::from_last_modification_time(metadata);
|
||||
filetime::set_file_times(dst, atime, mtime).with_context(|| {
|
||||
format!(
|
||||
"Failed to preserve directory timestamps for {}",
|
||||
dst.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn copy_tree_preserves_hardlinks_and_symlinks() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let src = temp.path().join("src");
|
||||
let dst = temp.path().join("dst");
|
||||
fs::create_dir_all(src.join("usr/bin"))?;
|
||||
fs::write(src.join("usr/bin/uutils"), "multicall")?;
|
||||
fs::hard_link(src.join("usr/bin/uutils"), src.join("usr/bin/ls"))?;
|
||||
std::os::unix::fs::symlink("uutils", src.join("usr/bin/cat"))?;
|
||||
|
||||
copy_tree_preserving_links(&src, &dst)?;
|
||||
|
||||
let uutils = dst.join("usr/bin/uutils").metadata()?;
|
||||
let ls = dst.join("usr/bin/ls").metadata()?;
|
||||
assert_eq!(uutils.ino(), ls.ino());
|
||||
assert_eq!(uutils.nlink(), 2);
|
||||
assert_eq!(
|
||||
fs::read_link(dst.join("usr/bin/cat"))?,
|
||||
PathBuf::from("uutils")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -259,8 +259,10 @@ mod tests {
|
||||
});
|
||||
|
||||
let start = Instant::now();
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c").arg("sleep 10");
|
||||
// Other parallel tests temporarily replace the process-wide PATH.
|
||||
// Use an absolute executable so this test only exercises interruption.
|
||||
let mut cmd = Command::new("/bin/sleep");
|
||||
cmd.arg("10");
|
||||
configure_child_process_group(&mut cmd);
|
||||
let mut child = cmd.spawn().expect("sleep command should spawn");
|
||||
let status = wait_for_child_with(&mut child, || interrupted.load(Ordering::Relaxed))
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
//! Depot - Not Your Average Package Manager
|
||||
//! A source-based package manager for Linux
|
||||
|
||||
mod bootstrap;
|
||||
mod build_options;
|
||||
mod builder;
|
||||
mod cli;
|
||||
@@ -12,6 +11,7 @@ mod cross;
|
||||
mod db;
|
||||
mod deps;
|
||||
mod fakeroot;
|
||||
mod fs_copy;
|
||||
mod hex;
|
||||
mod index;
|
||||
mod install;
|
||||
@@ -25,7 +25,6 @@ mod shell_helpers;
|
||||
mod signing;
|
||||
mod source;
|
||||
mod staging;
|
||||
mod system_state;
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
mod ui;
|
||||
|
||||
+56
-1
@@ -4,11 +4,19 @@ use crate::config::Config;
|
||||
use crate::metadata_time;
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tar::Builder;
|
||||
use tar::{Builder, EntryType};
|
||||
use zstd::stream::write::Encoder;
|
||||
|
||||
type HardlinkKey = (u64, u64);
|
||||
|
||||
fn hardlink_key(metadata: &fs::Metadata) -> Option<HardlinkKey> {
|
||||
(metadata.nlink() > 1).then_some((metadata.dev(), metadata.ino()))
|
||||
}
|
||||
|
||||
fn is_internal_staging_rel_path(rel_path: &Path) -> bool {
|
||||
let s = rel_path.to_string_lossy();
|
||||
let p = s.trim_start_matches('/');
|
||||
@@ -78,6 +86,7 @@ impl Packager {
|
||||
let _ = encoder.multithread(num_cpus() as u32);
|
||||
|
||||
let mut tar = Builder::new(encoder);
|
||||
let mut archived_hardlinks: HashMap<HardlinkKey, PathBuf> = HashMap::new();
|
||||
|
||||
// Manual walk to ensure symlinks aren't followed (preserving them as links)
|
||||
for entry in walkdir::WalkDir::new(&self.destdir)
|
||||
@@ -112,8 +121,23 @@ impl Packager {
|
||||
tar.append_link(&mut header, rel_path, target)?;
|
||||
} else {
|
||||
// Files
|
||||
let metadata = fs::symlink_metadata(path)?;
|
||||
let hardlink_key = hardlink_key(&metadata);
|
||||
if let Some(first_rel_path) =
|
||||
hardlink_key.and_then(|key| archived_hardlinks.get(&key))
|
||||
{
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_metadata_in_mode(&metadata, tar::HeaderMode::Deterministic);
|
||||
header.set_entry_type(EntryType::Link);
|
||||
header.set_size(0);
|
||||
tar.append_link(&mut header, rel_path, first_rel_path)?;
|
||||
} else {
|
||||
let mut file = fs::File::open(path)?;
|
||||
tar.append_file(rel_path, &mut file)?;
|
||||
if let Some(key) = hardlink_key {
|
||||
archived_hardlinks.insert(key, rel_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +344,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::package::{Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo};
|
||||
use std::io::Read;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
fn mk_packager(destdir: PathBuf) -> Packager {
|
||||
Packager::new(
|
||||
@@ -608,4 +633,34 @@ mod tests {
|
||||
assert!(paths.contains(&".metadata.toml".to_string()));
|
||||
assert!(paths.contains(&".files.yaml".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_package_preserves_hardlinks() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path().join("dest");
|
||||
let out = tmp.path().join("out");
|
||||
let extract = tmp.path().join("extract");
|
||||
fs::create_dir_all(dest.join("usr/bin")).unwrap();
|
||||
fs::create_dir_all(&out).unwrap();
|
||||
fs::create_dir_all(&extract).unwrap();
|
||||
|
||||
let coreutils = dest.join("usr/bin/coreutils");
|
||||
let ls = dest.join("usr/bin/ls");
|
||||
fs::write(&coreutils, "multicall").unwrap();
|
||||
fs::hard_link(&coreutils, &ls).unwrap();
|
||||
|
||||
let packager = mk_packager(dest.clone());
|
||||
let archive_path = packager.create_package(&out, "x86_64").unwrap();
|
||||
|
||||
let archive_file = fs::File::open(&archive_path).unwrap();
|
||||
let decoder = zstd::Decoder::new(archive_file).unwrap();
|
||||
let mut archive = tar::Archive::new(decoder);
|
||||
archive.unpack(&extract).unwrap();
|
||||
|
||||
let coreutils_meta = extract.join("usr/bin/coreutils").metadata().unwrap();
|
||||
let ls_meta = extract.join("usr/bin/ls").metadata().unwrap();
|
||||
assert_eq!(coreutils_meta.ino(), ls_meta.ino());
|
||||
assert_eq!(coreutils_meta.nlink(), 2);
|
||||
assert_eq!(ls_meta.nlink(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
-5274
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,378 @@
|
||||
use super::config::{normalize_append_key, preprocess_spec_toml_appends};
|
||||
use super::model::*;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Complete package specification from TOML
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct PackageSpec {
|
||||
pub package: PackageInfo,
|
||||
/// Optional additional package outputs produced from the same spec/destdir
|
||||
#[serde(default)]
|
||||
pub packages: Vec<PackageInfo>,
|
||||
#[serde(default)]
|
||||
pub alternatives: Alternatives,
|
||||
/// Manual (local) sources to copy before fetching remote sources.
|
||||
#[serde(default)]
|
||||
pub manual_sources: Vec<ManualSource>,
|
||||
#[serde(default, deserialize_with = "deserialize_sources")]
|
||||
pub source: Vec<Source>,
|
||||
pub build: Build,
|
||||
#[serde(default)]
|
||||
pub dependencies: Dependencies,
|
||||
/// Optional per-output alternatives/provides overrides keyed by package name.
|
||||
///
|
||||
/// Example:
|
||||
/// [package_alternatives.clang]
|
||||
/// provides = ["cc", "c++", "gcc"]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub package_alternatives: BTreeMap<String, Alternatives>,
|
||||
/// Optional per-output dependency overrides keyed by package name.
|
||||
///
|
||||
/// Example:
|
||||
/// [package_dependencies.clang]
|
||||
/// runtime = ["llvm-libs", "llvm-libgcc"]
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub package_dependencies: BTreeMap<String, Dependencies>,
|
||||
|
||||
/// Directory containing the spec file (used to resolve relative paths such as patches).
|
||||
#[serde(skip)]
|
||||
pub spec_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl PackageSpec {
|
||||
/// Load package spec from a TOML file
|
||||
pub fn from_file(path: &Path) -> Result<Self> {
|
||||
// Canonicalize path to ensure spec_dir is absolute
|
||||
let abs_path = path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to resolve path: {}", path.display()))?;
|
||||
|
||||
let content = fs::read_to_string(&abs_path)
|
||||
.with_context(|| format!("Failed to read package spec: {}", abs_path.display()))?;
|
||||
let (base_content, appends) =
|
||||
preprocess_spec_toml_appends(&content).with_context(|| {
|
||||
format!("Failed to preprocess package spec: {}", abs_path.display())
|
||||
})?;
|
||||
let mut unknown_key = None;
|
||||
let deserializer = toml::Deserializer::parse(&base_content)
|
||||
.with_context(|| format!("Failed to parse package spec: {}", abs_path.display()))?;
|
||||
let mut spec: PackageSpec = serde_ignored::deserialize(deserializer, |path| {
|
||||
if unknown_key.is_none() {
|
||||
unknown_key = Some(path.to_string());
|
||||
}
|
||||
})
|
||||
.with_context(|| format!("Failed to parse package spec: {}", abs_path.display()))?;
|
||||
if let Some(path) = unknown_key {
|
||||
anyhow::bail!(
|
||||
"Failed to parse package spec: {}: unknown key: {}",
|
||||
abs_path.display(),
|
||||
path
|
||||
);
|
||||
}
|
||||
spec.spec_dir = abs_path
|
||||
.parent()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
spec.apply_spec_appends(&appends)?;
|
||||
|
||||
// Require at least one source (remote or manual) unless this is a metapackage.
|
||||
if spec.source.is_empty() && spec.manual_sources.is_empty() && !spec.is_metapackage() {
|
||||
anyhow::bail!(
|
||||
"Package must have at least one source or manual_sources entry (except build.type = \"meta\")"
|
||||
);
|
||||
}
|
||||
spec.validate_manual_sources()?;
|
||||
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
fn apply_spec_appends(
|
||||
&mut self,
|
||||
appends: &std::collections::HashMap<String, Vec<toml::Value>>,
|
||||
) -> Result<()> {
|
||||
for (key, values) in appends {
|
||||
let key = normalize_append_key(key);
|
||||
if let Some(subkey) = key.strip_prefix("build.flags.") {
|
||||
self.apply_append(subkey, values);
|
||||
continue;
|
||||
}
|
||||
if let Some(subkey) = key.strip_prefix("flags.") {
|
||||
self.apply_append(subkey, values);
|
||||
continue;
|
||||
}
|
||||
if !key.contains('.') {
|
||||
self.apply_append(&key, values);
|
||||
continue;
|
||||
}
|
||||
anyhow::bail!("Unsupported '+=' key in package spec: {}", key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_manual_sources(&self) -> Result<()> {
|
||||
for (idx, manual) in self.manual_sources.iter().enumerate() {
|
||||
let has_file = manual
|
||||
.file
|
||||
.as_ref()
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
let has_url = manual
|
||||
.url
|
||||
.as_ref()
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
let file_count = manual.files.iter().filter(|s| !s.trim().is_empty()).count();
|
||||
let url_count = manual.urls.iter().filter(|s| !s.trim().is_empty()).count();
|
||||
let local_count = usize::from(has_file) + file_count;
|
||||
let remote_count = usize::from(has_url) + url_count;
|
||||
|
||||
if local_count == 0 && remote_count == 0 {
|
||||
anyhow::bail!(
|
||||
"manual_sources[{}] must specify one of 'file', 'files', 'url', or 'urls'",
|
||||
idx
|
||||
);
|
||||
}
|
||||
if local_count > 0 && remote_count > 0 {
|
||||
anyhow::bail!(
|
||||
"manual_sources[{}] cannot mix local ('file'/'files') and remote ('url'/'urls') entries",
|
||||
idx
|
||||
);
|
||||
}
|
||||
if (local_count > 1 || remote_count > 1)
|
||||
&& manual.dest.as_ref().is_some_and(|d| !d.trim().is_empty())
|
||||
{
|
||||
anyhow::bail!(
|
||||
"manual_sources[{}] cannot use 'dest' with multiple entries in one block",
|
||||
idx
|
||||
);
|
||||
}
|
||||
if (local_count > 1 || remote_count > 1)
|
||||
&& manual
|
||||
.sha256
|
||||
.as_ref()
|
||||
.is_some_and(|h| !h.trim().is_empty() && h.trim() != "skip")
|
||||
{
|
||||
anyhow::bail!(
|
||||
"manual_sources[{}] cannot use one 'sha256' for multiple entries in one block",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Expand variables like `$name` and `$version` in a string.
|
||||
pub fn expand_vars(&self, input: &str) -> String {
|
||||
let specdir = self.spec_dir.to_string_lossy();
|
||||
input
|
||||
.replace("$name", &self.package.name)
|
||||
.replace("$version", &self.package.version)
|
||||
.replace("$specdir", &specdir)
|
||||
.replace("$DEPOT_SPECDIR", &specdir)
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[Source] {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Returns true when this spec is a metadata-only package that exists to pull dependencies.
|
||||
pub fn is_metapackage(&self) -> bool {
|
||||
matches!(self.build.build_type, BuildType::Meta)
|
||||
}
|
||||
|
||||
/// Return all declared package outputs for this spec (primary + any extras).
|
||||
pub fn outputs(&self) -> Vec<PackageInfo> {
|
||||
let mut v = Vec::new();
|
||||
v.push(self.package.clone());
|
||||
v.extend(self.packages.clone());
|
||||
v
|
||||
}
|
||||
|
||||
/// Return the derived documentation package name for an output package.
|
||||
pub fn docs_package_name(pkg_name: &str) -> String {
|
||||
format!("{pkg_name}-docs")
|
||||
}
|
||||
|
||||
/// Build package metadata for an automatically generated documentation output.
|
||||
pub fn docs_package_for_output(&self, output: &PackageInfo) -> PackageInfo {
|
||||
let mut docs = output.clone();
|
||||
docs.name = Self::docs_package_name(&output.name);
|
||||
docs.description = format!("Documentation for {}", output.name);
|
||||
docs
|
||||
}
|
||||
|
||||
fn docs_parent_output_name(&self, pkg_name: &str) -> Option<String> {
|
||||
if !self.build.flags.split_docs {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base = pkg_name.strip_suffix("-docs")?;
|
||||
self.outputs()
|
||||
.into_iter()
|
||||
.find(|output| output.name == base)
|
||||
.map(|output| output.name)
|
||||
}
|
||||
|
||||
/// Return dependencies for a specific output package name.
|
||||
///
|
||||
/// If no per-output override exists, returns the top-level dependencies.
|
||||
pub fn dependencies_for_output(&self, pkg_name: &str) -> Dependencies {
|
||||
if pkg_name == self.lib32_package_name() {
|
||||
return self
|
||||
.package_dependencies
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.lib32_dependencies());
|
||||
}
|
||||
|
||||
if let Some(parent_output) = self.docs_parent_output_name(pkg_name) {
|
||||
return self
|
||||
.package_dependencies
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
let mut deps = Dependencies::default();
|
||||
deps.runtime.push(parent_output);
|
||||
deps
|
||||
});
|
||||
}
|
||||
|
||||
self.package_dependencies
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.dependencies.primary_dependencies())
|
||||
}
|
||||
|
||||
/// Return the generated lib32 companion package name for this spec.
|
||||
pub fn lib32_package_name(&self) -> String {
|
||||
format!("lib32-{}", self.package.name)
|
||||
}
|
||||
|
||||
/// Return true when this spec should emit the generated `lib32-*` package.
|
||||
pub fn builds_lib32_output(&self) -> bool {
|
||||
self.build.flags.build_32 || self.build.flags.lib32_only
|
||||
}
|
||||
|
||||
/// Return true when only the generated `lib32-*` package should be emitted.
|
||||
pub fn builds_only_lib32_output(&self) -> bool {
|
||||
self.build.flags.lib32_only
|
||||
}
|
||||
|
||||
/// Return true when builder-managed automatic tests should be skipped.
|
||||
///
|
||||
/// Automatic test phases are disabled when `build.flags.skip_tests` is set and for
|
||||
/// multilib builds, because the generated lib32 output is built in a separate 32-bit pass.
|
||||
pub fn should_skip_automatic_tests(&self) -> bool {
|
||||
self.build.flags.skip_tests || self.builds_lib32_output()
|
||||
}
|
||||
|
||||
/// Return the effective dependency set used by the generated lib32 companion package.
|
||||
pub fn lib32_dependencies(&self) -> Dependencies {
|
||||
let mut deps = self
|
||||
.dependencies
|
||||
.lib32_dependencies()
|
||||
.unwrap_or_else(|| self.dependencies.primary_dependencies());
|
||||
if !deps.runtime.iter().any(|dep| dep == &self.package.name) {
|
||||
deps.runtime.push(self.package.name.clone());
|
||||
}
|
||||
deps
|
||||
}
|
||||
|
||||
/// Return local package names/provided features for the selected output set.
|
||||
pub fn local_dependency_provides_for_selection(
|
||||
&self,
|
||||
include_primary_outputs: bool,
|
||||
include_lib32_output: bool,
|
||||
) -> HashSet<String> {
|
||||
let mut names = HashSet::new();
|
||||
if include_primary_outputs {
|
||||
for output in self.outputs() {
|
||||
let output_name = output.name.clone();
|
||||
names.insert(output_name.clone());
|
||||
let alternatives = self.alternatives_for_output(&output_name);
|
||||
for provided in alternatives.provides {
|
||||
names.insert(provided);
|
||||
}
|
||||
}
|
||||
}
|
||||
if include_lib32_output {
|
||||
let output_name = self.lib32_package_name();
|
||||
names.insert(output_name.clone());
|
||||
let alternatives = self.alternatives_for_output(&output_name);
|
||||
for provided in alternatives.provides {
|
||||
names.insert(provided);
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// Return alternatives/provides for a specific output package name.
|
||||
///
|
||||
/// If no per-output override exists, returns the top-level alternatives.
|
||||
pub fn alternatives_for_output(&self, pkg_name: &str) -> Alternatives {
|
||||
if pkg_name == self.lib32_package_name() {
|
||||
return self
|
||||
.package_alternatives
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.or_else(|| self.alternatives.lib32_alternatives())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
if self.docs_parent_output_name(pkg_name).is_some() {
|
||||
return self
|
||||
.package_alternatives
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
self.package_alternatives
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.alternatives.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PackageSpec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
"Package: {} v{}",
|
||||
self.package.name, self.package.version
|
||||
)?;
|
||||
writeln!(f, "Description: {}", self.package.description)?;
|
||||
writeln!(f, "Homepage: {}", self.package.homepage)?;
|
||||
writeln!(f, "License: {}", self.package.license.join(", "))?;
|
||||
writeln!(f, "Sources: {}", self.source.len())?;
|
||||
writeln!(f, "Build Type: {:?}", self.build.build_type)?;
|
||||
if !self.alternatives.provides.is_empty() {
|
||||
writeln!(f, "Provides: {}", self.alternatives.provides.join(", "))?;
|
||||
}
|
||||
if !self.alternatives.conflicts.is_empty() {
|
||||
writeln!(f, "Conflicts: {}", self.alternatives.conflicts.join(", "))?;
|
||||
}
|
||||
if !self.alternatives.replaces.is_empty() {
|
||||
writeln!(f, "Replaces: {}", self.alternatives.replaces.join(", "))?;
|
||||
}
|
||||
if !self.dependencies.groups.is_empty() {
|
||||
writeln!(f, "Groups: {}", self.dependencies.groups.join(", "))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageSpec {
|
||||
/// Generate the standard package filename: <name>-<version>-<revision>-<arch>.depot.pkg.tar.zst
|
||||
pub fn package_filename(&self, arch: &str) -> String {
|
||||
format!(
|
||||
"{}-{}-{}-{}.depot.pkg.tar.zst",
|
||||
self.package.name, self.package.version, self.package.revision, arch
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Package specification structures and TOML parsing
|
||||
|
||||
mod config;
|
||||
mod loading;
|
||||
mod model;
|
||||
|
||||
pub use loading::PackageSpec;
|
||||
pub use model::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+57
-1
@@ -144,6 +144,7 @@ struct NodeData {
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalSpecHit {
|
||||
spec_name: String,
|
||||
real_name: Option<String>,
|
||||
path: PathBuf,
|
||||
provides: Vec<String>,
|
||||
replaces: Vec<String>,
|
||||
@@ -433,7 +434,11 @@ impl<'a> Resolver<'a> {
|
||||
.or_else(|| self.opts.local_sibling_root.clone());
|
||||
if let Some(root) = local_sibling_root {
|
||||
for hit in self.local_sibling_hits(&root)? {
|
||||
let exact = hit.spec_name.eq_ignore_ascii_case(dep_name);
|
||||
let exact = hit.spec_name.eq_ignore_ascii_case(dep_name)
|
||||
|| hit
|
||||
.real_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(dep_name));
|
||||
let replaces = hit
|
||||
.replaces
|
||||
.iter()
|
||||
@@ -626,6 +631,7 @@ impl<'a> Resolver<'a> {
|
||||
spec.apply_config(self.config);
|
||||
hits.push(LocalSpecHit {
|
||||
spec_name: spec.package.name.clone(),
|
||||
real_name: spec.package.real_name.clone(),
|
||||
path: path.to_path_buf(),
|
||||
provides: spec.alternatives.provides.clone(),
|
||||
replaces: spec.alternatives.replaces.clone(),
|
||||
@@ -1249,6 +1255,56 @@ version = "1.0.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_dependency_install_plan_matches_local_sibling_real_name() {
|
||||
let rootfs = tempfile::tempdir().unwrap();
|
||||
let repo_root = tempfile::tempdir().unwrap();
|
||||
let config = Config::for_rootfs(rootfs.path());
|
||||
|
||||
let libressl_dir = repo_root.path().join("libressl43");
|
||||
fs::create_dir_all(&libressl_dir).unwrap();
|
||||
fs::write(
|
||||
libressl_dir.join("libressl43.toml"),
|
||||
r#"
|
||||
[build]
|
||||
type = "meta"
|
||||
|
||||
[package]
|
||||
description = "LibreSSL"
|
||||
homepage = "https://www.libressl.org/"
|
||||
license = "ISC"
|
||||
name = "libressl43"
|
||||
real_name = "libressl"
|
||||
version = "4.3.2"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let plan = build_dependency_install_plan(
|
||||
&config,
|
||||
rootfs.path(),
|
||||
&["libressl".to_string()],
|
||||
PlannerOptions {
|
||||
assume_yes: false,
|
||||
prefer_binary: false,
|
||||
local_sibling_root: Some(repo_root.path().to_path_buf()),
|
||||
include_test_deps: false,
|
||||
lib32_only_requested_specs: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(plan.steps.len(), 1);
|
||||
assert_eq!(plan.steps[0].package, "libressl43");
|
||||
assert!(matches!(
|
||||
plan.steps[0].origin,
|
||||
super::PlanOrigin::Source {
|
||||
local_sibling: true,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_dependency_edge_skips_active_binary_cycle_back_edge() {
|
||||
let rootfs = tempfile::tempdir().unwrap();
|
||||
|
||||
+1
-19
@@ -13,7 +13,6 @@ use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use tempfile::{NamedTempFile, tempdir};
|
||||
use walkdir::WalkDir;
|
||||
use zstd::stream::read::Decoder as ZstdDecoder;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -599,24 +598,7 @@ fn copy_file_preserve_metadata(src: &Path, dst: &Path) -> Result<()> {
|
||||
}
|
||||
|
||||
fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
||||
for entry in WalkDir::new(src) {
|
||||
crate::interrupts::check()?;
|
||||
let entry = entry?;
|
||||
let rel = entry.path().strip_prefix(src).unwrap();
|
||||
let target = dst.join(rel);
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target)?;
|
||||
} else if entry.file_type().is_symlink() {
|
||||
let target_link = fs::read_link(entry.path())?;
|
||||
unix_fs::symlink(target_link, &target)?;
|
||||
} else {
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
copy_file_preserve_metadata(entry.path(), &target)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
crate::fs_copy::copy_tree_preserving_links(src, dst)
|
||||
}
|
||||
|
||||
fn extract_zip_archive<R: Read + std::io::Seek>(
|
||||
|
||||
+10
-23
@@ -15,11 +15,9 @@ use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::os::unix::fs as unix_fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use url::Url;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn expand_manual_source_value(spec: &PackageSpec, raw: &str) -> String {
|
||||
let carch = spec.build.flags.carch.as_str();
|
||||
@@ -642,25 +640,7 @@ fn prepare_one(
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dst)?;
|
||||
for entry in WalkDir::new(src) {
|
||||
crate::interrupts::check()?;
|
||||
let entry = entry?;
|
||||
let rel = entry.path().strip_prefix(src).unwrap();
|
||||
let target = dst.join(rel);
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target)?;
|
||||
} else if entry.file_type().is_symlink() {
|
||||
let target_link = fs::read_link(entry.path())?;
|
||||
unix_fs::symlink(target_link, &target)?;
|
||||
} else {
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(entry.path(), &target)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
crate::fs_copy::copy_tree_preserving_links(src, dst)
|
||||
}
|
||||
|
||||
pub(crate) fn split_git_url(url: &str) -> Option<(String, String)> {
|
||||
@@ -686,9 +666,9 @@ pub(crate) fn split_git_url(url: &str) -> Option<(String, String)> {
|
||||
return Some((base.to_string(), rev.to_string()));
|
||||
}
|
||||
|
||||
// Check for bare .git URL without revision - default to HEAD
|
||||
// Check for bare .git URL or explicit git:// URL without revision - default to HEAD.
|
||||
let lower = url.to_ascii_lowercase();
|
||||
if lower.ends_with(".git") {
|
||||
if lower.ends_with(".git") || lower.starts_with("git://") {
|
||||
return Some((url.to_string(), "HEAD".to_string()));
|
||||
}
|
||||
|
||||
@@ -875,6 +855,13 @@ mod tests {
|
||||
assert_eq!(rev, "HEAD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_git_url_accepts_bare_git_scheme_url() {
|
||||
let (base, rev) = split_git_url("git://git.suckless.org/ubase").unwrap();
|
||||
assert_eq!(base, "git://git.suckless.org/ubase");
|
||||
assert_eq!(rev, "HEAD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_git_url_rejects_archive_urls() {
|
||||
assert!(split_git_url("https://example.com/foo.tar.gz#deadbeef").is_none());
|
||||
|
||||
+160
-401
@@ -2,12 +2,13 @@
|
||||
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -18,6 +19,12 @@ use walkdir::WalkDir;
|
||||
pub const INTERNAL_DEPOT_DIR: &str = ".depot";
|
||||
pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs";
|
||||
|
||||
type HardlinkKey = (u64, u64);
|
||||
|
||||
fn hardlink_key(metadata: &fs::Metadata) -> Option<HardlinkKey> {
|
||||
(metadata.nlink() > 1).then_some((metadata.dev(), metadata.ino()))
|
||||
}
|
||||
|
||||
fn is_info_dir_index_path(rel_path: &str) -> bool {
|
||||
matches!(
|
||||
rel_path,
|
||||
@@ -46,8 +53,6 @@ fn is_skipped_install_path(rel_path: &str) -> bool {
|
||||
|| p == INTERNAL_DEPOT_DIR
|
||||
|| p.strip_prefix(INTERNAL_DEPOT_DIR)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
|| p == "destdir"
|
||||
|| p.starts_with("destdir/")
|
||||
|| p == "scripts"
|
||||
|| p.starts_with("scripts/")
|
||||
|| is_purged_payload_path(p)
|
||||
@@ -533,20 +538,60 @@ fn is_elf_file(path: &Path) -> Result<bool> {
|
||||
|
||||
fn auto_strip_elf_files(destdir: &Path, strip_command: &str) -> Result<usize> {
|
||||
let mut stripped = 0usize;
|
||||
let mut hardlink_keys_by_path: HashMap<PathBuf, HardlinkKey> = HashMap::new();
|
||||
let mut stripped_hardlinks: HashMap<HardlinkKey, PathBuf> = HashMap::new();
|
||||
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;
|
||||
}
|
||||
let path = entry.path();
|
||||
let metadata = path
|
||||
.symlink_metadata()
|
||||
.with_context(|| format!("Failed to inspect {}", path.display()))?;
|
||||
if let Some(key) = hardlink_key(&metadata) {
|
||||
hardlink_keys_by_path.insert(path.to_path_buf(), key);
|
||||
}
|
||||
}
|
||||
|
||||
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
let metadata = path
|
||||
.symlink_metadata()
|
||||
.with_context(|| format!("Failed to inspect {}", path.display()))?;
|
||||
let hardlink_key = hardlink_keys_by_path
|
||||
.get(path)
|
||||
.copied()
|
||||
.or_else(|| hardlink_key(&metadata));
|
||||
if !is_elf_file(path)? {
|
||||
continue;
|
||||
}
|
||||
if let Some(stripped_path) = hardlink_key.and_then(|key| stripped_hardlinks.get(&key)) {
|
||||
fs::remove_file(path).with_context(|| {
|
||||
format!(
|
||||
"Failed to replace hardlinked ELF after stripping: {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
fs::hard_link(stripped_path, path).with_context(|| {
|
||||
format!(
|
||||
"Failed to restore stripped hardlink {} -> {}",
|
||||
path.display(),
|
||||
stripped_path.display()
|
||||
)
|
||||
})?;
|
||||
stripped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = Command::new(strip_command)
|
||||
.arg("--strip-debug")
|
||||
@@ -568,6 +613,9 @@ fn auto_strip_elf_files(destdir: &Path, strip_command: &str) -> Result<usize> {
|
||||
);
|
||||
}
|
||||
stripped += 1;
|
||||
if let Some(key) = hardlink_key {
|
||||
stripped_hardlinks.insert(key, path.to_path_buf());
|
||||
}
|
||||
}
|
||||
Ok(stripped)
|
||||
}
|
||||
@@ -909,189 +957,9 @@ pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let normalized = normalize_lbi_layout(destdir)?;
|
||||
if normalized > 0 {
|
||||
crate::log_info!("Normalized {} path(s) into the /system layout", normalized);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_lbi_layout(destdir: &Path) -> Result<usize> {
|
||||
let mut roots = vec![destdir.to_path_buf()];
|
||||
let outputs = output_staging_root(destdir);
|
||||
if outputs.exists() {
|
||||
let mut output_dirs = fs::read_dir(&outputs)
|
||||
.with_context(|| format!("Failed to read {}", outputs.display()))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to read output staging entry from {}",
|
||||
outputs.display()
|
||||
)
|
||||
})?;
|
||||
output_dirs.sort_by_key(|entry| entry.file_name());
|
||||
for entry in output_dirs {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
roots.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut changed = 0usize;
|
||||
for root in roots {
|
||||
changed += normalize_lbi_tree_paths(&root)?;
|
||||
changed += rewrite_lbi_pkgconfig_files(&root)?;
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
fn normalize_lbi_tree_paths(root: &Path) -> Result<usize> {
|
||||
let mappings = [
|
||||
("usr/share/man", "system/documentation/man-pages"),
|
||||
("usr/share/info", "system/documentation/info"),
|
||||
("usr/bin", "system/binaries"),
|
||||
("usr/sbin", "system/systembinaries"),
|
||||
("usr/lib64", "system/libraries"),
|
||||
("usr/lib", "system/libraries"),
|
||||
("usr/include", "system/headers"),
|
||||
("usr/share", "system/share"),
|
||||
("bin", "system/binaries"),
|
||||
("sbin", "system/systembinaries"),
|
||||
("lib64", "system/libraries"),
|
||||
("lib", "system/libraries"),
|
||||
("include", "system/headers"),
|
||||
("etc", "system/configuration"),
|
||||
("var", "system/variable"),
|
||||
("system/bin", "system/binaries"),
|
||||
("system/sbin", "system/systembinaries"),
|
||||
("system/lib64", "system/libraries"),
|
||||
("system/lib", "system/libraries"),
|
||||
("system/include", "system/headers"),
|
||||
("system/share/man", "system/documentation/man-pages"),
|
||||
("system/share/info", "system/documentation/info"),
|
||||
];
|
||||
|
||||
let mut changed = 0usize;
|
||||
for (from, to) in mappings {
|
||||
let src = root.join(from);
|
||||
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);
|
||||
move_lbi_path(&src, &dst)?;
|
||||
changed += 1;
|
||||
}
|
||||
prune_empty_dirs(root, &["usr", "system"])?;
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
fn move_lbi_path(src: &Path, dst: &Path) -> Result<()> {
|
||||
if src == dst {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = dst.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
if !dst.exists() {
|
||||
fs::rename(src, dst)
|
||||
.with_context(|| format!("Failed to move {} to {}", src.display(), dst.display()))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src_meta = fs::symlink_metadata(src)
|
||||
.with_context(|| format!("Failed to inspect {}", src.display()))?;
|
||||
let dst_meta = fs::symlink_metadata(dst)
|
||||
.with_context(|| format!("Failed to inspect {}", dst.display()))?;
|
||||
if src_meta.is_dir() && dst_meta.is_dir() {
|
||||
move_directory_contents(src, dst)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"Refusing to overwrite existing path while normalizing /system layout: {}",
|
||||
dst.display()
|
||||
);
|
||||
}
|
||||
|
||||
fn prune_empty_dirs(root: &Path, dirs: &[&str]) -> Result<()> {
|
||||
for dir in dirs {
|
||||
let path = root.join(dir);
|
||||
if path.exists() && path.is_dir() && is_directory_empty(&path)? {
|
||||
fs::remove_dir(&path)
|
||||
.with_context(|| format!("Failed to remove empty directory {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rewrite_lbi_pkgconfig_files(root: &Path) -> Result<usize> {
|
||||
let mut changed = 0usize;
|
||||
if !root.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
for entry in WalkDir::new(root).follow_links(false) {
|
||||
let entry = entry.with_context(|| format!("Failed to walk {}", root.display()))?;
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("pc") {
|
||||
continue;
|
||||
}
|
||||
let Ok(original) = fs::read_to_string(path) else {
|
||||
continue;
|
||||
};
|
||||
let rewritten = rewrite_lbi_pkgconfig_text(&original);
|
||||
if rewritten != original {
|
||||
fs::write(path, rewritten)
|
||||
.with_context(|| format!("Failed to rewrite pkg-config file {}", path.display()))?;
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
fn rewrite_lbi_pkgconfig_text(input: &str) -> String {
|
||||
input
|
||||
.replace("prefix=/usr", "prefix=/system")
|
||||
.replace("exec_prefix=/usr", "exec_prefix=/system")
|
||||
.replace("bindir=${prefix}/bin", "bindir=${prefix}/binaries")
|
||||
.replace("sbindir=${prefix}/sbin", "sbindir=${prefix}/systembinaries")
|
||||
.replace("libdir=${prefix}/lib64", "libdir=${prefix}/libraries")
|
||||
.replace("libdir=${prefix}/lib", "libdir=${prefix}/libraries")
|
||||
.replace(
|
||||
"includedir=${prefix}/include",
|
||||
"includedir=${prefix}/headers",
|
||||
)
|
||||
.replace(
|
||||
"mandir=${prefix}/share/man",
|
||||
"mandir=${prefix}/documentation/man-pages",
|
||||
)
|
||||
.replace(
|
||||
"infodir=${prefix}/share/info",
|
||||
"infodir=${prefix}/documentation/info",
|
||||
)
|
||||
.replace("/usr/sbin", "/system/systembinaries")
|
||||
.replace("/usr/bin", "/system/binaries")
|
||||
.replace("/usr/lib64", "/system/libraries")
|
||||
.replace("/usr/lib", "/system/libraries")
|
||||
.replace("/usr/include", "/system/headers")
|
||||
.replace("/usr/share/man", "/system/documentation/man-pages")
|
||||
.replace("/usr/share/info", "/system/documentation/info")
|
||||
.replace("/usr/share", "/system/share")
|
||||
}
|
||||
|
||||
/// Copy license files into the staged tree.
|
||||
///
|
||||
/// Copies common license file patterns from the source directory root into:
|
||||
@@ -1682,6 +1550,7 @@ pub fn install_atomic(
|
||||
removed: Vec::new(),
|
||||
};
|
||||
let mut staged_paths = HashSet::new();
|
||||
let mut installed_hardlinks: HashMap<HardlinkKey, String> = HashMap::new();
|
||||
|
||||
let result: Result<()> = (|| {
|
||||
// First, create all directories from destdir (for packages with only directories)
|
||||
@@ -1816,10 +1685,30 @@ pub fn install_atomic(
|
||||
std::os::unix::fs::symlink(target, &dest_path)
|
||||
.with_context(|| format!("Failed to create symlink: {}", install_rel_path))?;
|
||||
tx.replay_relocated_dir_if_present(&install_rel_path)?;
|
||||
} else {
|
||||
let hardlink_key = if keep_as_depotnew {
|
||||
None
|
||||
} else {
|
||||
hardlink_key(&metadata)
|
||||
};
|
||||
if let Some(first_rel_path) =
|
||||
hardlink_key.and_then(|key| installed_hardlinks.get(&key))
|
||||
{
|
||||
let first_path = rootfs.join(first_rel_path);
|
||||
fs::hard_link(&first_path, &dest_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to install hardlink: {} -> {}",
|
||||
install_rel_path, first_rel_path
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
fs::copy(src_path, &dest_path)
|
||||
.with_context(|| format!("Failed to install: {}", install_rel_path))?;
|
||||
apply_unix_mode(&dest_path, &metadata)?;
|
||||
if let Some(key) = hardlink_key {
|
||||
installed_hardlinks.insert(key, install_rel_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
staged_paths.insert(install_rel_path);
|
||||
}
|
||||
@@ -1893,9 +1782,9 @@ mod tests {
|
||||
let spec = mk_spec_for_stage_processing();
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
assert!(!destdir.join("system/libraries/libfoo.a").exists());
|
||||
assert!(!destdir.join("system/libraries/libfoo.la").exists());
|
||||
assert!(destdir.join("system/libraries/libfoo.so").exists());
|
||||
assert!(!destdir.join("usr/lib/libfoo.a").exists());
|
||||
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||
assert!(destdir.join("usr/lib/libfoo.so").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1910,8 +1799,8 @@ mod tests {
|
||||
spec.build.flags.no_delete_static = true;
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
assert!(destdir.join("system/libraries/libfoo.a").exists());
|
||||
assert!(!destdir.join("system/libraries/libfoo.la").exists());
|
||||
assert!(destdir.join("usr/lib/libfoo.a").exists());
|
||||
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1934,18 +1823,18 @@ mod tests {
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
let docs_destdir = output_staging_dir(&destdir, "foo-docs");
|
||||
assert!(docs_destdir.join("system/share/doc/foo/README").exists());
|
||||
assert!(docs_destdir.join("usr/share/doc/foo/README").exists());
|
||||
assert!(
|
||||
docs_destdir
|
||||
.join("system/share/gtk-doc/html/foo/index.html")
|
||||
.join("usr/share/gtk-doc/html/foo/index.html")
|
||||
.exists()
|
||||
);
|
||||
assert!(docs_destdir.join("opt/foo-docs/guide.txt").exists());
|
||||
assert!(destdir.join("system/binaries/foo").exists());
|
||||
assert!(!destdir.join("system/share/doc/foo/README").exists());
|
||||
assert!(destdir.join("usr/bin/foo").exists());
|
||||
assert!(!destdir.join("usr/share/doc/foo/README").exists());
|
||||
assert!(
|
||||
!destdir
|
||||
.join("system/share/gtk-doc/html/foo/index.html")
|
||||
.join("usr/share/gtk-doc/html/foo/index.html")
|
||||
.exists()
|
||||
);
|
||||
assert!(!destdir.join("opt/foo-docs/guide.txt").exists());
|
||||
@@ -1977,172 +1866,9 @@ mod tests {
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
let docs_destdir = output_staging_dir(&destdir, "foo-dev-docs");
|
||||
assert!(
|
||||
docs_destdir
|
||||
.join("system/share/doc/foo-dev/README")
|
||||
.exists()
|
||||
);
|
||||
assert!(dev_destdir.join("system/headers/foo.h").exists());
|
||||
assert!(!dev_destdir.join("system/share/doc/foo-dev/README").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_normalizes_usr_paths_into_system_layout() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/lib/pkgconfig")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/include/foo")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/share/man/man1")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/share/info")).unwrap();
|
||||
std::fs::write(destdir.join("usr/bin/foo"), "bin").unwrap();
|
||||
std::fs::write(destdir.join("usr/share/man/man1/foo.1"), "man").unwrap();
|
||||
std::fs::write(destdir.join("usr/share/info/foo.info"), "info").unwrap();
|
||||
std::fs::write(
|
||||
destdir.join("usr/lib/pkgconfig/foo.pc"),
|
||||
"prefix=/usr\nbindir=${prefix}/bin\nlibdir=${prefix}/lib\nincludedir=${prefix}/include\nmandir=${prefix}/share/man\ninfodir=${prefix}/share/info\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let spec = mk_spec_for_stage_processing();
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
assert!(destdir.join("system/binaries/foo").exists());
|
||||
assert!(destdir.join("system/headers/foo").is_dir());
|
||||
assert!(
|
||||
destdir
|
||||
.join("system/documentation/man-pages/man1/foo.1")
|
||||
.exists()
|
||||
);
|
||||
assert!(destdir.join("system/documentation/info/foo.info").exists());
|
||||
let pc =
|
||||
std::fs::read_to_string(destdir.join("system/libraries/pkgconfig/foo.pc")).unwrap();
|
||||
assert!(pc.contains("prefix=/system"));
|
||||
assert!(pc.contains("bindir=${prefix}/binaries"));
|
||||
assert!(pc.contains("libdir=${prefix}/libraries"));
|
||||
assert!(pc.contains("includedir=${prefix}/headers"));
|
||||
assert!(pc.contains("mandir=${prefix}/documentation/man-pages"));
|
||||
assert!(pc.contains("infodir=${prefix}/documentation/info"));
|
||||
assert!(!destdir.join("usr").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_normalizes_duplicate_identical_lbi_paths() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(destdir.join("system/lib/clang/22/include")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("system/libraries/clang/22/include")).unwrap();
|
||||
std::fs::write(
|
||||
destdir.join("system/lib/clang/22/include/builtins.h"),
|
||||
"same header\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
destdir.join("system/libraries/clang/22/include/builtins.h"),
|
||||
"same header\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let spec = mk_spec_for_stage_processing();
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
assert!(
|
||||
!destdir
|
||||
.join("system/lib/clang/22/include/builtins.h")
|
||||
.exists()
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(destdir.join("system/libraries/clang/22/include/builtins.h"))
|
||||
.unwrap(),
|
||||
"same header\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(destdir.join("system/lib/clang/22/include")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("system/libraries/clang/22/include")).unwrap();
|
||||
std::fs::write(
|
||||
destdir.join("system/lib/clang/22/include/builtins.h"),
|
||||
"left header\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
destdir.join("system/libraries/clang/22/include/builtins.h"),
|
||||
"right header\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let spec = mk_spec_for_stage_processing();
|
||||
let err = process(&destdir, &spec).unwrap_err();
|
||||
|
||||
assert!(
|
||||
err.to_string().contains("destination already exists"),
|
||||
"{err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_normalizes_split_output_usr_paths() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let destdir = tmp.path().join("dest");
|
||||
let out = destdir.join(".depot/outputs/foo-devel/usr/lib/pkgconfig");
|
||||
std::fs::create_dir_all(&out).unwrap();
|
||||
std::fs::write(out.join("foo.pc"), "prefix=/usr\nlibdir=/usr/lib\n").unwrap();
|
||||
|
||||
let spec = mk_spec_for_stage_processing();
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
let pc_path = destdir.join(".depot/outputs/foo-devel/system/libraries/pkgconfig/foo.pc");
|
||||
assert!(pc_path.exists());
|
||||
let pc = std::fs::read_to_string(pc_path).unwrap();
|
||||
assert!(pc.contains("prefix=/system"));
|
||||
assert!(pc.contains("libdir=/system/libraries"));
|
||||
assert!(docs_destdir.join("usr/share/doc/foo-dev/README").exists());
|
||||
assert!(dev_destdir.join("usr/include/foo.h").exists());
|
||||
assert!(!dev_destdir.join("usr/share/doc/foo-dev/README").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2319,6 +2045,29 @@ mod tests {
|
||||
assert!(!rootfs.join("etc/locale.gen.depotnew").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_atomic_preserves_staged_hardlinks() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let rootfs = tmp.path().join("root");
|
||||
let destdir = tmp.path().join("dest");
|
||||
let tx_base = tmp.path().join("tx");
|
||||
std::fs::create_dir_all(&rootfs).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||
|
||||
let coreutils = destdir.join("usr/bin/coreutils");
|
||||
let ls = destdir.join("usr/bin/ls");
|
||||
std::fs::write(&coreutils, "multicall").unwrap();
|
||||
std::fs::hard_link(&coreutils, &ls).unwrap();
|
||||
|
||||
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||
|
||||
let coreutils_meta = rootfs.join("usr/bin/coreutils").metadata().unwrap();
|
||||
let ls_meta = rootfs.join("usr/bin/ls").metadata().unwrap();
|
||||
assert_eq!(coreutils_meta.ino(), ls_meta.ino());
|
||||
assert_eq!(coreutils_meta.nlink(), 2);
|
||||
assert_eq!(ls_meta.nlink(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_atomic_keep_wildcard_matches_directory_children() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -2415,32 +2164,32 @@ mod tests {
|
||||
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();
|
||||
std::fs::create_dir_all(rootfs.join("lib/depot")).unwrap();
|
||||
std::fs::write(rootfs.join("lib/depot/lock"), "state").unwrap();
|
||||
std::fs::create_dir_all(rootfs.join("usr/lib/depot")).unwrap();
|
||||
std::fs::write(rootfs.join("usr/lib/depot/lock"), "state").unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/lib/misc")).unwrap();
|
||||
std::os::unix::fs::symlink("usr/lib", destdir.join("lib")).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());
|
||||
let lib_meta = rootfs.join("lib").symlink_metadata().unwrap();
|
||||
assert!(lib_meta.file_type().is_symlink());
|
||||
assert_eq!(
|
||||
std::fs::read_link(rootfs.join("var")).unwrap(),
|
||||
PathBuf::from("system/variable")
|
||||
std::fs::read_link(rootfs.join("lib")).unwrap(),
|
||||
PathBuf::from("usr/lib")
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("system/variable/lib/depot/lock")).unwrap(),
|
||||
std::fs::read_to_string(rootfs.join("usr/lib/depot/lock")).unwrap(),
|
||||
"state"
|
||||
);
|
||||
assert!(rootfs.join("system/variable/lib/misc").is_dir());
|
||||
assert!(rootfs.join("usr/lib/misc").is_dir());
|
||||
|
||||
tx.rollback().unwrap();
|
||||
let restored = rootfs.join("var").symlink_metadata().unwrap();
|
||||
let restored = rootfs.join("lib").symlink_metadata().unwrap();
|
||||
assert!(restored.file_type().is_dir());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("var/lib/depot/lock")).unwrap(),
|
||||
std::fs::read_to_string(rootfs.join("lib/depot/lock")).unwrap(),
|
||||
"state"
|
||||
);
|
||||
}
|
||||
@@ -2591,6 +2340,44 @@ mod tests {
|
||||
assert!(!is_elf_file(&text).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_strip_elf_files_restores_hardlinks_when_strip_replaces_file() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path().join("dest");
|
||||
let bin = dest.join("usr/bin");
|
||||
std::fs::create_dir_all(&bin).unwrap();
|
||||
|
||||
let fake_strip = tmp.path().join("fake-strip");
|
||||
std::fs::write(
|
||||
&fake_strip,
|
||||
"#!/bin/sh\nfor arg do path=$arg; done\ntmp=\"$path.tmp\"\ncp \"$path\" \"$tmp\"\nprintf stripped >> \"$tmp\"\nmv \"$tmp\" \"$path\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut perms = std::fs::metadata(&fake_strip).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&fake_strip, perms).unwrap();
|
||||
|
||||
let coreutils = bin.join("coreutils");
|
||||
let ls = bin.join("ls");
|
||||
std::fs::write(&coreutils, [0x7F, b'E', b'L', b'F', 0x02, 0x01]).unwrap();
|
||||
std::fs::hard_link(&coreutils, &ls).unwrap();
|
||||
|
||||
let stripped = auto_strip_elf_files(&dest, fake_strip.to_str().unwrap()).unwrap();
|
||||
|
||||
let coreutils_meta = coreutils.metadata().unwrap();
|
||||
let ls_meta = ls.metadata().unwrap();
|
||||
assert_eq!(stripped, 2);
|
||||
assert_eq!(coreutils_meta.ino(), ls_meta.ino());
|
||||
assert_eq!(coreutils_meta.nlink(), 2);
|
||||
assert_eq!(ls_meta.nlink(), 2);
|
||||
assert_eq!(
|
||||
std::fs::read(&coreutils).unwrap(),
|
||||
std::fs::read(&ls).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compress_manpages_zstd_rewrites_symlinks() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -2982,34 +2769,6 @@ mod tests {
|
||||
.contains(&".depot/outputs/clang/usr/bin/clang".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_manifest_skips_bootstrap_chroot_destdir_mountpoint() {
|
||||
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("destdir/system/documentation/xz")).unwrap();
|
||||
std::fs::write(destdir.join("system/binaries/xz"), "ok").unwrap();
|
||||
std::fs::write(
|
||||
destdir.join("destdir/system/documentation/xz/README"),
|
||||
"stale staging",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let manifest = generate_manifest_with_dirs(&destdir).unwrap();
|
||||
|
||||
assert!(manifest.files.contains(&"system/binaries/xz".to_string()));
|
||||
assert!(
|
||||
!manifest
|
||||
.files
|
||||
.contains(&"destdir/system/documentation/xz/README".to_string())
|
||||
);
|
||||
assert!(
|
||||
!manifest
|
||||
.directories
|
||||
.contains(&"destdir/system/documentation/xz".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Manifest containing files and directories for a package
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const STATE_FILENAME: &str = "system.toml";
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct SystemState {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) stage: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) target: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) arch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub(crate) layers: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
pub(crate) fn state_path(config: &Config) -> PathBuf {
|
||||
config.db_dir.join(STATE_FILENAME)
|
||||
}
|
||||
|
||||
pub(crate) fn load(config: &Config) -> Result<SystemState> {
|
||||
let path = state_path(config);
|
||||
if !path.exists() {
|
||||
return Ok(SystemState::default());
|
||||
}
|
||||
let content =
|
||||
fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))
|
||||
}
|
||||
|
||||
pub(crate) fn save(config: &Config, state: &SystemState) -> Result<PathBuf> {
|
||||
let path = state_path(config);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
let content = toml::to_string_pretty(state).context("Failed to serialize system state")?;
|
||||
fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub(crate) fn set_stage(config: &Config, stage: String) -> Result<SystemState> {
|
||||
let stage = normalize_token("stage", &stage)?;
|
||||
let mut state = load(config)?;
|
||||
state.stage = Some(stage);
|
||||
save(config, &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub(crate) fn add_packages_to_layer(
|
||||
config: &Config,
|
||||
layer: String,
|
||||
packages: &[String],
|
||||
) -> Result<SystemState> {
|
||||
let layer = normalize_token("layer", &layer)?;
|
||||
let mut state = load(config)?;
|
||||
let existing = state.layers.remove(&layer).unwrap_or_default();
|
||||
let mut merged = existing.into_iter().collect::<BTreeSet<_>>();
|
||||
for package in packages {
|
||||
merged.insert(normalize_token("package", package)?);
|
||||
}
|
||||
state.layers.insert(layer, merged.into_iter().collect());
|
||||
save(config, &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub(crate) fn set_layer_packages(
|
||||
config: &Config,
|
||||
layer: String,
|
||||
packages: &[String],
|
||||
) -> Result<SystemState> {
|
||||
let layer = normalize_token("layer", &layer)?;
|
||||
let mut state = load(config)?;
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
for package in packages {
|
||||
let package = normalize_token("package", package)?;
|
||||
if seen.insert(package.clone()) {
|
||||
normalized.push(package);
|
||||
}
|
||||
}
|
||||
state.layers.insert(layer, normalized);
|
||||
save(config, &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_packages_from_layer(
|
||||
config: &Config,
|
||||
layer: String,
|
||||
packages: &[String],
|
||||
) -> Result<SystemState> {
|
||||
let layer = normalize_token("layer", &layer)?;
|
||||
let remove = packages
|
||||
.iter()
|
||||
.map(|package| normalize_token("package", package))
|
||||
.collect::<Result<BTreeSet<_>>>()?;
|
||||
let mut state = load(config)?;
|
||||
if let Some(existing) = state.layers.get_mut(&layer) {
|
||||
existing.retain(|package| !remove.contains(package));
|
||||
if existing.is_empty() {
|
||||
state.layers.remove(&layer);
|
||||
}
|
||||
}
|
||||
save(config, &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub(crate) fn init_lbi_layout(
|
||||
rootfs: &Path,
|
||||
config: &Config,
|
||||
target: &str,
|
||||
arch: Option<&str>,
|
||||
force: bool,
|
||||
) -> Result<SystemState> {
|
||||
let target = normalize_token("target", target)?;
|
||||
let arch = match arch {
|
||||
Some(arch) => normalize_token("arch", arch)?,
|
||||
None => crate::cross::target_arch_from_triple(&target).to_string(),
|
||||
};
|
||||
|
||||
ensure_lbi_layout_paths(rootfs)?;
|
||||
write_lbi_build_config(rootfs, &target, &arch, force)?;
|
||||
|
||||
let mut state = load(config)?;
|
||||
state.target = Some(target);
|
||||
state.arch = Some(arch);
|
||||
state.stage.get_or_insert_with(|| "layout".to_string());
|
||||
save(config, &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_lbi_layout_paths(rootfs: &Path) -> Result<()> {
|
||||
create_lbi_directories(rootfs)?;
|
||||
create_lbi_links(rootfs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_token(kind: &str, value: &str) -> Result<String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
bail!("{kind} must not be empty");
|
||||
}
|
||||
if trimmed.contains('/') || trimmed.contains('\0') {
|
||||
bail!("{kind} must not contain '/' or NUL bytes: {trimmed}");
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn create_lbi_directories(rootfs: &Path) -> Result<()> {
|
||||
let dirs = [
|
||||
"system",
|
||||
"system/configuration",
|
||||
"system/binaries",
|
||||
"system/systembinaries",
|
||||
"system/libraries",
|
||||
"system/headers",
|
||||
"system/share",
|
||||
"system/documentation",
|
||||
"system/documentation/man-pages",
|
||||
"system/documentation/info",
|
||||
"system/tools",
|
||||
"system/variable",
|
||||
"system/variable/lib",
|
||||
"system/users",
|
||||
"system/charlie",
|
||||
"system/devices",
|
||||
"system/devices/pts",
|
||||
"system/devices/shm",
|
||||
"system/processes",
|
||||
"system/run",
|
||||
"system/system",
|
||||
"system/temporary",
|
||||
"usr",
|
||||
];
|
||||
|
||||
for dir in dirs {
|
||||
let path = rootfs.join(dir);
|
||||
fs::create_dir_all(&path)
|
||||
.with_context(|| format!("Failed to create {}", path.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_lbi_links(rootfs: &Path) -> Result<()> {
|
||||
let links = [
|
||||
("etc", "system/configuration"),
|
||||
("bin", "system/binaries"),
|
||||
("sbin", "system/systembinaries"),
|
||||
("lib", "system/libraries"),
|
||||
("var", "system/variable"),
|
||||
("home", "system/users"),
|
||||
("root", "system/charlie"),
|
||||
("dev", "system/devices"),
|
||||
("proc", "system/processes"),
|
||||
("run", "system/run"),
|
||||
("sys", "system/system"),
|
||||
("usr/bin", "../system/binaries"),
|
||||
("usr/sbin", "../system/systembinaries"),
|
||||
("usr/lib", "../system/libraries"),
|
||||
("usr/include", "../system/headers"),
|
||||
("usr/share", "../system/share"),
|
||||
("system/lib", "libraries"),
|
||||
];
|
||||
|
||||
for (link, target) in links {
|
||||
ensure_relative_symlink(rootfs, Path::new(link), Path::new(target))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn ensure_relative_symlink(rootfs: &Path, link: &Path, target: &Path) -> Result<()> {
|
||||
use std::os::unix::fs as unix_fs;
|
||||
|
||||
let link_path = rootfs.join(link);
|
||||
if let Ok(metadata) = fs::symlink_metadata(&link_path) {
|
||||
if !metadata.file_type().is_symlink() {
|
||||
if metadata.is_dir() {
|
||||
let target_path = link_path
|
||||
.parent()
|
||||
.unwrap_or(rootfs)
|
||||
.join(target)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
merge_dir_contents(&link_path, &target_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to merge existing directory {} into {}",
|
||||
link_path.display(),
|
||||
target_path.display()
|
||||
)
|
||||
})?;
|
||||
fs::remove_dir(&link_path)
|
||||
.with_context(|| format!("Failed to remove {}", link_path.display()))?;
|
||||
} else {
|
||||
bail!(
|
||||
"Refusing to replace non-directory path while creating LBI layout: {}",
|
||||
link_path.display()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let existing = fs::read_link(&link_path)
|
||||
.with_context(|| format!("Failed to read symlink {}", link_path.display()))?;
|
||||
if existing == target {
|
||||
return Ok(());
|
||||
}
|
||||
fs::remove_file(&link_path)
|
||||
.with_context(|| format!("Failed to replace symlink {}", link_path.display()))?;
|
||||
}
|
||||
}
|
||||
if let Some(parent) = link_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
unix_fs::symlink(target, &link_path)
|
||||
.with_context(|| format!("Failed to create symlink {}", link_path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_dir_contents(src: &Path, dest: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
|
||||
let mut entries = fs::read_dir(src)
|
||||
.with_context(|| format!("Failed to read {}", src.display()))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.with_context(|| format!("Failed to read entry from {}", src.display()))?;
|
||||
entries.sort_by_key(|entry| entry.file_name());
|
||||
|
||||
for entry in entries {
|
||||
let source_path = entry.path();
|
||||
let dest_path = dest.join(entry.file_name());
|
||||
if dest_path.exists() {
|
||||
let source_type = entry
|
||||
.file_type()
|
||||
.with_context(|| format!("Failed to inspect {}", source_path.display()))?;
|
||||
let dest_type = fs::symlink_metadata(&dest_path)
|
||||
.with_context(|| format!("Failed to inspect {}", dest_path.display()))?
|
||||
.file_type();
|
||||
if source_type.is_dir() && dest_type.is_dir() {
|
||||
merge_dir_contents(&source_path, &dest_path)?;
|
||||
fs::remove_dir(&source_path)
|
||||
.with_context(|| format!("Failed to remove {}", source_path.display()))?;
|
||||
continue;
|
||||
}
|
||||
bail!(
|
||||
"Refusing to overwrite existing path while merging LBI directory: {}",
|
||||
dest_path.display()
|
||||
);
|
||||
}
|
||||
fs::rename(&source_path, &dest_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to move {} to {}",
|
||||
source_path.display(),
|
||||
dest_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn ensure_relative_symlink(_rootfs: &Path, _link: &Path, _target: &Path) -> Result<()> {
|
||||
bail!("LBI layout initialization requires Unix symlink support")
|
||||
}
|
||||
|
||||
fn write_lbi_build_config(rootfs: &Path, target: &str, arch: &str, force: bool) -> Result<PathBuf> {
|
||||
let path = rootfs.join("etc/depot.d/build.toml");
|
||||
if path.exists() && !force {
|
||||
bail!(
|
||||
"{} already exists; re-run with --force to replace it",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
let content = lbi_build_config_toml(target, arch);
|
||||
fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn lbi_build_config_toml(target: &str, arch: &str) -> String {
|
||||
let makeflags = format!(
|
||||
"-j{}",
|
||||
std::thread::available_parallelism()
|
||||
.map(|parallelism| parallelism.get())
|
||||
.unwrap_or(1)
|
||||
);
|
||||
format!(
|
||||
r#"# Generated by `depot system init-lbi`.
|
||||
# These defaults mirror the Linux by Intent /system layout.
|
||||
|
||||
[flags]
|
||||
prefix = "/system"
|
||||
bindir = "/system/binaries"
|
||||
sbindir = "/system/systembinaries"
|
||||
libdir = "/system/libraries"
|
||||
libexecdir = "/system/libraries"
|
||||
sysconfdir = "/system/configuration"
|
||||
localstatedir = "/system/variable"
|
||||
sharedstatedir = "/system/variable/lib"
|
||||
includedir = "/system/headers"
|
||||
datarootdir = "/system/share"
|
||||
datadir = "/system/share"
|
||||
mandir = "/system/documentation/man-pages"
|
||||
infodir = "/system/documentation/info"
|
||||
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}"
|
||||
cflags = ["-O2", "-pipe"]
|
||||
cxxflags = ["-O2", "-pipe"]
|
||||
ldflags = ["-Wl,-rpath,/system/libraries"]
|
||||
makeflags = "{makeflags}"
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn layer_add_sorts_and_deduplicates_packages() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config = Config::for_rootfs(tmp.path());
|
||||
let state = add_packages_to_layer(
|
||||
&config,
|
||||
"base".to_string(),
|
||||
&["zlib".to_string(), "musl".to_string(), "zlib".to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(state.layers["base"], vec!["musl", "zlib"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_layer_packages_replaces_only_named_layer() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config = Config::for_rootfs(tmp.path());
|
||||
add_packages_to_layer(&config, "custom".to_string(), &["kept".to_string()]).unwrap();
|
||||
add_packages_to_layer(
|
||||
&config,
|
||||
"base".to_string(),
|
||||
&["old".to_string(), "zlib".to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
let state = set_layer_packages(
|
||||
&config,
|
||||
"base".to_string(),
|
||||
&["zlib".to_string(), "musl".to_string(), "zlib".to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(state.layers["base"], vec!["zlib", "musl"]);
|
||||
assert_eq!(state.layers["custom"], vec!["kept"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_is_persisted() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config = Config::for_rootfs(tmp.path());
|
||||
set_stage(&config, "cross-tools".to_string()).unwrap();
|
||||
let loaded = load(&config).unwrap();
|
||||
assert_eq!(loaded.stage.as_deref(), Some("cross-tools"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn init_lbi_creates_layout_and_build_defaults() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config = Config::for_rootfs(tmp.path());
|
||||
let state = init_lbi_layout(
|
||||
tmp.path(),
|
||||
&config,
|
||||
"x86_64-unknown-linux-musl",
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(state.stage.as_deref(), Some("layout"));
|
||||
assert_eq!(state.arch.as_deref(), Some("x86_64"));
|
||||
assert!(tmp.path().join("system/binaries").is_dir());
|
||||
assert!(tmp.path().join("system/devices/pts").is_dir());
|
||||
assert!(tmp.path().join("system/devices/shm").is_dir());
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("usr/bin")).unwrap(),
|
||||
PathBuf::from("../system/binaries")
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("dev")).unwrap(),
|
||||
PathBuf::from("system/devices")
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("proc")).unwrap(),
|
||||
PathBuf::from("system/processes")
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("sys")).unwrap(),
|
||||
PathBuf::from("system/system")
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("run")).unwrap(),
|
||||
PathBuf::from("system/run")
|
||||
);
|
||||
let build_toml = fs::read_to_string(tmp.path().join("etc/depot.d/build.toml")).unwrap();
|
||||
assert!(build_toml.contains("prefix = \"/system\""));
|
||||
assert!(build_toml.contains("chost = \"x86_64-unknown-linux-musl\""));
|
||||
let makeflags_line = build_toml
|
||||
.lines()
|
||||
.find(|line| line.trim_start().starts_with("makeflags = \"-j"))
|
||||
.expect("expected makeflags default in generated build.toml");
|
||||
let jobs = makeflags_line
|
||||
.trim()
|
||||
.trim_start_matches("makeflags = \"-j")
|
||||
.trim_end_matches('"')
|
||||
.parse::<usize>()
|
||||
.expect("expected numeric makeflags job count");
|
||||
assert!(jobs >= 1);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ensure_lbi_layout_paths_reconciles_existing_usr_include_directory() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(tmp.path().join("usr/include")).unwrap();
|
||||
fs::write(tmp.path().join("usr/include/marker.h"), "/* marker */").unwrap();
|
||||
|
||||
ensure_lbi_layout_paths(tmp.path()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("usr/include")).unwrap(),
|
||||
PathBuf::from("../system/headers")
|
||||
);
|
||||
assert!(tmp.path().join("system/headers/marker.h").exists());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn init_lbi_migrates_existing_var_contents_before_linking() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(tmp.path().join("var/lib/depot")).unwrap();
|
||||
fs::write(tmp.path().join("var/lib/depot/lock"), "").unwrap();
|
||||
let config = Config::for_rootfs(tmp.path());
|
||||
init_lbi_layout(
|
||||
tmp.path(),
|
||||
&config,
|
||||
"x86_64-unknown-linux-musl",
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fs::read_link(tmp.path().join("var")).unwrap(),
|
||||
PathBuf::from("system/variable")
|
||||
);
|
||||
assert!(tmp.path().join("system/variable/lib/depot/lock").exists());
|
||||
}
|
||||
}
|
||||
@@ -50,16 +50,6 @@ pub fn success(message: impl AsRef<str>) {
|
||||
println!("{} {}", label(Stream::Stdout, "OK", "32"), message.as_ref());
|
||||
}
|
||||
|
||||
pub fn merge_package(layer: &str, package: &str) {
|
||||
println!(
|
||||
"{} {} {} into layer {}",
|
||||
paint(Stream::Stdout, ">>>", "32;1"),
|
||||
paint(Stream::Stdout, "merging package", "36;1"),
|
||||
paint(Stream::Stdout, package, "32;1"),
|
||||
layer
|
||||
);
|
||||
}
|
||||
|
||||
pub fn warn(message: impl AsRef<str>) {
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
|
||||
Reference in New Issue
Block a user