From 9a544c60b0001e4fc6fdc83773c0169e2963728f Mon Sep 17 00:00:00 2001 From: SFG545 Date: Tue, 9 Jun 2026 06:14:15 -0500 Subject: [PATCH] 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 --- Cargo.lock | 12 +- Cargo.toml | 6 +- meson.build | 2 +- src/bootstrap.rs | 6811 --------------------------------- src/builder/autotools.rs | 2 +- src/builder/bin.rs | 26 +- src/builder/cmake.rs | 89 +- src/builder/custom.rs | 53 +- src/builder/makefile.rs | 76 +- src/builder/meson.rs | 14 +- src/builder/mod.rs | 287 +- src/builder/perl.rs | 10 - src/builder/rust.rs | 123 +- src/cli.rs | 105 - src/commands.rs | 16 +- src/commands/misc.rs | 111 - src/commands/misc/internal.rs | 28 +- src/commands/set.rs | 17 +- src/config.rs | 40 +- src/db/mod.rs | 103 +- src/deps.rs | 29 +- src/fakeroot.rs | 152 +- src/fs_copy.rs | 165 + src/interrupts.rs | 6 +- src/main.rs | 3 +- src/package/packager.rs | 61 +- src/package/spec.rs | 5274 ------------------------- src/package/spec/config.rs | 1570 ++++++++ src/package/spec/loading.rs | 378 ++ src/package/spec/mod.rs | 11 + src/package/spec/model.rs | 1061 +++++ src/package/spec/tests.rs | 2383 ++++++++++++ src/planner.rs | 58 +- src/source/extractor.rs | 20 +- src/source/mod.rs | 33 +- src/staging/mod.rs | 567 +-- src/system_state.rs | 505 --- src/ui.rs | 10 - 38 files changed, 6561 insertions(+), 13656 deletions(-) delete mode 100644 src/bootstrap.rs create mode 100644 src/fs_copy.rs delete mode 100755 src/package/spec.rs create mode 100644 src/package/spec/config.rs create mode 100644 src/package/spec/loading.rs create mode 100644 src/package/spec/mod.rs create mode 100644 src/package/spec/model.rs create mode 100644 src/package/spec/tests.rs delete mode 100644 src/system_state.rs diff --git a/Cargo.lock b/Cargo.lock index a3d3857..82f9fb0 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 7258d8d..8c2526e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/meson.build b/meson.build index 0e0ecf9..de140fd 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.36.0', + version: '0.50.0', meson_version: '>=0.60.0', default_options: ['buildtype=release'], ) diff --git a/src/bootstrap.rs b/src/bootstrap.rs deleted file mode 100644 index 7746a01..0000000 --- a/src/bootstrap.rs +++ /dev/null @@ -1,6811 +0,0 @@ -use crate::cli::BootstrapArgs; -use crate::{config, source, system_state, ui}; -use anyhow::{Context, Result}; -use std::collections::{BTreeMap, BTreeSet}; -use std::ffi::{OsStr, OsString}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use sys_mount::{Mount, MountFlags, Unmount, UnmountFlags}; -use url::Url; - -const TEMP_LAYER: &str = "temp"; -const BASE_LAYER: &str = "base"; -const DEVEL_LAYER: &str = "devel"; -const FILESYSTEM_PACKAGE: &str = "filesystem"; -const BOOTSTRAP_DIR: &str = "bootstrap/lbi"; -const DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS: &str = "DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS"; -const CHAPTER7_RETIRED_PACKAGES: &[&str] = &["llvm-clang-pass1"]; -const BOOTSTRAP_CHROOT_SHIM_DIR: &str = "/tmp/depot-bootstrap-tools"; -const BOOTSTRAP_CHROOT_PATH: &str = "/system/tools/bin:/system/binaries:/system/systembinaries:/tmp/depot-bootstrap-tools:/bin:/usr/bin:/sbin:/usr/sbin"; -const BOOK_FETCH_CACHE_BUST_PARAM: &str = "depot_refresh"; - -#[derive(Debug, Clone, PartialEq, Eq)] -struct BookPackage { - chapter: u8, - section: String, - title: String, - name: String, - version: String, - layer: String, - page_url: String, - recipe_id: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BookOperationKind { - ResetTargetTreeOwnership, - CreateVirtualFilesystemLinkTargets, - CopyBuildProfile, - EnterChroot, - CreateEssentialSystemFiles, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct BookOperation { - section: String, - title: String, - kind: BookOperationKind, - recipe_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum BookStep { - Package(BookPackage), - Operation(BookOperation), -} - -#[derive(Debug, Clone)] -struct GeneratedRecipe { - package: BookPackage, - spec_path: PathBuf, - progress_path: PathBuf, -} - -#[derive(Debug, Clone)] -struct PageRecipe { - input_files: Vec, - source_urls: Vec, - extract_dir: Option, - commands: Vec, - dependencies: Vec, - license: String, - description: String, -} - -#[derive(Debug, Clone)] -struct ManifestEntry { - url: String, - output_name: String, -} - -#[derive(Debug, Clone, Default)] -struct SourceManifest { - entries: Vec, - blake2b_512: BTreeMap, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BootstrapBuildMode { - Host, - Cross, - Chroot, -} - -#[derive(Debug, Clone)] -struct BootstrapInstallInvocation { - args: Vec, - env: Vec<(OsString, OsString)>, -} - -pub(crate) fn run(args: BootstrapArgs) -> Result<()> { - let config = config::Config::for_rootfs(&args.sysroot); - let (target, arch) = bootstrap_target_arch(&args, &config)?; - ensure_lbi_layout_for_fresh_bootstrap(&args.sysroot, &config, &target, &arch)?; - let pdf = load_book_pdf(&args)?; - let text = - pdf_extract::extract_text_from_mem(&pdf).context("Failed to extract text from book PDF")?; - let steps = parse_book_steps(&text, &args.book_url)?; - let packages = packages_from_steps(&steps); - if packages.is_empty() { - anyhow::bail!("No package sections were found in the Linux by Intent book"); - } - - let recipes = generate_recipes(&args, &config, &packages, &target, &arch)?; - let recipes_by_id = recipes - .iter() - .map(|recipe| (recipe.package.recipe_id.as_str(), recipe)) - .collect::>(); - let progress_root = bootstrap_progress_root(&config); - - for (idx, step) in steps.iter().enumerate() { - if step_requires_root(step) - && !step_is_done(step, &recipes_by_id, &progress_root, &args.sysroot, &config) - && !crate::fakeroot::is_root() - { - ensure_root_for_bootstrap()?; - return Ok(()); - } - - match step { - BookStep::Package(package) => { - let recipe = recipes_by_id - .get(package.recipe_id.as_str()) - .copied() - .with_context(|| format!("Missing generated recipe for {}", package.title))?; - let mode = build_mode_for_package(package); - ui::merge_package(&package.layer, &package.name); - install_recipe(&args.sysroot, &config, recipe, mode, &target)?; - system_state::set_stage(&config, stage_for_step(step))?; - } - BookStep::Operation(operation) => { - run_operation_step(&args.sysroot, &config, operation, &target, &arch)?; - system_state::set_stage(&config, stage_for_step(step))?; - } - } - - if step_completes_chapter(step, steps.get(idx + 1), 7) { - retire_packages_after_chapter7(&args.sysroot, &config)?; - system_state::set_stage(&config, "bootstrap-chapter7-cleanup".to_string())?; - } - } - - let layers = packages_by_layer(&packages); - for layer in [TEMP_LAYER, BASE_LAYER, DEVEL_LAYER] { - let packages = bootstrap_layer_packages_for_state(&layers, layer); - system_state::set_layer_packages(&config, layer.to_string(), &packages)?; - } - - system_state::set_stage(&config, "bootstrap-complete".to_string())?; - ui::success(format!( - "Bootstrapped {} Linux by Intent package(s) through chapter 9", - packages.len() - )); - Ok(()) -} - -fn bootstrap_target_arch( - args: &BootstrapArgs, - config: &config::Config, -) -> Result<(String, String)> { - let state = system_state::load(config)?; - let target = args - .target - .clone() - .or(state.target) - .unwrap_or_else(|| "x86_64-lbi-linux-musl".to_string()); - let arch = args - .arch - .clone() - .or(state.arch) - .unwrap_or_else(|| crate::cross::target_arch_from_triple(&target).to_string()); - Ok((target, arch)) -} - -fn ensure_lbi_layout_for_fresh_bootstrap( - sysroot: &Path, - config: &config::Config, - target: &str, - arch: &str, -) -> Result<()> { - let state = system_state::load(config)?; - system_state::ensure_lbi_layout_paths(sysroot)?; - if state.stage.is_some() - || !state.layers.is_empty() - || sysroot.join("etc/depot.d/build.toml").exists() - { - ui::info("Resuming existing bootstrap state"); - return Ok(()); - } - - ui::info("Initializing Linux by Intent sysroot layout"); - system_state::init_lbi_layout(sysroot, config, target, Some(arch), false)?; - Ok(()) -} - -fn install_recipe( - sysroot: &Path, - config: &config::Config, - recipe: &GeneratedRecipe, - mode: BootstrapBuildMode, - target: &str, -) -> Result<()> { - if bootstrap_package_is_complete(sysroot, config, recipe)? { - ui::info(format!( - "Skipping already completed package {} ({})", - recipe.package.name, recipe.package.section - )); - return Ok(()); - } - - prepare_bootstrap_package_install(sysroot, config, &recipe.package)?; - let exe = std::env::current_exe().context("Failed to locate depot executable")?; - let invocation = bootstrap_install_invocation(sysroot, recipe, mode, target, &exe)?; - let mut cmd = Command::new(&exe); - cmd.args(&invocation.args); - for (key, value) in &invocation.env { - cmd.env(key, value); - } - let status = cmd - .status() - .with_context(|| format!("Failed to run depot install for {}", recipe.package.name))?; - if !status.success() { - anyhow::bail!( - "depot install failed for {} with status {status}", - recipe.package.name - ); - } - if let Some(parent) = recipe.progress_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; - } - let mut progress = format!( - "section = \"{}\"\npackage = \"{}\"\nlayer = \"{}\"\n", - recipe.package.section, recipe.package.name, recipe.package.layer - ); - if let Some(revision) = bootstrap_recipe_revision(&recipe.package.name) { - progress.push_str(&format!("recipe_revision = {revision}\n")); - } - fs::write(&recipe.progress_path, progress) - .with_context(|| format!("Failed to write {}", recipe.progress_path.display()))?; - Ok(()) -} - -fn bootstrap_package_is_complete( - sysroot: &Path, - config: &config::Config, - recipe: &GeneratedRecipe, -) -> Result { - if package_is_retired(config, &recipe.package.name) { - return Ok(true); - } - - if !recipe.progress_path.exists() { - return Ok(false); - } - - if let Some(revision) = bootstrap_recipe_revision(&recipe.package.name) { - let progress = fs::read_to_string(&recipe.progress_path) - .with_context(|| format!("Failed to read {}", recipe.progress_path.display()))?; - let expected = format!("recipe_revision = {revision}"); - if !progress.lines().any(|line| line.trim() == expected) { - ui::info(format!( - "Reinstalling {} because its bootstrap recipe changed", - recipe.package.name - )); - return Ok(false); - } - } - - let db_path = config.installed_db_path(sysroot); - let replaced = crate::db::get_all_replaces(&db_path).with_context(|| { - format!( - "Failed to inspect replacement metadata for {}", - recipe.package.name - ) - })?; - if replaced.contains(&recipe.package.name) { - return Ok(true); - } - - let files = - crate::db::get_package_files(&db_path, &recipe.package.name).with_context(|| { - format!( - "Failed to inspect installed files for {}", - recipe.package.name - ) - })?; - if files.is_empty() { - ui::info(format!( - "Reinstalling {} because bootstrap progress exists but no installed files were recorded", - recipe.package.name - )); - return Ok(false); - } - - for rel_path in files { - if crate::staging::is_purged_payload_path(&rel_path) { - continue; - } - let disk_path = sysroot.join(&rel_path); - if disk_path.symlink_metadata().is_err() { - ui::info(format!( - "Reinstalling {} because {} is missing from the sysroot", - recipe.package.name, rel_path - )); - return Ok(false); - } - } - - for rel_path in bootstrap_required_payload_paths(&recipe.package.name) { - let disk_path = sysroot.join(rel_path); - if !disk_path.exists() { - ui::info(format!( - "Reinstalling {} because required bootstrap payload {} is missing from the sysroot", - recipe.package.name, rel_path - )); - return Ok(false); - } - } - - Ok(true) -} - -fn bootstrap_required_payload_paths(package: &str) -> &'static [&'static str] { - match package { - "bmake" => &["system/binaries/bmake", "system/share/mk/sys.mk"], - "byacc" => &["system/binaries/yacc"], - "llvm-clang-pass1" => &[ - "system/tools/bin/llvm-config", - "system/tools/bin/llvm-tblgen", - "system/tools/bin/clang-tblgen", - ], - "ubase" => &["system/binaries/id"], - _ => &[], - } -} - -fn bootstrap_recipe_revision(package: &str) -> Option { - match package { - "bmake" => Some(2), - "byacc" => Some(1), - "llvm" => Some(3), - "ubase" => Some(1), - _ => None, - } -} - -const BSDDIFFUTILS_SBASE_HANDOFF_PATHS: &[&str] = &[ - "system/binaries/cmp", - "system/documentation/man-pages/man1/cmp.1", -]; - -const BSDGREP_SBASE_HANDOFF_PATHS: &[&str] = &[ - "system/binaries/grep", - "system/documentation/man-pages/man1/grep.1", -]; - -fn prepare_bootstrap_package_install( - sysroot: &Path, - config: &config::Config, - package: &BookPackage, -) -> Result<()> { - let handoff_paths = match package.name.as_str() { - "bsddiffutils" => BSDDIFFUTILS_SBASE_HANDOFF_PATHS, - "bsdgrep" => BSDGREP_SBASE_HANDOFF_PATHS, - _ => return Ok(()), - }; - - if handoff_paths.is_empty() { - return Ok(()); - } - - let db_path = config.installed_db_path(sysroot); - if !db_path.exists() { - return Ok(()); - } - - let mut conn = rusqlite::Connection::open(&db_path) - .with_context(|| format!("Failed to open package database {}", db_path.display()))?; - let tx = conn.transaction()?; - - for rel_path in handoff_paths { - let owner = match tx.query_row( - "SELECT p.name FROM files f JOIN packages p ON f.package_id = p.id WHERE f.path = ?1", - rusqlite::params![rel_path], - |row| row.get::<_, String>(0), - ) { - Ok(owner) => owner, - Err(rusqlite::Error::QueryReturnedNoRows) => continue, - Err(err) => return Err(err).context("Failed to query package file ownership"), - }; - - if owner != "sbase" { - continue; - } - - tx.execute( - "DELETE FROM files WHERE path = ?1 AND package_id = (SELECT id FROM packages WHERE name = 'sbase')", - rusqlite::params![rel_path], - ) - .with_context(|| format!("Failed to clear sbase ownership for {rel_path}"))?; - - let disk_path = sysroot.join(rel_path); - if disk_path.exists() { - fs::remove_file(&disk_path) - .with_context(|| format!("Failed to remove {}", disk_path.display()))?; - } - ui::info(format!( - "Handed off {} from sbase to {}", - rel_path, package.name - )); - } - - tx.commit() - .with_context(|| format!("Failed to update package database {}", db_path.display()))?; - Ok(()) -} - -fn bootstrap_install_invocation( - sysroot: &Path, - recipe: &GeneratedRecipe, - mode: BootstrapBuildMode, - target: &str, - depot_exe: &Path, -) -> Result { - let mut args = vec![ - OsString::from("install"), - OsString::from("-r"), - sysroot.as_os_str().to_os_string(), - OsString::from("--yes"), - OsString::from("--no-deps"), - ]; - if mode == BootstrapBuildMode::Cross { - args.push(OsString::from("--cross-prefix")); - args.push(OsString::from(target)); - } - args.push(recipe.spec_path.as_os_str().to_os_string()); - - let mut env = Vec::new(); - if matches!(mode, BootstrapBuildMode::Cross | BootstrapBuildMode::Chroot) { - env.push(( - OsString::from("PATH"), - bootstrap_tool_path( - sysroot, - std::env::var_os("PATH").as_deref(), - mode == BootstrapBuildMode::Chroot, - )?, - )); - } - env.push(( - OsString::from("DEPOT_LBI_SYSROOT"), - bootstrap_sysroot_env_path(sysroot)?.into_os_string(), - )); - env.push(( - OsString::from("LWI_MAKE_FLAGS"), - OsString::from(bootstrap_parallel_makeflags()), - )); - env.push(( - OsString::from("LWI_MAKE_JOBS"), - OsString::from(bootstrap_parallel_make_jobs()), - )); - env.push(( - OsString::from(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS), - OsString::from("1"), - )); - if mode == BootstrapBuildMode::Chroot { - env.extend([ - (OsString::from("DEPOT_LBI_CHROOT"), OsString::from("1")), - ( - OsString::from("DEPOT_LBI_CHROOT_ROOT"), - sysroot.as_os_str().to_os_string(), - ), - ( - OsString::from("DEPOT_LBI_DEPOT_EXE"), - depot_exe.as_os_str().to_os_string(), - ), - ]); - } - - Ok(BootstrapInstallInvocation { args, env }) -} - -fn bootstrap_sysroot_env_path(sysroot: &Path) -> Result { - if sysroot.is_absolute() { - Ok(sysroot.to_path_buf()) - } else { - Ok(std::env::current_dir() - .context("Failed to resolve current directory for bootstrap sysroot")? - .join(sysroot)) - } -} - -fn bootstrap_tool_path( - sysroot: &Path, - inherited_path: Option<&OsStr>, - include_target_paths: bool, -) -> Result { - let mut paths = vec![ - sysroot.join("system/tools/bin"), - sysroot.join("system/tools/sbin"), - ]; - if include_target_paths { - paths.extend([ - sysroot.join("system/binaries"), - sysroot.join("system/systembinaries"), - ]); - } - if let Some(inherited) = inherited_path { - paths.extend(std::env::split_paths(inherited)); - } - std::env::join_paths(paths).context("Failed to construct bootstrap PATH") -} - -fn bootstrap_parallel_makeflags() -> String { - if let Ok(value) = std::env::var("LWI_MAKE_FLAGS") { - let trimmed = value.trim(); - if !trimmed.is_empty() { - return trimmed.to_string(); - } - } - - format!("-j{}", bootstrap_parallel_make_jobs()) -} - -fn bootstrap_parallel_make_jobs() -> String { - if let Ok(value) = std::env::var("LWI_MAKE_JOBS") { - let trimmed = value.trim(); - if !trimmed.is_empty() { - return trimmed.to_string(); - } - } - - std::thread::available_parallelism() - .map(|parallelism| parallelism.get()) - .unwrap_or(1) - .to_string() -} - -fn bootstrap_progress_root(config: &config::Config) -> PathBuf { - config.db_dir.join(BOOTSTRAP_DIR).join("progress") -} - -fn retired_package_progress_path(config: &config::Config, package: &str) -> PathBuf { - bootstrap_progress_root(config).join(format!("retired-{package}.done")) -} - -fn package_is_retired(config: &config::Config, package: &str) -> bool { - retired_package_progress_path(config, package).exists() -} - -fn step_is_done( - step: &BookStep, - recipes: &BTreeMap<&str, &GeneratedRecipe>, - progress_root: &Path, - sysroot: &Path, - config: &config::Config, -) -> bool { - match step { - BookStep::Package(package) => { - recipes - .get(package.recipe_id.as_str()) - .is_some_and(|recipe| { - bootstrap_package_is_complete(sysroot, config, recipe).unwrap_or(false) - }) - } - BookStep::Operation(operation) => { - operation_is_complete(progress_root, operation, sysroot, config).unwrap_or(false) - } - } -} - -fn step_chapter(step: &BookStep) -> Option { - section_chapter(step.section()) -} - -fn step_completes_chapter(step: &BookStep, next: Option<&BookStep>, chapter: u8) -> bool { - step_chapter(step) == Some(chapter) && next.and_then(step_chapter) != Some(chapter) -} - -fn step_requires_root(step: &BookStep) -> bool { - match step { - BookStep::Package(package) => build_mode_for_package(package) == BootstrapBuildMode::Chroot, - BookStep::Operation(_) => true, - } -} - -fn build_mode_for_package(package: &BookPackage) -> BootstrapBuildMode { - if use_cross_toolchain_by_default(package) { - return BootstrapBuildMode::Cross; - } - - match package.chapter { - 5 => BootstrapBuildMode::Host, - 6 => BootstrapBuildMode::Cross, - 7..=9 => BootstrapBuildMode::Chroot, - _ => BootstrapBuildMode::Host, - } -} - -fn stage_for_step(step: &BookStep) -> String { - match step { - BookStep::Package(package) => format!("bootstrap-{}", package.recipe_id), - BookStep::Operation(operation) => format!("bootstrap-{}", operation.recipe_id), - } -} - -fn operation_progress_path(progress_root: &Path, operation: &BookOperation) -> PathBuf { - progress_root.join(format!("{}.done", operation.recipe_id)) -} - -fn operation_is_complete( - progress_root: &Path, - operation: &BookOperation, - sysroot: &Path, - config: &config::Config, -) -> Result { - if !operation_progress_path(progress_root, operation).exists() { - return Ok(false); - } - - match operation.kind { - BookOperationKind::CreateEssentialSystemFiles => { - filesystem_package_is_registered(sysroot, config) - } - _ => Ok(true), - } -} - -fn retire_packages_after_chapter7(sysroot: &Path, config: &config::Config) -> Result<()> { - let db_path = config.installed_db_path(sysroot); - let installed = crate::db::get_installed_packages(&db_path).with_context(|| { - format!( - "Failed to inspect installed packages in {}", - db_path.display() - ) - })?; - let needs_removal = CHAPTER7_RETIRED_PACKAGES - .iter() - .any(|package| installed.contains(*package)); - - if needs_removal && !crate::fakeroot::is_root() { - ensure_root_for_bootstrap()?; - } - - for package in CHAPTER7_RETIRED_PACKAGES { - if installed.contains(*package) { - ui::info(format!( - "Retiring bootstrap-only package {package} after chapter 7" - )); - crate::commands::remove_installed_package_with_hooks(package, sysroot, config) - .with_context(|| format!("Failed to retire bootstrap-only package {package}"))?; - } - - let progress_path = retired_package_progress_path(config, package); - if let Some(parent) = progress_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; - } - fs::write( - &progress_path, - "reason = \"chapter7-complete\"\nretired = true\n", - ) - .with_context(|| format!("Failed to write {}", progress_path.display()))?; - } - - Ok(()) -} - -fn run_operation_step( - sysroot: &Path, - config: &config::Config, - operation: &BookOperation, - target: &str, - arch: &str, -) -> Result<()> { - let progress_path = operation_progress_path(&bootstrap_progress_root(config), operation); - if operation_is_complete(&bootstrap_progress_root(config), operation, sysroot, config)? { - ui::info(format!( - "Skipping already completed bootstrap step {} ({})", - operation.title, operation.section - )); - return Ok(()); - } - - ui::info(format!("Running bootstrap step: {}", operation.title)); - match operation.kind { - BookOperationKind::ResetTargetTreeOwnership => reset_target_tree_ownership(sysroot)?, - BookOperationKind::CreateVirtualFilesystemLinkTargets => { - create_virtual_filesystem_link_targets(sysroot)? - } - BookOperationKind::CopyBuildProfile => copy_build_profile(sysroot, target, arch)?, - BookOperationKind::EnterChroot => mark_chroot_entry(sysroot)?, - BookOperationKind::CreateEssentialSystemFiles => { - create_essential_system_files(sysroot)?; - register_filesystem_package(sysroot, config)?; - } - } - - if let Some(parent) = progress_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; - } - fs::write( - &progress_path, - format!( - "section = \"{}\"\noperation = \"{}\"\n", - operation.section, - operation_slug(operation.kind) - ), - ) - .with_context(|| format!("Failed to write {}", progress_path.display()))?; - Ok(()) -} - -fn reset_target_tree_ownership(sysroot: &Path) -> Result<()> { - let status = Command::new("chown") - .arg("-R") - .arg("0:0") - .arg(sysroot) - .status() - .with_context(|| format!("Failed to run chown for {}", sysroot.display()))?; - if !status.success() { - anyhow::bail!( - "Failed to reset target tree ownership for {}: chown exited with {}", - sysroot.display(), - status - ); - } - Ok(()) -} - -fn create_virtual_filesystem_link_targets(sysroot: &Path) -> Result<()> { - system_state::ensure_lbi_layout_paths(sysroot)?; - for rel in [ - "system/devices/pts", - "system/devices/shm", - "system/temporary", - ] { - let path = sysroot.join(rel); - fs::create_dir_all(&path) - .with_context(|| format!("Failed to create {}", path.display()))?; - } - Ok(()) -} - -fn copy_build_profile(sysroot: &Path, target: &str, arch: &str) -> Result<()> { - let profile_dir = sysroot.join("etc/profile.d"); - fs::create_dir_all(&profile_dir) - .with_context(|| format!("Failed to create {}", profile_dir.display()))?; - let profile_path = profile_dir.join("lbi-build.sh"); - let content = format!( - r#"# Generated by `depot bootstrap`. -export LBI_TARGET="{target}" -export LBI_ARCH="{arch}" -export CHOST="$LBI_TARGET" -export CARCH="$LBI_ARCH" -export LBI_ROOT="/" -export LBI_SOURCES="/sources" -export PATH="/system/tools/bin:/system/binaries:/system/systembinaries:/bin:/usr/bin:/sbin:/usr/sbin" - -lbi_configure() {{ - ./configure \ - --target="$LBI_TARGET" \ - --host="$LBI_TARGET" \ - --prefix=/system \ - --bindir=/system/binaries \ - --sbindir=/system/systembinaries \ - --libdir=/system/libraries \ - --includedir=/system/headers \ - --sysconfdir=/system/configuration \ - --localstatedir=/system/variable \ - --datarootdir=/system/share \ - --mandir=/system/documentation/man-pages \ - --infodir=/system/documentation/info \ - "$@" -}} -"# - ); - fs::write(&profile_path, content) - .with_context(|| format!("Failed to write {}", profile_path.display()))?; - Ok(()) -} - -fn mark_chroot_entry(sysroot: &Path) -> Result<()> { - let marker = sysroot.join("var/lib/depot/bootstrap-chroot-ready"); - if let Some(parent) = marker.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; - } - fs::write(&marker, "ready\n").with_context(|| format!("Failed to write {}", marker.display())) -} - -fn create_essential_system_files(sysroot: &Path) -> Result<()> { - write_if_missing( - &sysroot.join("etc/passwd"), - "root:x:0:0:root:/system/charlie:/bin/oksh\n\ -bin:x:1:1:bin:/dev/null:/system/binaries/false\n\ -daemon:x:6:6:Daemon User:/dev/null:/system/binaries/false\n\ -messagebus:x:18:18:D-Bus Message Daemon User:/run/dbus:/system/binaries/false\n\ -uuidd:x:80:80:UUID Generation Daemon User:/dev/null:/system/binaries/false\n\ -nobody:x:65534:65534:Unprivileged User:/dev/null:/system/binaries/false\n", - )?; - write_if_missing( - &sysroot.join("etc/group"), - "root:x:0:\n\ -bin:x:1:daemon\n\ -sys:x:2:\n\ -kmem:x:3:\n\ -tape:x:4:\n\ -tty:x:5:\n\ -daemon:x:6:\n\ -floppy:x:7:\n\ -disk:x:8:\n\ -lp:x:9:\n\ -dialout:x:10:\n\ -audio:x:11:\n\ -video:x:12:\n\ -utmp:x:13:\n\ -clock:x:14:\n\ -cdrom:x:15:\n\ -adm:x:16:\n\ -messagebus:x:18:\n\ -input:x:24:\n\ -mail:x:34:\n\ -kvm:x:61:\n\ -uuidd:x:80:\n\ -wheel:x:97:\n\ -users:x:999:\n\ -nogroup:x:65534:\n", - )?; - write_if_missing( - &sysroot.join("etc/hosts"), - "127.0.0.1 localhost\n::1 localhost\n", - )?; - write_if_missing( - &sysroot.join("etc/fstab"), - "# file system mount point type options dump pass\n", - )?; - write_if_missing( - &sysroot.join("etc/shells"), - "/bin/sh\n/system/binaries/sh\n", - )?; - Ok(()) -} - -fn filesystem_package_is_registered(sysroot: &Path, config: &config::Config) -> Result { - let installed = crate::db::get_installed_packages(&config.installed_db_path(sysroot))?; - Ok(installed.contains(FILESYSTEM_PACKAGE)) -} - -fn register_filesystem_package(sysroot: &Path, config: &config::Config) -> Result<()> { - ui::info("Registering system layout as package filesystem"); - system_state::ensure_lbi_layout_paths(sysroot)?; - create_essential_system_files(sysroot)?; - let staged = tempfile::tempdir().context("Failed to create filesystem package staging dir")?; - system_state::ensure_lbi_layout_paths(staged.path())?; - create_essential_system_files(staged.path())?; - crate::db::register_package( - &config.installed_db_path(sysroot), - &filesystem_package_spec(), - staged.path(), - ) - .context("Failed to register filesystem package") -} - -fn filesystem_package_spec() -> crate::package::PackageSpec { - crate::package::PackageSpec { - package: crate::package::PackageInfo { - name: FILESYSTEM_PACKAGE.to_string(), - real_name: None, - version: "1.0".to_string(), - revision: 1, - description: "Linux by Intent system layout".to_string(), - homepage: "https://www.vertexlinux.net/lbi/".to_string(), - abi_breaking: false, - license: vec!["MIT".to_string()], - }, - packages: Vec::new(), - alternatives: crate::package::Alternatives::default(), - manual_sources: Vec::new(), - source: Vec::new(), - build: crate::package::Build { - build_type: crate::package::BuildType::Meta, - flags: crate::package::BuildFlags::default(), - }, - dependencies: crate::package::Dependencies { - groups: vec![BASE_LAYER.to_string()], - ..crate::package::Dependencies::default() - }, - package_alternatives: BTreeMap::new(), - package_dependencies: BTreeMap::new(), - spec_dir: PathBuf::new(), - } -} - -fn write_if_missing(path: &Path, content: &str) -> Result<()> { - if path.exists() { - return Ok(()); - } - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; - } - fs::write(path, content).with_context(|| format!("Failed to write {}", path.display())) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum RootTransition { - AlreadyRoot, - Reexec(PathBuf), -} - -fn ensure_root_for_bootstrap() -> Result<()> { - match root_transition( - crate::fakeroot::is_root(), - std::env::var_os("PATH").as_deref(), - )? { - RootTransition::AlreadyRoot => Ok(()), - RootTransition::Reexec(helper) => { - let exe = std::env::current_exe().context("Failed to locate depot executable")?; - ui::info(format!( - "Re-executing bootstrap through {} for root-owned and chroot steps", - helper.display() - )); - let status = Command::new(&helper) - .arg(exe) - .args(std::env::args_os().skip(1)) - .status() - .with_context(|| { - format!( - "Failed to re-execute depot bootstrap via {}", - helper.display() - ) - })?; - if status.success() { - std::process::exit(0); - } - anyhow::bail!( - "depot bootstrap via {} failed with status {}", - helper.display(), - status - ); - } - } -} - -fn root_transition(is_root: bool, path: Option<&OsStr>) -> Result { - if is_root { - return Ok(RootTransition::AlreadyRoot); - } - let Some(helper) = privilege_helper(path) else { - anyhow::bail!( - "Bootstrap needs root for ownership and chroot steps, but neither sudo nor doas was found in PATH" - ); - }; - Ok(RootTransition::Reexec(helper)) -} - -fn privilege_helper(path: Option<&OsStr>) -> Option { - for name in ["sudo", "doas"] { - if let Some(found) = find_executable_in_path(name, path) { - return Some(found); - } - } - None -} - -fn find_executable_in_path(name: &str, path: Option<&OsStr>) -> Option { - let path = path?; - for dir in std::env::split_paths(path) { - let candidate = dir.join(name); - if !candidate.is_file() { - continue; - } - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let Ok(metadata) = fs::metadata(&candidate) else { - continue; - }; - if metadata.permissions().mode() & 0o111 == 0 { - continue; - } - } - return Some(candidate); - } - None -} - -#[derive(Default)] -struct BootstrapChrootMountGuard { - mounted: Vec, - created_files: Vec, -} - -impl BootstrapChrootMountGuard { - fn mount_path( - &mut self, - source: &Path, - target: &Path, - fstype: Option<&str>, - flags: MountFlags, - data: Option<&str>, - ) -> Result<()> { - let mut builder = Mount::builder().flags(flags); - if let Some(fstype) = fstype { - builder = builder.fstype(fstype); - } - if let Some(data) = data { - builder = builder.data(data); - } - let mount = builder - .mount(source, target) - .with_context(|| format!("Failed to mount {}", target.display()))?; - self.mounted.push(mount); - Ok(()) - } - - fn prepare_file_mount_target(&mut self, target: &Path) -> Result<()> { - if let Some(parent) = target.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create {}", parent.display()))?; - } - - match target.symlink_metadata() { - Ok(metadata) if metadata.file_type().is_dir() => { - anyhow::bail!("Mount target is a directory: {}", target.display()); - } - Ok(_) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - fs::File::create(target) - .with_context(|| format!("Failed to create {}", target.display()))?; - self.created_files.push(target.to_path_buf()); - Ok(()) - } - Err(err) => Err(err).with_context(|| format!("Failed to inspect {}", target.display())), - } - } - - fn mount_host_resolver_config(&mut self, rootfs: &Path) -> Result { - let source = Path::new("/etc/resolv.conf"); - if !source.is_file() { - return Ok(false); - } - - let target = rootfs.join("etc/resolv.conf"); - self.prepare_file_mount_target(&target)?; - self.mount_path(source, &target, None, MountFlags::BIND, None)?; - Ok(true) - } -} - -impl Drop for BootstrapChrootMountGuard { - fn drop(&mut self) { - for mount in self.mounted.iter().rev() { - if mount.unmount(UnmountFlags::empty()).is_ok() { - continue; - } - let _ = mount.unmount(UnmountFlags::DETACH); - } - for path in self.created_files.iter().rev() { - let _ = fs::remove_file(path); - } - } -} - -pub(crate) fn run_bootstrap_chroot( - rootfs: &Path, - sources: &Path, - destdir: &Path, - workdir: &str, - script: &str, -) -> Result<()> { - if !crate::fakeroot::is_root() { - anyhow::bail!("internal bootstrap-chroot requires root"); - } - if !sources.is_dir() { - anyhow::bail!( - "Bootstrap source workspace is not a directory: {}", - sources.display() - ); - } - fs::create_dir_all(destdir) - .with_context(|| format!("Failed to create {}", destdir.display()))?; - - let _mounts = mount_bootstrap_chroot(rootfs, sources, destdir)?; - install_bootstrap_chroot_tool_shims(rootfs)?; - let command = format!( - "cd {} && exec /bin/sh {}", - shell_quote(workdir), - shell_quote(script) - ); - let mut cmd = Command::new("chroot"); - cmd.arg(rootfs) - .arg("/bin/sh") - .arg("-lc") - .arg(command) - .env("DEPOT_LBI_INSIDE_CHROOT", "1") - .env("DESTDIR", "/destdir") - .env("DEPOT_PRIMARY_DESTDIR", "/destdir") - .env("DEPOT_LBI_SYSROOT", "/") - .env("DEPOT_STARBUILD_WORKDIR", "/sources") - .env("LBI_ROOT", "/destdir") - .env("LBI_SYSROOT", "/") - .env("LBI_SOURCES", "/sources") - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()); - apply_bootstrap_chroot_tool_env(&mut cmd); - let status = crate::interrupts::command_status(&mut cmd) - .with_context(|| format!("Failed to execute bootstrap build in {}", rootfs.display()))?; - if !status.success() { - anyhow::bail!("Bootstrap chroot build failed with status {}", status); - } - Ok(()) -} - -fn bootstrap_chroot_tool_env() -> &'static [(&'static str, &'static str)] { - &[ - ("CC", "cc"), - ("CXX", "c++"), - ("AR", "ar"), - ("RANLIB", "ranlib"), - ("NM", "nm"), - ("STRIP", "strip"), - ("PATH", BOOTSTRAP_CHROOT_PATH), - ] -} - -fn apply_bootstrap_chroot_tool_env(cmd: &mut Command) { - for (key, value) in bootstrap_chroot_tool_env() { - cmd.env(key, value); - } - cmd.env_remove("CROSS_COMPILE"); -} - -fn install_bootstrap_chroot_tool_shims(rootfs: &Path) -> Result<()> { - let shim_dir = rootfs.join(BOOTSTRAP_CHROOT_SHIM_DIR.trim_start_matches('/')); - fs::create_dir_all(&shim_dir) - .with_context(|| format!("Failed to create {}", shim_dir.display()))?; - - let id_path = shim_dir.join("id"); - fs::write( - &id_path, - r#"#!/bin/sh -case "${1:-}" in - "" ) - printf '%s\n' 'uid=0(root) gid=0(root) groups=0(root)' - ;; - -u ) - printf '%s\n' 0 - ;; - -g ) - printf '%s\n' 0 - ;; - -G ) - printf '%s\n' 0 - ;; - -un ) - printf '%s\n' root - ;; - -gn ) - printf '%s\n' root - ;; - * ) - printf '%s\n' "id: unsupported bootstrap option: $1" >&2 - exit 1 - ;; -esac -"#, - ) - .with_context(|| format!("Failed to write {}", id_path.display()))?; - make_executable(&id_path)?; - Ok(()) -} - -#[cfg(unix)] -fn make_executable(path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut perms = fs::metadata(path) - .with_context(|| format!("Failed to stat {}", path.display()))? - .permissions(); - perms.set_mode(0o755); - fs::set_permissions(path, perms).with_context(|| format!("Failed to chmod {}", path.display())) -} - -#[cfg(not(unix))] -fn make_executable(_path: &Path) -> Result<()> { - Ok(()) -} - -fn mount_bootstrap_chroot( - rootfs: &Path, - sources: &Path, - destdir: &Path, -) -> Result { - for rel in [ - "proc", "dev", "dev/pts", "sys", "run", "tmp", "sources", "destdir", - ] { - let path = rootfs.join(rel); - fs::create_dir_all(&path) - .with_context(|| format!("Failed to create {}", path.display()))?; - } - set_world_writable_sticky(&rootfs.join("tmp"))?; - - let mut guard = BootstrapChrootMountGuard::default(); - guard.mount_path( - Path::new("proc"), - &rootfs.join("proc"), - Some("proc"), - MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::NOSUID, - None, - )?; - guard.mount_path( - Path::new("/dev"), - &rootfs.join("dev"), - None, - MountFlags::BIND, - None, - )?; - guard.mount_path( - Path::new("sysfs"), - &rootfs.join("sys"), - Some("sysfs"), - MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::NOSUID, - None, - )?; - if let Err(_err) = guard.mount_path( - Path::new("devpts"), - &rootfs.join("dev/pts"), - Some("devpts"), - MountFlags::NOSUID | MountFlags::NOEXEC, - Some("gid=5,mode=620"), - ) { - guard.mount_path( - Path::new("devpts"), - &rootfs.join("dev/pts"), - Some("devpts"), - MountFlags::NOSUID | MountFlags::NOEXEC, - None, - )?; - } - guard.mount_path( - Path::new("/run"), - &rootfs.join("run"), - None, - MountFlags::BIND, - None, - )?; - guard.mount_path( - sources, - &rootfs.join("sources"), - None, - MountFlags::BIND, - None, - )?; - guard.mount_path( - destdir, - &rootfs.join("destdir"), - None, - MountFlags::BIND, - None, - )?; - if guard.mount_host_resolver_config(rootfs)? { - ui::info("Mounted host resolver configuration for bootstrap chroot"); - } - Ok(guard) -} - -#[cfg(unix)] -fn set_world_writable_sticky(path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut perms = fs::metadata(path) - .with_context(|| format!("Failed to stat {}", path.display()))? - .permissions(); - perms.set_mode(0o1777); - fs::set_permissions(path, perms) - .with_context(|| format!("Failed to set permissions on {}", path.display())) -} - -#[cfg(not(unix))] -fn set_world_writable_sticky(_path: &Path) -> Result<()> { - Ok(()) -} - -fn shell_quote(input: &str) -> String { - if input.is_empty() { - return "''".to_string(); - } - format!("'{}'", input.replace('\'', "'\\''")) -} - -fn load_book_pdf(args: &BootstrapArgs) -> Result> { - if let Some(path) = &args.book_pdf { - return fs::read(path).with_context(|| format!("Failed to read {}", path.display())); - } - - ui::info(format!("Fetching Linux by Intent book: {}", args.book_url)); - let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - let client = source::build_blocking_client(&ua, Some(Duration::from_secs(60)))?; - let fetch_url = fresh_book_fetch_url(&args.book_url)?; - let mut response = client - .get(fetch_url.as_str()) - .send() - .with_context(|| format!("Failed to fetch {}", args.book_url))?; - let status = response.status(); - if !status.is_success() { - anyhow::bail!("HTTP error fetching {}: {}", args.book_url, status); - } - let mut body = Vec::new(); - response - .copy_to(&mut body) - .with_context(|| format!("Failed to read {}", args.book_url))?; - Ok(body) -} - -fn generate_recipes( - args: &BootstrapArgs, - config: &config::Config, - packages: &[BookPackage], - target: &str, - arch: &str, -) -> Result> { - let root = config.db_dir.join(BOOTSTRAP_DIR); - let recipe_root = root.join("recipes"); - let page_root = root.join("pages"); - let progress_root = root.join("progress"); - fs::create_dir_all(&recipe_root) - .with_context(|| format!("Failed to create {}", recipe_root.display()))?; - fs::create_dir_all(&page_root) - .with_context(|| format!("Failed to create {}", page_root.display()))?; - fs::create_dir_all(&progress_root) - .with_context(|| format!("Failed to create {}", progress_root.display()))?; - - let manifest = load_source_manifest(args, &root)?; - let mut generated = Vec::new(); - for package in packages { - let (actual_page_url, html) = fetch_package_page(package)?; - let mut package = package.clone(); - package.page_url = actual_page_url; - - let page_path = page_root.join(format!("{}.html", package.recipe_id)); - fs::write(&page_path, &html) - .with_context(|| format!("Failed to write {}", page_path.display()))?; - - let page_recipe = parse_page_recipe(&html, &package)?; - let recipe_dir = recipe_root.join(&package.layer).join(&package.recipe_id); - fs::create_dir_all(&recipe_dir) - .with_context(|| format!("Failed to create {}", recipe_dir.display()))?; - let spec_path = recipe_dir.join(format!("{}.toml", package.name)); - let build_path = recipe_dir.join("build.sh"); - write_generated_recipe( - &spec_path, - &build_path, - &package, - &page_recipe, - &manifest, - target, - arch, - )?; - generated.push(GeneratedRecipe { - package: package.clone(), - spec_path, - progress_path: progress_root.join(format!("{}.done", package.recipe_id)), - }); - } - Ok(generated) -} - -fn load_source_manifest(args: &BootstrapArgs, root: &Path) -> Result { - let manifest_url = book_base_url(&args.book_url)?.join("scripts/sources.manifest")?; - let b2sums_url = book_base_url(&args.book_url)?.join("scripts/sources.b2sums")?; - let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - let client = source::build_blocking_client(&ua, Some(Duration::from_secs(60)))?; - let manifest_fetch_url = fresh_book_fetch_url(manifest_url.as_str())?; - let b2sums_fetch_url = fresh_book_fetch_url(b2sums_url.as_str())?; - ui::info(format!( - "Fetching Linux by Intent source manifest: {manifest_url}" - )); - let body = client - .get(manifest_fetch_url.as_str()) - .send() - .with_context(|| format!("Failed to fetch {manifest_url}"))? - .error_for_status() - .with_context(|| format!("HTTP error fetching {manifest_url}"))? - .text() - .with_context(|| format!("Failed to read {manifest_url}"))?; - let path = root.join("sources.manifest"); - fs::write(&path, &body).with_context(|| format!("Failed to write {}", path.display()))?; - ui::info(format!( - "Fetching Linux by Intent BLAKE2 source manifest: {b2sums_url}" - )); - let b2sums = client - .get(b2sums_fetch_url.as_str()) - .send() - .with_context(|| format!("Failed to fetch {b2sums_url}"))? - .error_for_status() - .with_context(|| format!("HTTP error fetching {b2sums_url}"))? - .text() - .with_context(|| format!("Failed to read {b2sums_url}"))?; - let b2sums_path = root.join("sources.b2sums"); - fs::write(&b2sums_path, &b2sums) - .with_context(|| format!("Failed to write {}", b2sums_path.display()))?; - Ok(SourceManifest { - entries: parse_source_manifest(&body), - blake2b_512: parse_source_b2sums(&b2sums)?, - }) -} - -fn fetch_page(url: &str) -> Result { - let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - let client = source::build_blocking_client(&ua, Some(Duration::from_secs(60)))?; - let fetch_url = fresh_book_fetch_url(url)?; - client - .get(fetch_url.as_str()) - .send() - .with_context(|| format!("Failed to fetch {url}"))? - .error_for_status() - .with_context(|| format!("HTTP error fetching {url}"))? - .text() - .with_context(|| format!("Failed to read {url}")) -} - -fn fresh_book_fetch_url(url: &str) -> Result { - let mut parsed = Url::parse(url).with_context(|| format!("Invalid book fetch URL: {url}"))?; - let stamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .context("System clock is before UNIX epoch")? - .as_secs() - .to_string(); - parsed - .query_pairs_mut() - .append_pair(BOOK_FETCH_CACHE_BUST_PARAM, &stamp); - Ok(parsed.to_string()) -} - -fn fetch_package_page(package: &BookPackage) -> Result<(String, String)> { - match fetch_page(&package.page_url) { - Ok(html) => Ok((package.page_url.clone(), html)), - Err(primary_err) => { - let Some(fallback_url) = fallback_package_page_url(package)? else { - return Err(primary_err); - }; - - ui::info(format!( - "Primary page fetch failed for {}; trying fallback {}", - package.page_url, fallback_url - )); - - fetch_page(&fallback_url) - .map(|html| (fallback_url, html)) - .with_context(|| { - format!( - "Failed to fetch package page for {}. Primary URL was {}; fallback URL was tried after primary error: {primary_err}", - package.name, - package.page_url - ) - }) - } - } -} - -fn fallback_package_page_url(package: &BookPackage) -> Result> { - if !package.page_url.ends_with("-stage2.html") { - return Ok(None); - } - - let mut url = Url::parse(&package.page_url) - .with_context(|| format!("Invalid package page URL: {}", package.page_url))?; - - let path = url.path().to_string(); - let fallback_path = path.replace( - &format!("{}-stage2.html", package.name), - &format!("{}.html", package.name), - ); - - if fallback_path == path { - return Ok(None); - } - - url.set_path(&fallback_path); - Ok(Some(url.to_string())) -} - -fn rewrite_make_flags(input: &str) -> String { - input - .lines() - .map(|line| { - let trimmed = line.trim_start(); - let indent_len = line.len() - trimmed.len(); - let indent = &line[..indent_len]; - - if trimmed == "make" { - format!("{indent}make ${{LWI_MAKE_FLAGS:-}}") - } else if let Some(rest) = trimmed.strip_prefix("make ") { - if rest.contains("LWI_MAKE_FLAGS") || rest.contains("MAKEFLAGS") { - line.to_string() - } else { - format!("{indent}make ${{LWI_MAKE_FLAGS:-}} {rest}") - } - } else { - line.to_string() - } - }) - .collect::>() - .join("\n") -} - -fn rewrite_parallel_job_counts(input: &str) -> String { - input - .replace("\"$(nproc)\"", "\"${LWI_MAKE_JOBS}\"") - .replace("'$(nproc)'", "\"${LWI_MAKE_JOBS}\"") - .replace("$(nproc)", "${LWI_MAKE_JOBS}") -} - -fn write_generated_recipe( - spec_path: &Path, - build_path: &Path, - package: &BookPackage, - recipe: &PageRecipe, - manifest: &SourceManifest, - target: &str, - arch: &str, -) -> Result<()> { - let archive_inputs = recipe - .input_files - .iter() - .filter(|file| is_archive_filename(file)) - .cloned() - .collect::>(); - - let primary_source = archive_inputs.first(); - let mut spec = String::new(); - spec.push_str("[package]\n"); - spec.push_str(&format!("name = \"{}\"\n", toml_escape(&package.name))); - spec.push_str(&format!( - "version = \"{}\"\n", - toml_escape(&package.version) - )); - spec.push_str("revision = 1\n"); - spec.push_str(&format!( - "description = \"{}\"\n", - toml_escape(&recipe.description) - )); - spec.push_str(&format!( - "homepage = \"{}\"\n", - toml_escape(&package.page_url) - )); - spec.push_str(&format!( - "license = \"{}\"\n\n", - toml_escape(&recipe.license) - )); - let provides = bootstrap_package_provides(package); - let replacements = bootstrap_package_replacements(package); - if !provides.is_empty() || !replacements.is_empty() { - spec.push_str("[alternatives]\n"); - write_toml_string_array(&mut spec, "provides", &provides); - write_toml_static_array(&mut spec, "replaces", replacements); - spec.push('\n'); - } - if !recipe.dependencies.is_empty() { - spec.push_str("[dependencies]\n"); - write_toml_string_array(&mut spec, "runtime", &recipe.dependencies); - spec.push('\n'); - } - spec.push_str("[build]\n"); - spec.push_str("type = \"custom\"\n\n"); - spec.push_str("[build.flags]\n"); - spec.push_str("skip_tests = true\n"); - spec.push_str("no_flags = true\n"); - if bootstrap_preserves_static_archives(package) { - spec.push_str("no_delete_static = true\n"); - } - spec.push_str("no_strip = true\n"); - spec.push_str( - "passthrough_env = [\"DEPOT_LBI_CHROOT\", \"DEPOT_LBI_CHROOT_ROOT\", \"DEPOT_LBI_DEPOT_EXE\", \"DEPOT_LBI_SYSROOT\", \"LBI_CCACHE\", \"LWI_MAKE_FLAGS\", \"LWI_MAKE_JOBS\"]\n\n", - ); - - if let Some(primary) = primary_source { - let source_url = source_url_for_input(primary, package, recipe, &manifest.entries)?; - let checksum = source_checksum_for_input(primary, &source_url, manifest); - spec.push_str("[[source]]\n"); - spec.push_str(&format!("url = \"{}\"\n", toml_escape(&source_url))); - spec.push_str(&format!("sha256 = \"{}\"\n", toml_escape(&checksum))); - spec.push_str(&format!( - "extract_dir = \"{}\"\n\n", - toml_escape( - recipe - .extract_dir - .as_deref() - .unwrap_or_else(|| strip_archive_suffix(primary)) - ) - )); - } - - for input in &recipe.input_files { - if Some(input) == primary_source || input.trim().is_empty() { - continue; - } - let source_url = source_url_for_input(input, package, recipe, &manifest.entries)?; - let checksum = source_checksum_for_input(input, &source_url, manifest); - spec.push_str("[[manual_sources]]\n"); - spec.push_str(&format!("url = \"{}\"\n", toml_escape(&source_url))); - spec.push_str(&format!("sha256 = \"{}\"\n", toml_escape(&checksum))); - spec.push_str(&format!("dest = \"{}\"\n\n", toml_escape(input))); - } - - let build_script = generated_build_script(package, recipe, target, arch); - fs::write(spec_path, spec) - .with_context(|| format!("Failed to write {}", spec_path.display()))?; - fs::write(build_path, build_script) - .with_context(|| format!("Failed to write {}", build_path.display()))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(build_path)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(build_path, perms)?; - } - Ok(()) -} - -fn bootstrap_preserves_static_archives(package: &BookPackage) -> bool { - matches!(package.name.as_str(), "musl" | "rustc") - || package.name.starts_with("llvm") - || package.name.starts_with("musl-") -} - -fn write_toml_string_array(spec: &mut String, key: &str, values: &[String]) { - if values.is_empty() { - return; - } - spec.push_str(key); - spec.push_str(" = ["); - for (idx, value) in values.iter().enumerate() { - if idx > 0 { - spec.push_str(", "); - } - spec.push_str(&format!("\"{}\"", toml_escape(value))); - } - spec.push_str("]\n"); -} - -fn write_toml_static_array(spec: &mut String, key: &str, values: &[&str]) { - if values.is_empty() { - return; - } - spec.push_str(key); - spec.push_str(" = ["); - for (idx, value) in values.iter().enumerate() { - if idx > 0 { - spec.push_str(", "); - } - spec.push_str(&format!("\"{}\"", toml_escape(value))); - } - spec.push_str("]\n"); -} - -fn bootstrap_package_provides(package: &BookPackage) -> Vec { - let provides: &[&str] = match package.name.as_str() { - "musl-libc-pass2" | "musl" => &["musl", "libc"], - "llvm-clang-pass1" => &["llvm", "clang", "lld", "cc", "c++"], - "llvm-runtimes" => &["libunwind", "libcxxabi", "libcxx"], - "llvm-clang-pass2" | "llvm" => &[ - "llvm", - "clang", - "lld", - "cc", - "c++", - "compiler-rt", - "libunwind", - "libcxxabi", - "libcxx", - ], - "byacc" => &["yacc"], - "dash" => &["sh"], - "libressl" => &["openssl", "libssl", "libcrypto"], - "make" => &["gmake"], - "oksh" => &["ksh"], - "om4" => &["m4"], - "pigz" => &["gzip", "gunzip", "zcat"], - "python" => &["python3", "pip"], - "rustc" => &["rust", "cargo"], - "samurai" => &["ninja", "samu"], - "zlib-ng" => &["zlib", "libz"], - _ => &[], - }; - provides - .iter() - .filter(|provided| **provided != package.name) - .map(|provided| (*provided).to_string()) - .collect() -} - -fn bootstrap_package_replacements(package: &BookPackage) -> &'static [&'static str] { - match package.name.as_str() { - "musl-libc-pass2" => &["musl-libc-headers"], - "musl" => &["musl-libc-pass2", "musl-libc-headers", "musl-libc"], - "llvm-clang-pass2" => &["llvm-runtimes"], - "llvm" => &["llvm-clang-pass2", "llvm-clang-pass1", "llvm-runtimes"], - _ => &[], - } -} - -fn generated_build_script( - package: &BookPackage, - recipe: &PageRecipe, - target: &str, - arch: &str, -) -> String { - let mut script = String::new(); - script.push_str("#!/bin/sh\nset -eu\n\n"); - script.push_str(&format!("# Generated from {}\n", package.page_url)); - script.push_str("export LBI_ROOT=\"${DESTDIR:?DESTDIR is required}\"\n"); - script.push_str("export LBI_SYSROOT=\"${DEPOT_LBI_SYSROOT:-$LBI_ROOT}\"\n"); - script.push_str(&format!( - "export LBI_TARGET=\"${{LBI_TARGET:-{target}}}\"\n" - )); - script.push_str(&format!("export LBI_ARCH=\"${{LBI_ARCH:-{arch}}}\"\n")); - script.push_str("export LBI_SOURCES=\"${DEPOT_STARBUILD_WORKDIR:-$PWD}\"\n"); - script.push_str( - r#"if [ -z "${LWI_MAKE_JOBS:-}" ]; then - jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 1)" - case "$jobs" in - ""|*[!0-9]*) jobs=1 ;; - esac - export LWI_MAKE_JOBS="$jobs" -else - export LWI_MAKE_JOBS -fi -if [ -z "${LWI_MAKE_FLAGS:-}" ]; then - if [ -n "${MAKEFLAGS:-}" ]; then - export LWI_MAKE_FLAGS="$MAKEFLAGS" - else - export LWI_MAKE_FLAGS="-j${LWI_MAKE_JOBS}" - fi -else - export LWI_MAKE_FLAGS -fi -"#, - ); - script.push_str("export LWI_CFLAGS=\"${LWI_CFLAGS:-${CFLAGS:-}}\"\n"); - script.push_str("export LWI_CXXFLAGS=\"${LWI_CXXFLAGS:-$LWI_CFLAGS}\"\n"); - script.push_str("export LBI_CUSTOM_LDFLAGS=\"${LBI_CUSTOM_LDFLAGS:-${LDFLAGS:-}}\"\n\n"); - if uses_llvm_cmake_ccache(package) { - script.push_str( - r#"if [ -z "${LBI_CCACHE:-}" ]; then - LBI_CCACHE="$(command -v ccache)" || { - echo "depot: LLVM bootstrap requires ccache in PATH for the CMake compiler launcher" >&2 - exit 1 - } -fi -export LBI_CCACHE - -"#, - ); - } - let uses_cross_toolchain = use_cross_toolchain_by_default(package); - if uses_cross_toolchain { - script.push_str( - r#"lbi_find_cross_tool() { - for name in "$@"; do - for dir in "$LBI_SYSROOT/system/tools/bin"; do - if [ -x "$dir/$LBI_TARGET-$name" ]; then - printf '%s\n' "$dir/$LBI_TARGET-$name" - return 0 - fi - done - if command -v "$LBI_TARGET-$name" >/dev/null 2>&1; then - command -v "$LBI_TARGET-$name" - return 0 - fi - done - for name in "$@"; do - for dir in "$LBI_SYSROOT/system/tools/bin"; do - if [ -x "$dir/$name" ]; then - printf '%s\n' "$dir/$name" - return 0 - fi - done - if command -v "$name" >/dev/null 2>&1; then - command -v "$name" - return 0 - fi - done - echo "depot: could not find cross tool for $LBI_TARGET: $*" >&2 - return 1 -} - -export PATH="$LBI_SYSROOT/system/tools/bin:$PATH" -export CC="${CC:-$(lbi_find_cross_tool clang)}" -export CXX="${CXX:-$(lbi_find_cross_tool clang++)}" -export AR="${AR:-$(lbi_find_cross_tool ar llvm-ar)}" -export AS="${AS:-$CC}" -export NM="${NM:-$(lbi_find_cross_tool nm llvm-nm)}" -export RANLIB="${RANLIB:-$(lbi_find_cross_tool ranlib llvm-ranlib llvm-ar)}" -export OBJCOPY="${OBJCOPY:-$(lbi_find_cross_tool objcopy llvm-objcopy)}" -export OBJDUMP="${OBJDUMP:-$(lbi_find_cross_tool objdump llvm-objdump)}" -export STRIP="${STRIP:-$(lbi_find_cross_tool strip llvm-strip llvm-objcopy)}" -export LD="${LD:-$(lbi_find_cross_tool ld ld.lld lld)}" - -"#, - ); - } - if package.name == "musl-libc-pass2" { - script.push_str( - r#"if [ -z "${LIBCC:-}" ]; then - case "$LBI_ARCH" in - x86_64|amd64) compiler_rt_arch=x86_64 ;; - i?86) compiler_rt_arch=i386 ;; - aarch64|arm64) compiler_rt_arch=aarch64 ;; - *) compiler_rt_arch=$LBI_ARCH ;; - esac - - LIBCC=$( - for dir in \ - "$LBI_SYSROOT/system/tools/lib/clang" \ - "$LBI_SYSROOT/system/tools/lib" \ - "$LBI_SYSROOT/system/libraries/clang" \ - "$LBI_SYSROOT/system/lib/clang" - do - if [ -d "$dir" ]; then - find "$dir" -type f -name "libclang_rt.builtins-${compiler_rt_arch}.a" 2>/dev/null - fi - done | head -n1 - ) - - if [ -z "$LIBCC" ]; then - candidate=$("$CC" -print-file-name="libclang_rt.builtins-${compiler_rt_arch}.a" 2>/dev/null || true) - if [ -n "$candidate" ] && [ "$candidate" != "libclang_rt.builtins-${compiler_rt_arch}.a" ]; then - LIBCC=$candidate - fi - fi - - if [ -z "$LIBCC" ]; then - candidate=$("$CC" -print-libgcc-file-name 2>/dev/null || true) - if [ -n "$candidate" ] && [ "$candidate" != "libgcc.a" ]; then - LIBCC=$candidate - fi - fi - - if [ -z "$LIBCC" ]; then - echo "depot: musl pass2 requires compiler-rt builtins for $compiler_rt_arch before building libc.so" >&2 - exit 1 - fi - - export LIBCC -fi - -"#, - ); - } - let chroot_workdir = recipe - .extract_dir - .as_deref() - .map(|dir| format!("/sources/{dir}")) - .unwrap_or_else(|| "/sources".to_string()); - script.push_str(&format!( - "export DEPOT_LBI_CHROOT_WORKDIR=\"${{DEPOT_LBI_CHROOT_WORKDIR:-{}}}\"\n", - shell_double_quote_literal(&chroot_workdir) - )); - script.push_str( - "export DEPOT_LBI_CHROOT_SCRIPT=\"${DEPOT_LBI_CHROOT_SCRIPT:-$DEPOT_LBI_CHROOT_WORKDIR/build.sh}\"\n", - ); - script.push_str( - r#"if [ "${DEPOT_LBI_CHROOT:-0}" = "1" ] && [ "${DEPOT_LBI_INSIDE_CHROOT:-0}" != "1" ]; then - : "${DEPOT_LBI_CHROOT_ROOT:?DEPOT_LBI_CHROOT_ROOT is required}" - : "${DEPOT_LBI_DEPOT_EXE:?DEPOT_LBI_DEPOT_EXE is required}" - exec "$DEPOT_LBI_DEPOT_EXE" internal bootstrap-chroot \ - --rootfs "$DEPOT_LBI_CHROOT_ROOT" \ - --sources "$LBI_SOURCES" \ - --destdir "$DESTDIR" \ - --workdir "$DEPOT_LBI_CHROOT_WORKDIR" \ - --script "$DEPOT_LBI_CHROOT_SCRIPT" -fi - -"#, - ); - script.push_str( - r#"lbi_configure() { - ./configure \ - --target="$LBI_TARGET" \ - --host="$LBI_TARGET" \ - --prefix=/system \ - --bindir=/system/binaries \ - --sbindir=/system/systembinaries \ - --libdir=/system/libraries \ - --includedir=/system/headers \ - --sysconfdir=/system/configuration \ - --localstatedir=/system/variable \ - --datarootdir=/system/share \ - --mandir=/system/documentation/man-pages \ - --infodir=/system/documentation/info \ - "$@" -} - -lbi_cmake() { - build_dir="$1" - shift - cmake -S . -B "$build_dir" -G Ninja \ - -DCMAKE_SYSTEM_NAME=Linux \ - -DCMAKE_SYSTEM_PROCESSOR="$LBI_ARCH" \ - -DCMAKE_SYSROOT="$LBI_SYSROOT" \ - -DCMAKE_C_COMPILER_TARGET="$LBI_TARGET" \ - -DCMAKE_CXX_COMPILER_TARGET="$LBI_TARGET" \ - -DCMAKE_ASM_COMPILER_TARGET="$LBI_TARGET" \ - -DCMAKE_INSTALL_PREFIX=/system \ - -DCMAKE_INSTALL_BINDIR=/system/binaries \ - -DCMAKE_INSTALL_SBINDIR=/system/systembinaries \ - -DCMAKE_INSTALL_LIBDIR=/system/libraries \ - -DCMAKE_INSTALL_INCLUDEDIR=/system/headers \ - -DCMAKE_INSTALL_SYSCONFDIR=/system/configuration \ - -DCMAKE_INSTALL_LOCALSTATEDIR=/system/variable \ - -DCMAKE_INSTALL_DATAROOTDIR=/system/share \ - -DCMAKE_INSTALL_MANDIR=/system/documentation/man-pages \ - -DCMAKE_INSTALL_INFODIR=/system/documentation/info \ - "$@" -} - -lbi_meson() { - build_dir=build - use_lbi_cross_file=1 - - case "${1-}" in - "") ;; - -*) ;; - *) - build_dir=$1 - shift - ;; - esac - - for arg in "$@"; do - case "$arg" in - --cross-file|--cross-file=*) use_lbi_cross_file=0 ;; - esac - done - - case "$LBI_ARCH" in - amd64) lbi_meson_cpu_family=x86_64 ;; - i?86) lbi_meson_cpu_family=x86 ;; - arm64) lbi_meson_cpu_family=aarch64 ;; - *) lbi_meson_cpu_family=$LBI_ARCH ;; - esac - - if [ "$use_lbi_cross_file" = "1" ]; then - lbi_meson_cross_file="$PWD/.lbi-meson-cross.ini" - cat > "$lbi_meson_cross_file" <= 7 { - rewritten = restore_chroot_compiler_search_paths(&rewritten); - } - if package.name == "rustc" { - rewritten = restore_rust_bootstrap_runtime_tool_paths(&rewritten); - } - if is_clang_driver_config_command(&rewritten) { - rewritten = restore_clang_driver_config_runtime_paths(&rewritten); - } - if !rewritten.trim().is_empty() { - script.push_str(&rewritten); - script.push_str("\n\n"); - } - } - script -} - -fn use_cross_toolchain_by_default(package: &BookPackage) -> bool { - if !(5..=6).contains(&package.chapter) || package.name == "llvm-clang-pass1" { - return false; - } - if package.chapter == 6 { - return true; - } - section_at_or_after(&package.section, "5.4") -} - -fn section_at_or_after(section: &str, threshold: &str) -> bool { - let left = parse_section_numbers(section); - let right = parse_section_numbers(threshold); - if left.is_empty() || right.is_empty() { - return false; - } - let max_len = left.len().max(right.len()); - for idx in 0..max_len { - let l = *left.get(idx).unwrap_or(&0); - let r = *right.get(idx).unwrap_or(&0); - match l.cmp(&r) { - std::cmp::Ordering::Greater => return true, - std::cmp::Ordering::Less => return false, - std::cmp::Ordering::Equal => {} - } - } - true -} - -fn parse_section_numbers(section: &str) -> Vec { - section - .split('.') - .map(str::trim) - .filter(|part| !part.is_empty()) - .filter_map(|part| part.parse::().ok()) - .collect() -} - -fn apply_bootstrap_package_overrides(package: &BookPackage, command: &str) -> String { - if package.name == "llvm-clang-pass1" { - let command = ensure_llvm_pass1_unsets_destdir(command); - let command = ensure_llvm_cmake_uses_ccache(&command); - let command = ensure_llvm_pass1_builtins_only(&command); - let command = ensure_llvm_pass1_default_target_only(&command); - let command = ensure_llvm_pass1_builtins_sysroot(&command); - let command = ensure_llvm_pass1_default_sysroot(&command); - ensure_llvm_pass1_fresh_build_dir(&command) - } else if package.name == "llvm-runtimes" { - let command = ensure_llvm_runtime_links_skip_missing_crt(command); - ensure_llvm_runtimes_builds_compiler_rt_crt(&command) - } else if package.name == "llvm-clang-pass2" { - let command = ensure_llvm_cmake_uses_ccache(command); - let command = ensure_llvm_runtime_links_skip_missing_crt(&command); - let command = ensure_llvm_pass2_uses_pass1_native_tools(&command); - ensure_llvm_pass2_resource_layout(&command) - } else if matches!(package.name.as_str(), "musl-libc-pass2" | "musl") { - ensure_musl_loader_paths(command) - } else if package.name == "ncurses" { - let command = if package.chapter == 6 { - ensure_ncurses_host_tic_uses_build_toolchain(command) - } else { - command.to_string() - }; - ensure_ncurses_progs_avoid_private_safe_fopen(&command) - } else if package.chapter == 6 && package.name == "file" { - ensure_file_uses_host_magic_compiler(command) - } else if package.name == "gettext-tiny" { - ensure_gettext_tiny_uses_musl_libintl_mode(command) - } else if package.name == "shadow" { - ensure_shadow_uses_staged_account_tools(command) - } else if package.name == "bsddiffutils" { - ensure_bsddiffutils_uses_lbi_header_layout(command) - } else if package.name == "mandoc" { - ensure_mandoc_postinstall_uses_staged_man(command) - } else if package.name == "bmake" { - ensure_bmake_install_avoids_double_destdir(command) - } else if package.name == "byacc" { - ensure_byacc_exposes_unprefixed_yacc(command) - } else if package.name == "ubase" { - ensure_ubase_moves_bin_payload(command) - } else if package.name == "sqlite" { - ensure_sqlite_uses_supported_autosetup_dirs(command) - } else { - command.to_string() - } -} - -fn ensure_ubase_moves_bin_payload(command: &str) -> String { - if !command_runs_install_step(command) { - return command.to_string(); - } - - format!( - "{command}\n{}", - r#"if [ -d "$DESTDIR/system/bin" ]; then - mkdir -p "$DESTDIR/system/binaries" - set -- "$DESTDIR/system/bin"/* - if [ -e "$1" ] || [ -L "$1" ]; then - for path do - mv "$path" "$DESTDIR/system/binaries/" - done - fi - rmdir "$DESTDIR/system/bin" 2>/dev/null || true -fi"# - ) -} - -fn ensure_byacc_exposes_unprefixed_yacc(command: &str) -> String { - if !command_runs_install_step(command) { - return command.to_string(); - } - - format!( - "{command}\n{}", - r#"if [ ! -e "$DESTDIR/system/binaries/yacc" ] && [ -e "$DESTDIR/system/binaries/$LBI_TARGET-yacc" ]; then - ln -sf "$LBI_TARGET-yacc" "$DESTDIR/system/binaries/yacc" -fi -if [ ! -e "$DESTDIR/system/documentation/man-pages/man1/yacc.1" ] \ - && [ -e "$DESTDIR/system/documentation/man-pages/man1/$LBI_TARGET-yacc.1" ]; then - ln -sf "$LBI_TARGET-yacc.1" "$DESTDIR/system/documentation/man-pages/man1/yacc.1" -fi"# - ) -} - -fn command_runs_install_step(command: &str) -> bool { - command.lines().any(|line| { - let trimmed = line.trim_start(); - !trimmed.starts_with('#') - && (trimmed == "install" - || trimmed.starts_with("install ") - || trimmed.contains(" install") - || trimmed.contains(" install ")) - }) -} - -fn ensure_gettext_tiny_uses_musl_libintl_mode(command: &str) -> String { - command.replace("LIBINTL=musl", "LIBINTL=MUSL") -} - -fn ensure_shadow_uses_staged_account_tools(command: &str) -> String { - command.replace( - "pwconv\ngrpconv\nmkdir -p /etc/default\nuseradd -D --gid 999\npasswd root", - "\"$DESTDIR/system/systembinaries/pwconv\" -R /\nif ! grep -q '^[^:]*:[^:]*:999:' /etc/group; then\n printf '%s\\n' 'users:x:999:' >> /etc/group\nfi\n\"$DESTDIR/system/systembinaries/grpconv\" -R /\nmkdir -p /etc/default\n\"$DESTDIR/system/systembinaries/useradd\" -D -R / --gid 999\n\"$DESTDIR/system/binaries/passwd\" -R / -d root", - ) -} - -fn ensure_bsddiffutils_uses_lbi_header_layout(command: &str) -> String { - command - .replace( - r#"sed -i \ - 's|char\\s*\\*splice(char \\*, char \\*);|char\t*diff_splice(char *, char *);|' \ - src/diff/diff.h"#, - r#"sed -i \ - 's|char[[:space:]]*\*splice(char \*, char \*);|char *diff_splice(char *, char *);|' \ - src/diff/diff.h"#, - ) - .replace( - r#"sed -i \ - 's/\bsplice(/diff_splice(/g' \ - src/diff/diff.c src/diff/diffreg.c"#, - r#"sed -i \ - -e 's|= splice(|= diff_splice(|g' \ - -e 's|^splice(char \*dir, char \*file)|diff_splice(char *dir, char *file)|' \ - src/diff/diff.c src/diff/diffreg.c"#, - ) - .replace( - r#"sed -i \ - 's/u_char ch, \\*p1, \\*p2;/unsigned char ch, *p1, *p2;/' \ - src/cmp/regular.c"#, - r#"sed -i \ - -e 's|u_char ch, \*p1, \*p2;|unsigned char ch, *p1, *p2;|' \ - src/cmp/regular.c"#, - ) - .replace( - "CPPFLAGS=\"--target=$LBI_TARGET --sysroot=$LBI_ROOT -include ../../include/sys/cdefs.h\"", - "CPPFLAGS=\"--target=$LBI_TARGET --sysroot=$LBI_SYSROOT -I../../include -isystem $LBI_SYSROOT/system/headers -Wno-#warnings -include sys/cdefs.h\"", - ) - .replace( - "LDFLAGS=\"--target=$LBI_TARGET --sysroot=$LBI_ROOT $LBI_CUSTOM_LDFLAGS\"", - "LDFLAGS=\"--target=$LBI_TARGET --sysroot=$LBI_SYSROOT -L$LBI_SYSROOT/system/libraries $LBI_CUSTOM_LDFLAGS\"", - ) - .replace( - r#"sed -i '' \ - '/#include /a char *fgetln(FILE *, size_t *);' \ - diff/diff.c"#, - r##"awk ' - { print } - $0 == "#include " { - print "char *fgetln(FILE *, size_t *);" - } -' diff/diff.c > diff/diff.c.new -mv diff/diff.c.new diff/diff.c"##, - ) - .replace( - r#"sed -i \ - '/#include /a char *fgetln(FILE *, size_t *);' \ - diff/diff.c"#, - r##"awk ' - { print } - $0 == "#include " { - print "char *fgetln(FILE *, size_t *);" - } -' diff/diff.c > diff/diff.c.new -mv diff/diff.c.new diff/diff.c"##, - ) - .replace( - r#"sed -i '' \ - '/#include /a char *fgetln(FILE *, size_t *);' \ - diff3/diff3prog.c"#, - r##"awk ' - { print } - $0 == "#include " { - print "char *fgetln(FILE *, size_t *);" - } -' diff3/diff3prog.c > diff3/diff3prog.c.new -mv diff3/diff3prog.c.new diff3/diff3prog.c"##, - ) - .replace( - r#"sed -i \ - '/#include /a char *fgetln(FILE *, size_t *);' \ - diff3/diff3prog.c"#, - r##"awk ' - { print } - $0 == "#include " { - print "char *fgetln(FILE *, size_t *);" - } -' diff3/diff3prog.c > diff3/diff3prog.c.new -mv diff3/diff3prog.c.new diff3/diff3prog.c"##, - ) - .replace( - r#"sed -i '' \ - '/include_directories: \[sysdefs\],/a \ link_with: [libcompat],' \ - diff3/meson.build"#, - r#"awk ' - { print } - index($0, "include_directories: [sysdefs],") { - print " link_with: [libcompat]," - } -' diff3/meson.build > diff3/meson.build.new -mv diff3/meson.build.new diff3/meson.build"#, - ) - .replace( - r#"sed -i \ - '/include_directories: \[sysdefs\],/a \ link_with: [libcompat],' \ - diff3/meson.build"#, - r#"awk ' - { print } - index($0, "include_directories: [sysdefs],") { - print " link_with: [libcompat]," - } -' diff3/meson.build > diff3/meson.build.new -mv diff3/meson.build.new diff3/meson.build"#, - ) -} - -fn ensure_ncurses_host_tic_uses_build_toolchain(command: &str) -> String { - if !command.contains("make -C progs tic") { - return command.to_string(); - } - - r#"rm -rf build obj obj_s obj_g obj_x lib -mkdir -pv build -( - cd build - env \ - CC="${BUILD_CC:-cc}" \ - CXX="${BUILD_CXX:-c++}" \ - CPP="${BUILD_CPP:-cpp}" \ - AR="${BUILD_AR:-ar}" \ - AS="${BUILD_AS:-as}" \ - LD="${BUILD_LD:-ld}" \ - NM="${BUILD_NM:-nm}" \ - RANLIB="${BUILD_RANLIB:-ranlib}" \ - STRIP="${BUILD_STRIP:-strip}" \ - ../configure --prefix="$LBI_SYSROOT/system/tools" AWK=gawk - make -C include - make -C progs tic - install -vm755 progs/tic "$LBI_SYSROOT/system/tools/bin/tic" -) -rm -rf obj obj_s obj_g obj_x lib"# - .to_string() -} - -fn ensure_ncurses_progs_avoid_private_safe_fopen(command: &str) -> String { - if !command.contains("lbi_configure") { - return command.to_string(); - } - - let mut rewritten = command.to_string(); - if !rewritten.contains("--enable-root-access") { - rewritten = rewritten.replace( - " --with-shared \\", - " --with-shared \\\n --enable-root-access \\", - ); - } - if !rewritten.contains("USE_ROOT_ACCESS") { - rewritten = - format!("export CPPFLAGS=\"${{CPPFLAGS:+$CPPFLAGS }}-DUSE_ROOT_ACCESS\"\n{rewritten}"); - } - rewritten -} - -fn ensure_sqlite_uses_supported_autosetup_dirs(command: &str) -> String { - command.replace( - "lbi_configure \\", - "./configure \\\n --prefix=/system \\\n --bindir=/system/binaries \\\n --libdir=/system/libraries \\\n --includedir=/system/headers \\\n --mandir=/system/documentation/man-pages \\", - ) -} - -fn ensure_bmake_install_avoids_double_destdir(command: &str) -> String { - command.replace( - "MAKESYSPATH=mk \\\n./bmake -f Makefile install \\", - "DESTDIR= \\\nMAKESYSPATH=mk \\\n./bmake -f Makefile install \\", - ) -} - -fn ensure_mandoc_postinstall_uses_staged_man(command: &str) -> String { - command.replace( - "MANPAGER=cat man mandoc >/dev/null", - "MANPATH=\"$DESTDIR/system/documentation/man-pages\" MANPAGER=cat \"$DESTDIR/system/binaries/man\" mandoc >/dev/null", - ) -} - -fn ensure_file_uses_host_magic_compiler(command: &str) -> String { - if command.contains("lbi_configure") && command.contains("--host=\"$LBI_TARGET\"") { - return format!( - r#"rm -rf build-host-file -mkdir -p build-host-file -( - cd build-host-file - env \ - CC="${{BUILD_CC:-cc}}" \ - CXX="${{BUILD_CXX:-c++}}" \ - CPP="${{BUILD_CPP:-cpp}}" \ - AR="${{BUILD_AR:-ar}}" \ - RANLIB="${{BUILD_RANLIB:-ranlib}}" \ - ../configure --prefix="$PWD/host-tools" --disable-shared --enable-static --disable-libseccomp - make ${{LWI_MAKE_FLAGS:-}} -C src file -) - -{command}"# - ); - } - - if command.trim() == "make $LWI_MAKE_FLAGS" { - "make $LWI_MAKE_FLAGS FILE_COMPILE=\"$PWD/build-host-file/src/file\"".to_string() - } else { - command.to_string() - } -} - -fn ensure_llvm_pass1_fresh_build_dir(command: &str) -> String { - if command.contains("rm -rf build-llvm") { - return command.to_string(); - } - command.replace( - "mkdir -p build-llvm\ncd build-llvm", - "rm -rf build-llvm\nmkdir -p build-llvm\ncd build-llvm", - ) -} - -fn ensure_llvm_pass1_unsets_destdir(command: &str) -> String { - if command.contains("cmake -G Ninja \"../llvm\"") - && command.contains("-DCMAKE_INSTALL_PREFIX=$LBI_ROOT/system/tools") - && !command.contains("unset DESTDIR") - { - format!("unset DESTDIR\n{command}") - } else { - command.to_string() - } -} - -fn ensure_llvm_pass1_builtins_only(command: &str) -> String { - if !command.contains("-DLLVM_ENABLE_RUNTIMES=\"compiler-rt\"") { - return command.to_string(); - } - - let missing_flags = [ - "-DCOMPILER_RT_BUILD_CRT=OFF", - "-DCOMPILER_RT_BUILD_MEMPROF=OFF", - "-DCOMPILER_RT_BUILD_ORC=OFF", - "-DCOMPILER_RT_BUILD_CTX_PROFILE=OFF", - "-DCOMPILER_RT_INCLUDE_TESTS=OFF", - ] - .into_iter() - .filter(|flag| !command.contains(flag)) - .map(|flag| format!(" {flag} \\")) - .collect::>(); - - if missing_flags.is_empty() { - return command.to_string(); - } - - let block = missing_flags.join("\n"); - let profile_anchor = " -DCOMPILER_RT_BUILD_PROFILE=OFF \\"; - if command.contains(profile_anchor) { - return command.replace(profile_anchor, &format!("{profile_anchor}\n{block}")); - } - - let builtins_anchor = " -DCOMPILER_RT_BUILD_BUILTINS=ON \\"; - command.replace(builtins_anchor, &format!("{builtins_anchor}\n{block}")) -} - -fn ensure_llvm_pass1_default_target_only(command: &str) -> String { - if !command.contains("-DLLVM_ENABLE_RUNTIMES=\"compiler-rt\"") - || command.contains("-DCOMPILER_RT_DEFAULT_TARGET_ONLY=") - { - return command.to_string(); - } - - command.replace( - " -DCOMPILER_RT_BUILD_BUILTINS=ON \\", - " -DCOMPILER_RT_BUILD_BUILTINS=ON \\\n -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \\", - ) -} - -fn ensure_llvm_pass1_builtins_sysroot(command: &str) -> String { - if !command.contains("-DLLVM_ENABLE_RUNTIMES=\"compiler-rt\"") - || command.contains("-DBUILTINS_CMAKE_ARGS=") - { - return command.to_string(); - } - - command.replace( - " -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \\", - " -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \\\n -DBUILTINS_CMAKE_ARGS=\"-DCMAKE_C_FLAGS=--sysroot=$LBI_SYSROOT;-DCMAKE_ASM_FLAGS=--sysroot=$LBI_SYSROOT\" \\", - ) -} - -fn ensure_llvm_pass1_default_sysroot(command: &str) -> String { - if !command.contains("-DLLVM_ENABLE_RUNTIMES=\"compiler-rt\"") { - return command.to_string(); - } - - command.replace( - "-DDEFAULT_SYSROOT=$LBI_ROOT", - "-DDEFAULT_SYSROOT=$LBI_SYSROOT", - ) -} - -fn ensure_musl_loader_paths(command: &str) -> String { - let command = command.replace( - "$LBI_ROOT/usr/lib/ld-musl-${LBI_ARCH}.so.1", - "$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1", - ); - - command.replace( - "ln -snf ./libc.so \\\n \"$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1\"", - "rm -f \"$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1\"\nln -sf ./libc.so \\\n \"$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1\"\nrm -f \"$LBI_ROOT/lib/ld-musl-${LBI_ARCH}.so.1\"\nrmdir \"$LBI_ROOT/lib\" 2>/dev/null || true", - ) -} - -fn ensure_llvm_runtime_links_skip_missing_crt(command: &str) -> String { - if !command.contains("-DLLVM_ENABLE_RUNTIMES=\"libunwind;libcxxabi;libcxx\"") { - return command.to_string(); - } - - let command = ensure_cmake_linker_flag(command, "CMAKE_SHARED_LINKER_FLAGS", "-nostartfiles"); - ensure_cmake_linker_flag(&command, "CMAKE_MODULE_LINKER_FLAGS", "-nostartfiles") -} - -fn ensure_llvm_cmake_uses_ccache(command: &str) -> String { - let command = command - .replace(" -DLLVM_CCACHE_BUILD=ON \\\n", "") - .replace(" -DLLVM_CCACHE_BUILD=ON \\", "") - .replace("-DLLVM_CCACHE_BUILD=ON", ""); - - if command.contains("-DCMAKE_C_COMPILER_LAUNCHER=") - && command.contains("-DCMAKE_CXX_COMPILER_LAUNCHER=") - && command.contains("-DCMAKE_ASM_COMPILER_LAUNCHER=") - { - return command; - } - - let launcher_flags = concat!( - " -DCMAKE_C_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\\n", - " -DCMAKE_CXX_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\\n", - " -DCMAKE_ASM_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\" - ); - - if command.contains("lbi_cmake build-llvm") { - return command.replace( - "lbi_cmake build-llvm \\", - &format!("lbi_cmake build-llvm \\\n{launcher_flags}"), - ); - } - - if command.contains("cmake -G Ninja \"../llvm\"") { - return command.replace( - "cmake -G Ninja \"../llvm\" \\", - &format!("cmake -G Ninja \"../llvm\" \\\n{launcher_flags}"), - ); - } - - command -} - -fn uses_llvm_cmake_ccache(package: &BookPackage) -> bool { - matches!( - package.name.as_str(), - "llvm-clang-pass1" | "llvm-clang-pass2" - ) -} - -fn ensure_llvm_pass2_uses_pass1_native_tools(command: &str) -> String { - let command = command.replace( - "-DLLVM_NATIVE_TOOL_DIR=\"$LBI_ROOT/system/tools/bin\"", - "-DLLVM_NATIVE_TOOL_DIR=\"$LBI_SYSROOT/system/tools/bin\"", - ); - if command.contains("-DLLVM_CONFIG_PATH=") { - return command; - } - - command.replace( - "-DLLVM_NATIVE_TOOL_DIR=\"$LBI_SYSROOT/system/tools/bin\" \\", - "-DLLVM_NATIVE_TOOL_DIR=\"$LBI_SYSROOT/system/tools/bin\" \\\n -DLLVM_CONFIG_PATH=\"$LBI_SYSROOT/system/tools/bin/llvm-config\" \\", - ) -} - -fn ensure_llvm_pass2_resource_layout(command: &str) -> String { - let command = command.replace( - r#"mkdir -p "$LBI_ROOT/system/lib/clang/22/lib/$LBI_TARGET" - -if [ -f "$LBI_ROOT/system/tools/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" ]; then - ln -sf "/system/tools/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" \ - "$LBI_ROOT/system/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" -fi"#, - r#"case "$LBI_ARCH" in - x86_64|amd64) compiler_rt_arch=x86_64 ;; - i?86) compiler_rt_arch=i386 ;; - aarch64|arm64) compiler_rt_arch=aarch64 ;; - *) compiler_rt_arch=$LBI_ARCH ;; -esac - -mkdir -p \ - "$LBI_ROOT/system/libraries/clang/22/lib/linux" \ - "$LBI_ROOT/system/libraries/clang/22/lib/$LBI_TARGET" - -if [ -d "$LBI_ROOT/system/lib/clang/22" ]; then - cp -R "$LBI_ROOT/system/lib/clang/22/." "$LBI_ROOT/system/libraries/clang/22/" - rm -rf "$LBI_ROOT/system/lib/clang/22" - rmdir "$LBI_ROOT/system/lib/clang" "$LBI_ROOT/system/lib" 2>/dev/null || true -fi - -builtins_name="libclang_rt.builtins-${compiler_rt_arch}.a" -builtins_src=$({ find \ - "$LBI_ROOT/system/libraries/clang/22/lib" \ - "$LBI_ROOT/system/lib/clang/22/lib" \ - -type f -name "$builtins_name" 2>/dev/null || true; } | head -n1) - -if [ -z "$builtins_src" ]; then - echo "depot: compiler-rt builtins archive $builtins_name was not installed" >&2 - exit 1 -fi - -clang_resource_root="$LBI_ROOT/system/libraries/clang/22" -mkdir -p "$clang_resource_root/lib/linux" "$clang_resource_root/lib/$LBI_TARGET" -if [ ! -f "$clang_resource_root/lib/linux/$builtins_name" ]; then - install -m644 "$builtins_src" "$clang_resource_root/lib/linux/$builtins_name" -fi -ln -sf "../linux/$builtins_name" \ - "$clang_resource_root/lib/$LBI_TARGET/libclang_rt.builtins.a""#, - ); - - command.replace( - r#"CRTBEGIN_OBJ=$(find "$LBI_ROOT/system/libraries/clang" \ - -type f \( -name 'crtbeginS.o' -o -name 'clang_rt.crtbegin*.o' \) | head -n1) -CRTEND_OBJ=$(find "$LBI_ROOT/system/libraries/clang" \ - -type f \( -name 'crtendS.o' -o -name 'clang_rt.crtend*.o' \) | head -n1) - -CRT_DIR=$(dirname "$CRTBEGIN_OBJ") - -if [ -n "$CRTBEGIN_OBJ" ] && [ -n "$CRTEND_OBJ" ]; then - ln -sf "$(basename "$CRTBEGIN_OBJ")" "$CRT_DIR/crtbeginS.o" - ln -sf "$(basename "$CRTEND_OBJ")" "$CRT_DIR/crtendS.o" - - ln -sf "${CRTBEGIN_OBJ#$LBI_ROOT/system}" "$LBI_ROOT/system/libraries/crtbeginS.o" - ln -sf "${CRTEND_OBJ#$LBI_ROOT/system}" "$LBI_ROOT/system/libraries/crtendS.o" -fi"#, - r#"CRTBEGIN_OBJ=$({ find \ - "$LBI_ROOT/system/libraries/clang" \ - "$LBI_ROOT/system/lib/clang" \ - -type f \( -name 'crtbeginS.o' -o -name 'clang_rt.crtbegin*.o' \) 2>/dev/null || true; } | head -n1) -CRTEND_OBJ=$({ find \ - "$LBI_ROOT/system/libraries/clang" \ - "$LBI_ROOT/system/lib/clang" \ - -type f \( -name 'crtendS.o' -o -name 'clang_rt.crtend*.o' \) 2>/dev/null || true; } | head -n1) - -if [ -n "$CRTBEGIN_OBJ" ] && [ -n "$CRTEND_OBJ" ]; then - CRT_DIR=$(dirname "$CRTBEGIN_OBJ") - ln -sf "$(basename "$CRTBEGIN_OBJ")" "$CRT_DIR/crtbeginS.o" - ln -sf "$(basename "$CRTEND_OBJ")" "$CRT_DIR/crtendS.o" - - install -m644 "$CRTBEGIN_OBJ" "$LBI_ROOT/system/libraries/crtbeginS.o" - install -m644 "$CRTEND_OBJ" "$LBI_ROOT/system/libraries/crtendS.o" -fi"#, - ) -} - -fn ensure_llvm_runtimes_builds_compiler_rt_crt(command: &str) -> String { - let installs_runtimes = command.contains("install") && command.contains("build-runtimes"); - if !installs_runtimes || command.contains("build-compiler-rt-crt") { - return command.to_string(); - } - - format!( - r#"{command} - -cd ../compiler-rt -rm -rf build-compiler-rt-crt -lbi_cmake build-compiler-rt-crt \ - -G Ninja \ - -DCMAKE_C_COMPILER="$CC" \ - -DCMAKE_CXX_COMPILER="$CXX" \ - -DCMAKE_ASM_COMPILER="$CC" \ - -DCMAKE_AR="$AR" \ - -DCMAKE_NM="$NM" \ - -DCMAKE_RANLIB="$RANLIB" \ - -DLLVM_CMAKE_DIR="$LBI_SYSROOT/system/tools/lib/cmake/llvm" \ - -DCMAKE_SYSROOT="$LBI_SYSROOT" \ - -DCMAKE_C_COMPILER_TARGET="$LBI_TARGET" \ - -DCMAKE_CXX_COMPILER_TARGET="$LBI_TARGET" \ - -DCMAKE_ASM_COMPILER_TARGET="$LBI_TARGET" \ - -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ - -DCMAKE_C_FLAGS="--target=$LBI_TARGET --sysroot=$LBI_SYSROOT $LWI_CFLAGS" \ - -DCMAKE_CXX_FLAGS="--target=$LBI_TARGET --sysroot=$LBI_SYSROOT ${{LWI_CXXFLAGS:-$LWI_CFLAGS}}" \ - -DCMAKE_ASM_FLAGS="--target=$LBI_TARGET --sysroot=$LBI_SYSROOT $LWI_CFLAGS" \ - -DCOMPILER_RT_INSTALL_PATH=/system/tools/lib/clang/22 \ - -DCOMPILER_RT_BUILD_BUILTINS=ON \ - -DCOMPILER_RT_BUILD_CRT=ON \ - -DCOMPILER_RT_BUILD_LIBFUZZER=OFF \ - -DCOMPILER_RT_BUILD_MEMPROF=OFF \ - -DCOMPILER_RT_BUILD_ORC=OFF \ - -DCOMPILER_RT_BUILD_PROFILE=OFF \ - -DCOMPILER_RT_BUILD_CTX_PROFILE=OFF \ - -DCOMPILER_RT_BUILD_SANITIZERS=OFF \ - -DCOMPILER_RT_BUILD_XRAY=OFF \ - -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \ - -DCOMPILER_RT_INCLUDE_TESTS=OFF \ - -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF \ - -DCMAKE_BUILD_TYPE=Release - -cmake --build build-compiler-rt-crt --target crt $LWI_MAKE_FLAGS - -crt_resource_dir="$LBI_ROOT/system/tools/lib/clang/22/lib/$LBI_TARGET" -mkdir -p "$crt_resource_dir" "$LBI_ROOT/system/libraries" -crtbegin_obj=$(find build-compiler-rt-crt \ - -type f \( -name 'crtbeginS.o' -o -name 'clang_rt.crtbegin*.o' \) 2>/dev/null | head -n1) -crtend_obj=$(find build-compiler-rt-crt \ - -type f \( -name 'crtendS.o' -o -name 'clang_rt.crtend*.o' \) 2>/dev/null | head -n1) - -if [ -z "$crtbegin_obj" ] || [ -z "$crtend_obj" ]; then - echo "depot: compiler-rt CRT build did not produce crtbegin/crtend objects" >&2 - exit 1 -fi - -install -m644 "$crtbegin_obj" "$crt_resource_dir/crtbeginS.o" -install -m644 "$crtend_obj" "$crt_resource_dir/crtendS.o" -install -m644 "$crtbegin_obj" "$LBI_ROOT/system/libraries/crtbeginS.o" -install -m644 "$crtend_obj" "$LBI_ROOT/system/libraries/crtendS.o""# - ) -} - -fn ensure_cmake_linker_flag(command: &str, variable: &str, flag: &str) -> String { - let define = format!("-D{variable}="); - if let Some(start) = command.find(&define) { - let value_start = start + define.len(); - let Some(first) = command[value_start..].chars().next() else { - return command.to_string(); - }; - - let (content_start, content_end) = if first == '"' { - let content_start = value_start + 1; - let Some(rel_end) = command[content_start..].find('"') else { - return command.to_string(); - }; - (content_start, content_start + rel_end) - } else { - let rel_end = command[value_start..] - .find(|ch: char| ch.is_whitespace()) - .unwrap_or(command.len() - value_start); - (value_start, value_start + rel_end) - }; - - let value = &command[content_start..content_end]; - if value.split_whitespace().any(|part| part == flag) { - return command.to_string(); - } - - let mut out = String::with_capacity(command.len() + flag.len() + 1); - out.push_str(&command[..content_start]); - out.push_str(flag); - if !value.is_empty() { - out.push(' '); - out.push_str(value); - } - out.push_str(&command[content_end..]); - return out; - } - - let inserted = format!(" -D{variable}=\"{flag}\" \\"); - let anchor = " -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \\"; - if command.contains(anchor) { - command.replace(anchor, &format!("{anchor}\n{inserted}")) - } else { - command.to_string() - } -} - -fn parse_source_manifest(input: &str) -> Vec { - let mut entries = Vec::new(); - for line in input.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } - let mut fields = trimmed.split_whitespace(); - let Some(url) = fields.next() else { - continue; - }; - let output_name = fields - .next() - .map(str::to_string) - .unwrap_or_else(|| filename_from_url(url)); - if !output_name.is_empty() { - entries.push(ManifestEntry { - url: url.trim_start_matches("git+").to_string(), - output_name, - }); - } - } - entries -} - -fn parse_source_b2sums(input: &str) -> Result> { - let mut sums = BTreeMap::new(); - for (idx, line) in input.lines().enumerate() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } - let mut fields = trimmed.split_whitespace(); - let Some(hash) = fields.next() else { - continue; - }; - let Some(filename) = fields.next() else { - anyhow::bail!("Malformed BLAKE2 source manifest line {}", idx + 1); - }; - if hash.len() != 128 || !hash.chars().all(|ch| ch.is_ascii_hexdigit()) { - anyhow::bail!("Invalid BLAKE2b-512 digest on line {}", idx + 1); - } - let filename = filename.trim_start_matches('*'); - if filename.is_empty() { - anyhow::bail!("Empty filename in BLAKE2 source manifest line {}", idx + 1); - } - let hash = hash.to_ascii_lowercase(); - if let Some(existing) = sums.insert(filename.to_string(), hash.clone()) - && existing != hash - { - anyhow::bail!( - "Conflicting BLAKE2 source checksums for {} on line {}", - filename, - idx + 1 - ); - } - } - Ok(sums) -} - -fn source_checksum_for_input(input: &str, source_url: &str, manifest: &SourceManifest) -> String { - if let Some(hash) = manifest.blake2b_512.get(input) { - return format!("b2sum:{hash}"); - } - - let source_filename = filename_from_url(source_url); - if source_filename != input - && let Some(hash) = manifest.blake2b_512.get(&source_filename) - { - return format!("b2sum:{hash}"); - } - - "skip".to_string() -} - -fn parse_page_recipe(html: &str, package: &BookPackage) -> Result { - let prelude = html.split("

").next().unwrap_or(html); - let input_files = input_files_from_prelude(prelude); - let source_urls = source_urls_from_prelude(prelude); - let extract_dir = extract_dir_from_html(html); - let mut commands = Vec::new(); - for (heading, code) in shell_code_blocks_by_heading(html) { - let heading_lower = heading.to_ascii_lowercase(); - if heading_lower.contains("verify") || heading_lower.contains("quick verification") { - continue; - } - let command = strip_book_extract_scaffolding(&code, extract_dir.as_deref()); - if !command.trim().is_empty() { - commands.push(command); - } - } - if commands.is_empty() { - anyhow::bail!( - "No shell command blocks were found for {} at {}", - package.name, - package.page_url - ); - } - - Ok(PageRecipe { - input_files, - source_urls, - extract_dir, - commands, - dependencies: dependency_names_from_items(list_items_after_heading(html, "Dependencies")), - license: first_list_item_after_heading(html, "Licenses") - .unwrap_or_else(|| "unknown".into()), - description: lead_text(html).unwrap_or_else(|| package.title.clone()), - }) -} - -fn shell_code_blocks_by_heading(html: &str) -> Vec<(String, String)> { - let mut out = Vec::new(); - let mut rest = html; - let mut heading = String::new(); - - loop { - let h2_pos = rest.find(" break, - - (Some(h), Some(p)) if h < p => { - let after_h2_tag = &rest[h..]; - let Some(h2_close) = after_h2_tag.find('>') else { - break; - }; - let after = &after_h2_tag[h2_close + 1..]; - let Some(end) = after.find("

") else { - break; - }; - - heading = clean_code_block(&after[..end]); - rest = &after[end + "".len()..]; - } - - (Some(_), Some(p)) | (None, Some(p)) => { - let after_pre = &rest[p..]; - let Some(pre_tag_end) = after_pre.find('>') else { - break; - }; - - let pre_body = &after_pre[pre_tag_end + 1..]; - let Some(pre_end) = pre_body.find("") else { - break; - }; - - let raw = &pre_body[..pre_end]; - - let code = if let Some(code_start_tag) = raw.find("') { - let code_body = &after_code_tag[code_tag_end + 1..]; - let code_body = code_body - .split_once("") - .map(|(before, _)| before) - .unwrap_or(code_body); - - clean_code_block(code_body) - } else { - clean_code_block(raw) - } - } else { - clean_code_block(raw) - }; - - out.push((heading.clone(), code)); - rest = &pre_body[pre_end + "".len()..]; - } - - (Some(h), None) => { - let after_h2_tag = &rest[h..]; - let Some(h2_close) = after_h2_tag.find('>') else { - break; - }; - let after = &after_h2_tag[h2_close + 1..]; - let Some(end) = after.find("") else { - break; - }; - - heading = clean_code_block(&after[..end]); - rest = &after[end + "".len()..]; - } - } - } - - out -} - -fn strip_book_extract_scaffolding(command: &str, extract_dir: Option<&str>) -> String { - let mut out = Vec::new(); - let mut skip_continuation = false; - for line in command.lines() { - let trimmed = line.trim(); - if skip_continuation { - skip_continuation = trimmed.ends_with('\\'); - continue; - } - if trimmed == "cd \"$LBI_SOURCES\"" - || trimmed == "cd /sources" - || trimmed == "cd \"/sources\"" - { - continue; - } - if extract_dir - .is_some_and(|dir| trimmed == format!("cd {dir}") || trimmed == format!("rm -rf {dir}")) - { - continue; - } - if let Some(dir) = extract_dir - && let Some(subdir) = cd_subdir_after_extract_dir(trimmed, dir) - { - let indent_len = line.len() - line.trim_start().len(); - out.push(format!("{}cd {subdir}", &line[..indent_len])); - continue; - } - if is_book_archive_extract_command(trimmed) { - skip_continuation = trimmed.ends_with('\\'); - continue; - } - out.push(line.to_string()); - } - out.join("\n") -} - -fn is_book_archive_extract_command(trimmed: &str) -> bool { - if trimmed.contains("../") { - return false; - } - - let Some(command) = trimmed.split_whitespace().next() else { - return false; - }; - command == "unzip" || (command == "tar" && trimmed.split_whitespace().any(|arg| arg == "-xf")) -} - -fn cd_subdir_after_extract_dir<'a>(trimmed: &'a str, extract_dir: &str) -> Option<&'a str> { - let dir = trimmed.strip_prefix("cd ")?; - let dir = dir.trim_matches('"').trim_matches('\''); - let subdir = dir.strip_prefix(&format!("{extract_dir}/"))?; - (!subdir.is_empty() && !subdir.starts_with('/')).then_some(subdir) -} - -fn rewrite_lbi_command(command: &str, extract_dir: Option<&str>) -> String { - let mut rewritten = command - .replace(" /sources", " \"$LBI_SOURCES\"") - .replace("cd \"/sources\"", "cd \"$LBI_SOURCES\"") - .replace("/sources/", "$LBI_SOURCES/") - .replace( - "$LBI_ROOT/system/tools/bin/", - "$LBI_SYSROOT/system/tools/bin/", - ) - .replace( - "$LBI_ROOT/system/tools/lib/", - "$LBI_SYSROOT/system/tools/lib/", - ) - .replace( - "-DCMAKE_SYSROOT=\"$LBI_ROOT\"", - "-DCMAKE_SYSROOT=\"$LBI_SYSROOT\"", - ) - .replace( - "-DCMAKE_FIND_ROOT_PATH=\"$LBI_ROOT;$LBI_ROOT/system\"", - "-DCMAKE_FIND_ROOT_PATH=\"$LBI_SYSROOT;$LBI_SYSROOT/system\"", - ) - .replace("--sysroot=$LBI_ROOT", "--sysroot=$LBI_SYSROOT") - .replace(">", ">") - .replace("<", "<"); - if let Some(dir) = extract_dir { - rewritten = rewritten - .replace(&format!("cd {dir}\n"), "") - .replace(&format!("rm -rf {dir}\n"), ""); - } - let rewritten = rewrite_absolute_system_paths(&rewritten); - let rewritten = rewrite_make_flags(&rewritten); - rewrite_parallel_job_counts(&rewritten) -} - -fn rewrite_cross_tool_paths(input: &str) -> String { - input - .replace("$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-clang++", "$CXX") - .replace("$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-clang", "$CC") - .replace("$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-ar", "$AR") - .replace("$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-nm", "$NM") - .replace( - "$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-ranlib", - "$RANLIB", - ) - .replace( - "$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-objcopy", - "$OBJCOPY", - ) - .replace( - "$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-objdump", - "$OBJDUMP", - ) - .replace("$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-strip", "$STRIP") - .replace("$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-ld", "$LD") -} - -fn restore_chroot_compiler_search_paths(input: &str) -> String { - input - .replace("-B$DESTDIR/system/", "-B/system/") - .replace("-B${DESTDIR}/system/", "-B/system/") - .replace("-I$DESTDIR/system/", "-I/system/") - .replace("-I${DESTDIR}/system/", "-I/system/") - .replace("-L$DESTDIR/system/", "-L/system/") - .replace("-L${DESTDIR}/system/", "-L/system/") - .replace("-isystem $DESTDIR/system/", "-isystem /system/") - .replace("-isystem ${DESTDIR}/system/", "-isystem /system/") -} - -fn is_clang_driver_config_command(input: &str) -> bool { - input.contains("/configuration/clang/clang.cfg") - || input.contains("/configuration/clang/clang++.cfg") -} - -fn restore_clang_driver_config_runtime_paths(input: &str) -> String { - input - .replace("-B$DESTDIR/system/", "-B/system/") - .replace("-B${DESTDIR}/system/", "-B/system/") - .replace("-I$DESTDIR/system/", "-I/system/") - .replace("-I${DESTDIR}/system/", "-I/system/") - .replace("-L$DESTDIR/system/", "-L/system/") - .replace("-L${DESTDIR}/system/", "-L/system/") - .replace("-isystem $DESTDIR/system/", "-isystem /system/") - .replace("-isystem ${DESTDIR}/system/", "-isystem /system/") - .replace("rpath,$DESTDIR/system/", "rpath,/system/") - .replace("rpath,${DESTDIR}/system/", "rpath,/system/") -} - -fn restore_rust_bootstrap_runtime_tool_paths(input: &str) -> String { - if !input.contains("bootstrap.toml") { - return input.to_string(); - } - - input - .replace("$DESTDIR/system", "/system") - .replace("${DESTDIR}/system", "/system") - .replace("$DESTDIR/system/binaries/cc", "/system/binaries/cc") - .replace("${DESTDIR}/system/binaries/cc", "/system/binaries/cc") - .replace("$DESTDIR/system/binaries/c++", "/system/binaries/c++") - .replace("${DESTDIR}/system/binaries/c++", "/system/binaries/c++") - .replace( - "$DESTDIR/system/binaries/llvm-ar", - "/system/binaries/llvm-ar", - ) - .replace( - "${DESTDIR}/system/binaries/llvm-ar", - "/system/binaries/llvm-ar", - ) - .replace( - "$DESTDIR/system/binaries/llvm-ranlib", - "/system/binaries/llvm-ranlib", - ) - .replace( - "${DESTDIR}/system/binaries/llvm-ranlib", - "/system/binaries/llvm-ranlib", - ) - .replace( - "$DESTDIR/system/binaries/llvm-config", - "/system/binaries/llvm-config", - ) - .replace( - "${DESTDIR}/system/binaries/llvm-config", - "/system/binaries/llvm-config", - ) -} - -fn rewrite_absolute_system_paths(input: &str) -> String { - let mut out = String::with_capacity(input.len()); - let bytes = input.as_bytes(); - let mut i = 0usize; - - while i < bytes.len() { - if bytes[i..].starts_with(b"/system") && is_system_path_boundary(input, i) { - let prefix = &input[..i]; - - let already_destdir = prefix.ends_with("$DESTDIR") - || prefix.ends_with("${DESTDIR}") - || prefix.ends_with("\"$DESTDIR") - || prefix.ends_with("\"${DESTDIR}") - || prefix.ends_with("$LBI_ROOT") - || prefix.ends_with("${LBI_ROOT}") - || prefix.ends_with("\"$LBI_ROOT\"") - || prefix.ends_with("\"${LBI_ROOT}\"") - || prefix.ends_with("$LBI_SYSROOT") - || prefix.ends_with("${LBI_SYSROOT}") - || prefix.ends_with("\"$LBI_SYSROOT\"") - || prefix.ends_with("\"${LBI_SYSROOT}\"") - || prefix_ends_with_shell_assignment(prefix) - || prefix_ends_with_configure_path_option(prefix); - - if !already_destdir { - out.push_str("$DESTDIR"); - } - - out.push_str("/system"); - i += "/system".len(); - } else { - let ch = input[i..] - .chars() - .next() - .expect("byte index must be inside string"); - out.push(ch); - i += ch.len_utf8(); - } - } - - out -} - -fn prefix_ends_with_configure_path_option(prefix: &str) -> bool { - let token_start = prefix - .char_indices() - .rev() - .find_map(|(idx, ch)| { - (ch.is_whitespace() || matches!(ch, ';' | '|' | '&' | '(')) - .then_some(idx + ch.len_utf8()) - }) - .unwrap_or(0); - let token = &prefix[token_start..]; - let Some((option, _)) = token.split_once('=') else { - return false; - }; - matches!( - option, - "--prefix" - | "--exec-prefix" - | "--bindir" - | "--sbindir" - | "--libdir" - | "--includedir" - | "--sysconfdir" - | "--localstatedir" - | "--datarootdir" - | "--datadir" - | "--mandir" - | "--infodir" - | "--with-default-sys-path" - ) -} - -fn is_system_path_boundary(input: &str, start: usize) -> bool { - let after = start + "/system".len(); - matches!( - input.as_bytes().get(after).copied(), - None | Some(b'/') - | Some(b':') - | Some(b'"') - | Some(b'\'') - | Some(b' ') - | Some(b'\t') - | Some(b'\n') - ) -} - -fn prefix_ends_with_shell_assignment(prefix: &str) -> bool { - let token_start = prefix - .char_indices() - .rev() - .find_map(|(idx, ch)| { - (ch.is_whitespace() || matches!(ch, ';' | '|' | '&' | '(')) - .then_some(idx + ch.len_utf8()) - }) - .unwrap_or(0); - let token = &prefix[token_start..]; - let Some((name, _)) = token.split_once('=') else { - return false; - }; - let name = name.strip_prefix("-D").unwrap_or(name); - let mut chars = name.chars(); - let Some(first) = chars.next() else { - return false; - }; - (first == '_' || first.is_ascii_alphabetic()) - && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) -} - -fn source_url_for_input( - input: &str, - package: &BookPackage, - recipe: &PageRecipe, - manifest: &[ManifestEntry], -) -> Result { - for url in &recipe.source_urls { - if filename_from_url(url) == input || url.ends_with(input) { - return Ok(url.trim_start_matches("git+").to_string()); - } - } - if let Some(entry) = manifest.iter().find(|entry| entry.output_name == input) { - return Ok(entry.url.clone()); - } - let mut filename_matches = manifest - .iter() - .filter(|entry| filename_from_url(&entry.url) == input) - .collect::>(); - if filename_matches.len() > 1 { - filename_matches.retain(|entry| manifest_entry_matches_package(entry, package)); - } - if let Some(entry) = filename_matches.first() { - return Ok(entry.url.clone()); - } - if input.ends_with(".patch") || input.ends_with(".diff") { - return Ok(Url::parse(&package.page_url)? - .join(&format!("../../patches/{input}"))? - .to_string()); - } - anyhow::bail!( - "Could not find source URL for input file {} required by {}", - input, - package.name - ) -} - -fn manifest_entry_matches_package(entry: &ManifestEntry, package: &BookPackage) -> bool { - let package = normalize_source_match_token(&package.name); - !package.is_empty() - && (normalize_source_match_token(&entry.output_name).contains(&package) - || normalize_source_match_token(&entry.url).contains(&package)) -} - -fn source_urls_from_prelude(html: &str) -> Vec { - code_values(html) - .into_iter() - .filter(|code| source_value_looks_like_url(code)) - .collect() -} - -fn input_files_from_prelude(html: &str) -> Vec { - let explicit_files = input_files_from_explicit_metadata(html); - if !explicit_files.is_empty() { - return explicit_files; - } - - collect_input_files_from_code_values(code_values(html)) -} - -fn input_files_from_explicit_metadata(html: &str) -> Vec { - let mut input_files = Vec::new(); - let mut seen_input_files = BTreeSet::new(); - - for paragraph in paragraph_bodies(html) { - if !paragraph_has_input_file_metadata_label(paragraph) { - continue; - } - - for file in collect_input_files_from_code_values(code_values(paragraph)) { - if seen_input_files.insert(file.clone()) { - input_files.push(file); - } - } - } - - input_files -} - -fn paragraph_bodies(html: &str) -> Vec<&str> { - let mut paragraphs = Vec::new(); - let mut rest = html; - while let Some(start) = rest.find("') else { - break; - }; - let body_start = start + tag_end + 1; - let after_body = &rest[body_start..]; - let Some(body_end) = after_body.find("

") else { - break; - }; - - paragraphs.push(&rest[body_start..body_start + body_end]); - rest = &after_body[body_end + "

".len()..]; - } - paragraphs -} - -fn paragraph_has_input_file_metadata_label(paragraph: &str) -> bool { - let text = strip_html_tags(paragraph).to_ascii_lowercase(); - let Some((label, _)) = text.split_once(':') else { - return false; - }; - matches!( - label.trim(), - "input assumption" - | "input assumptions" - | "input file" - | "input files" - | "source package" - | "source packages" - | "source archive" - | "source archives" - | "patch" - | "patches" - ) -} - -fn collect_input_files_from_code_values(code_values: Vec) -> Vec { - let mut input_files = Vec::new(); - let mut seen_input_files = BTreeSet::new(); - for code in code_values { - for file in words_that_look_like_input_files(&code) { - if seen_input_files.insert(file.clone()) { - input_files.push(file); - } - } - } - input_files -} - -fn extract_dir_from_html(html: &str) -> Option { - for (_, code) in shell_code_blocks_by_heading(html) { - for line in code.lines() { - let trimmed = line.trim(); - if trimmed == "cd \"$LBI_SOURCES\"" - || trimmed == "cd /sources" - || trimmed == "cd \"/sources\"" - { - continue; - } - if let Some(dir) = trimmed.strip_prefix("cd ") { - let dir = dir.trim_matches('"').trim_matches('\''); - if !dir.starts_with('$') && !dir.starts_with('/') && !dir.contains(' ') { - return dir.split('/').next().map(str::to_string); - } - } - } - } - None -} - -fn code_values(html: &str) -> Vec { - let mut values = Vec::new(); - let mut rest = html; - while let Some(start) = rest.find("') else { - break; - }; - let value_start = start + close + 1; - let after_value = &rest[value_start..]; - let Some(end) = after_value.find("") else { - break; - }; - values.push(html_decode(&after_value[..end])); - rest = &after_value[end + "".len()..]; - } - values -} - -fn words_that_look_like_input_files(input: &str) -> Vec { - input - .split(|c: char| { - c.is_whitespace() || c == ',' || c == '(' || c == ')' || c == '"' || c == '\'' - }) - .map(|word| word.trim_matches(|c: char| c == '`' || c == '.' || c == ';')) - .filter(|word| { - !source_value_looks_like_url(word) - && (is_archive_filename(word) - || word.ends_with(".patch") - || word.ends_with(".diff") - || word.ends_with(".pem")) - }) - .map(str::to_string) - .collect() -} - -fn source_value_looks_like_url(input: &str) -> bool { - let lower = input.to_ascii_lowercase(); - lower.starts_with("http://") - || lower.starts_with("https://") - || lower.starts_with("git+http://") - || lower.starts_with("git+https://") -} - -fn first_list_item_after_heading(html: &str, heading: &str) -> Option { - list_items_after_heading(html, heading).into_iter().next() -} - -fn list_items_after_heading(html: &str, heading: &str) -> Vec { - let marker = format!("

{heading}:

"); - let Some(after) = html.split_once(&marker).map(|(_, after)| after) else { - return Vec::new(); - }; - let list = after - .split_once("") - .map(|(list, _)| list) - .unwrap_or(after); - let mut items = Vec::new(); - let mut rest = list; - while let Some((_, after_li)) = rest.split_once("
  • ") { - let Some((item, after_item)) = after_li.split_once("
  • ") else { - break; - }; - let item = strip_html_tags(item); - if !item.is_empty() { - items.push(item); - } - rest = after_item; - } - items -} - -fn dependency_names_from_items(items: Vec) -> Vec { - let mut deps = Vec::new(); - let mut seen = BTreeSet::new(); - for item in items { - for dep in normalize_dependency_item(&item) { - if seen.insert(dep.clone()) { - deps.push(dep); - } - } - } - deps -} - -fn normalize_dependency_item(item: &str) -> Vec { - let item = item.replace('`', ""); - let lower = item.trim().to_ascii_lowercase(); - if lower.is_empty() - || lower.contains("(host)") - || lower.starts_with("host ") - || lower.contains("host c compiler") - { - return Vec::new(); - } - - let dep = if lower.contains("musl") || lower == "pthreads" { - "musl" - } else if lower.contains("clang/llvm") || lower.contains("llvm toolchain") { - "llvm" - } else if lower.starts_with("compiler-rt") { - "compiler-rt" - } else if lower.starts_with("libunwind") { - "libunwind" - } else if lower.starts_with("libcxxabi") { - "libcxxabi" - } else if lower.starts_with("libcxx") { - "libcxx" - } else if lower.starts_with("lld") { - "lld" - } else if lower.starts_with("llvm") { - "llvm" - } else if lower.starts_with("clang") || lower.starts_with("c++ compiler") { - "clang" - } else if lower.starts_with("cargo") { - "cargo" - } else if lower.starts_with("rustc") { - "rustc" - } else if lower.starts_with("libressl") { - "libressl" - } else if lower.starts_with("sqlite") { - "sqlite" - } else if lower.starts_with("zlib-ng") { - "zlib-ng" - } else if lower.starts_with("xz") { - "xz" - } else if lower.starts_with("zstd") { - "zstd" - } else if lower.starts_with("curl") { - "curl" - } else if lower.starts_with("pkgconf") { - "pkgconf" - } else if lower.starts_with("ca-certificates") { - "ca-certificates" - } else if lower.starts_with("python-flit-core") || lower.starts_with("flit-core") { - "python-flit-core" - } else if lower.starts_with("python-packaging") || lower.starts_with("packaging") { - "python-packaging" - } else if lower.starts_with("python-wheel") || lower.starts_with("wheel") { - "python-wheel" - } else if lower.starts_with("python-setuptools") || lower.starts_with("setuptools") { - "python-setuptools" - } else if lower.starts_with("python") { - "python" - } else if lower.starts_with("pip") { - "pip" - } else if lower.starts_with("cmake") { - "cmake" - } else if lower.starts_with("meson") { - "meson" - } else if lower.starts_with("samurai") { - "samurai" - } else if lower.starts_with("ninja") { - "ninja" - } else if lower.starts_with("bmake") { - "bmake" - } else if lower.starts_with("make") { - "make" - } else if lower.starts_with("bison") - || lower.starts_with("yacc") - || lower.contains("yacc-compatible") - { - "byacc" - } else if lower.starts_with("lex") || lower.starts_with("flex") { - "flex" - } else if lower == "m4" || lower.starts_with("m4 ") { - "m4" - } else if lower.starts_with("om4") { - "om4" - } else if lower.starts_with("ncurses") { - "ncurses" - } else if lower.starts_with("libedit") { - "libedit" - } else if lower.starts_with("libarchive") { - "libarchive" - } else if lower.starts_with("bheaded") { - "bheaded" - } else if lower.starts_with("awk") { - "awk" - } else { - return Vec::new(); - }; - - vec![dep.to_string()] -} - -fn lead_text(html: &str) -> Option { - let after = html.split_once("

    ")?.1; - let text = after.split_once("

    ")?.0; - Some(strip_html_tags(text)) -} - -fn strip_html_tags(input: &str) -> String { - let mut out = String::new(); - let mut in_tag = false; - for c in input.chars() { - match c { - '<' => in_tag = true, - '>' => in_tag = false, - _ if !in_tag => out.push(c), - _ => {} - } - } - html_decode(out.trim()) -} - -fn html_decode(input: &str) -> String { - input - .replace(""", "\"") - .replace("'", "'") - .replace("'", "'") - .replace(">", ">") - .replace("<", "<") - .replace("&", "&") -} - -fn clean_code_block(input: &str) -> String { - let stripped = strip_html_tags(input); - stripped - .lines() - .map(|line| line.trim_end()) - .collect::>() - .join("\n") -} - -fn is_archive_filename(input: &str) -> bool { - let lower = input.to_ascii_lowercase(); - lower.ends_with(".tar.gz") - || lower.ends_with(".tgz") - || lower.ends_with(".tar.xz") - || lower.ends_with(".txz") - || lower.ends_with(".tar.bz2") - || lower.ends_with(".tbz2") - || lower.ends_with(".zip") - || lower.ends_with(".tar") -} - -fn strip_archive_suffix(input: &str) -> &str { - for suffix in [ - ".tar.gz", ".tar.xz", ".tar.bz2", ".tgz", ".txz", ".tbz2", ".zip", ".tar", - ] { - if let Some(stripped) = input.strip_suffix(suffix) { - return stripped; - } - } - input -} - -fn filename_from_url(url: &str) -> String { - let clean = url - .trim_start_matches("git+") - .split('#') - .next() - .unwrap_or(url); - Url::parse(clean) - .ok() - .and_then(|url| { - url.path_segments() - .and_then(|mut segments| segments.next_back().map(str::to_string)) - }) - .filter(|name| !name.is_empty()) - .unwrap_or_else(|| clean.rsplit('/').next().unwrap_or(clean).to_string()) -} - -fn normalize_source_match_token(input: &str) -> String { - input - .chars() - .filter(|c| c.is_ascii_alphanumeric()) - .flat_map(char::to_lowercase) - .collect() -} - -fn toml_escape(input: &str) -> String { - input.replace('\\', "\\\\").replace('"', "\\\"") -} - -fn shell_double_quote_literal(input: &str) -> String { - input - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('$', "\\$") - .replace('`', "\\`") -} - -fn version_from_title(title: &str) -> String { - title - .split_whitespace() - .rev() - .find(|word| word.chars().any(|c| c.is_ascii_digit())) - .map(|word| { - word.trim_matches(|c: char| c == ',' || c == ';' || c == ':' || c == '.') - .to_string() - }) - .unwrap_or_else(|| "0".to_string()) -} - -fn page_slug_from_title(title: &str, package: &str) -> String { - let lower = normalize_title(title); - - match lower.as_str() { - s if s.starts_with("musl libc final pass") => "musl-libc-final-pass".to_string(), - s if s.starts_with("llvm final") => "llvm-final".to_string(), - - // These chapter 8 pages are stage-2 packages in the title, - // but the actual LBI page filenames do not include "-stage2". - s if s.starts_with("zstd") => "zstd".to_string(), - s if s.starts_with("samurai") => "samurai".to_string(), - - s if s.contains("stage 2") => format!("{package}-stage2"), - - _ => package.to_string(), - } -} - -fn book_base_url(book_url: &str) -> Result { - let mut url = Url::parse(book_url).with_context(|| format!("Invalid book URL: {book_url}"))?; - { - let mut segments = url - .path_segments_mut() - .map_err(|_| anyhow::anyhow!("Book URL cannot be a base URL: {book_url}"))?; - segments.pop(); - segments.push(""); - } - Ok(url) -} - -fn parse_book_steps(text: &str, book_url: &str) -> Result> { - let book_base = book_base_url(book_url)?; - let mut sections = Vec::<(String, String)>::new(); - let mut seen_sections = BTreeSet::::new(); - for line in text.lines() { - if let Some((section, title)) = parse_section_line(line) - && seen_sections.insert(section.clone()) - { - sections.push((section, title)); - } - } - - let mut steps = Vec::new(); - for (section, title) in sections { - let Some(chapter) = section_chapter(§ion) else { - continue; - }; - if let Some(kind) = operation_kind_from_title(&title) { - steps.push(BookStep::Operation(BookOperation { - section: section.clone(), - title: title.clone(), - kind, - recipe_id: format!("{}-{}", section.replace('.', "-"), operation_slug(kind)), - })); - continue; - } - - let Some(name) = package_name_from_title(&title) else { - continue; - }; - let slug = page_slug_from_title(&title, &name); - let page_url = book_base - .join(&format!("chapters/chapter{chapter}/{slug}.html"))? - .to_string(); - let recipe_id = format!("{}-{}", section.replace('.', "-"), name); - steps.push(BookStep::Package(BookPackage { - chapter, - section: section.clone(), - title: title.clone(), - version: version_from_title(&title), - layer: layer_for_package(chapter, &name), - name, - page_url, - recipe_id, - })); - } - Ok(steps) -} - -fn packages_from_steps(steps: &[BookStep]) -> Vec { - steps - .iter() - .filter_map(|step| match step { - BookStep::Package(package) => Some(package.clone()), - BookStep::Operation(_) => None, - }) - .collect() -} - -impl BookStep { - fn section(&self) -> &str { - match self { - BookStep::Package(package) => &package.section, - BookStep::Operation(operation) => &operation.section, - } - } -} - -fn packages_by_layer(packages: &[BookPackage]) -> BTreeMap> { - let mut layers = BTreeMap::>::new(); - let mut seen = BTreeMap::>::new(); - for package in packages { - if seen - .entry(package.layer.clone()) - .or_default() - .insert(package.name.clone()) - { - layers - .entry(package.layer.clone()) - .or_default() - .push(package.name.clone()); - } - } - layers -} - -fn bootstrap_layer_packages_for_state( - layers: &BTreeMap>, - layer: &str, -) -> Vec { - let mut packages = layers.get(layer).cloned().unwrap_or_default(); - if layer == BASE_LAYER && !packages.iter().any(|package| package == FILESYSTEM_PACKAGE) { - packages.insert(0, FILESYSTEM_PACKAGE.to_string()); - } - filter_retired_layer_packages(layer, packages) -} - -fn filter_retired_layer_packages(layer: &str, packages: Vec) -> Vec { - if layer != TEMP_LAYER { - return packages; - } - - packages - .into_iter() - .filter(|package| !CHAPTER7_RETIRED_PACKAGES.contains(&package.as_str())) - .collect() -} - -fn parse_section_line(line: &str) -> Option<(String, String)> { - let trimmed = line - .trim_start_matches(|c: char| c == '\u{c}' || c.is_whitespace()) - .trim_start_matches(['â–ª', '•', 'â—¦', '*', '-']) - .trim(); - let mut fields = trimmed.split_whitespace(); - let section_raw = fields.next()?.trim_end_matches('.'); - if !section_raw.contains('.') { - return None; - } - if !section_raw - .split('.') - .all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit())) - { - return None; - } - let chapter = section_chapter(section_raw)?; - if !(5..=9).contains(&chapter) { - return None; - } - let title = fields.collect::>().join(" "); - if title.is_empty() { - return None; - } - Some((section_raw.to_string(), title)) -} - -fn section_chapter(section: &str) -> Option { - section.split('.').next()?.parse::().ok() -} - -fn package_name_from_title(title: &str) -> Option { - let lower = normalize_title(title); - if should_skip_title(&lower) { - return None; - } - - let mapped = match lower.as_str() { - s if s.starts_with("linux api headers") => "linux-api-headers", - s if s.starts_with("musl libc headers") => "musl-libc-headers", - s if s.starts_with("llvm/clang pass 1") => "llvm-clang-pass1", - s if s.starts_with("musl libc pass 2") => "musl-libc-pass2", - s if s.starts_with("llvm runtimes") => "llvm-runtimes", - s if s.starts_with("llvm/clang pass 2") => "llvm-clang-pass2", - s if s.starts_with("musl libc final pass") => "musl", - s if s.starts_with("gnu make") => "make", - s if s.starts_with("bsd-diffutils") => "bsddiffutils", - s if s.starts_with("patch stage 2") => "bsdpatch", - s if s.starts_with("llvm final") => "llvm", - s if s.starts_with("cmake") => "cmake", - s if s.starts_with("shadow") => "shadow", - s if s.starts_with("libressl") => "libressl", - s if s.starts_with("python-flit-core") => "python-flit-core", - s if s.starts_with("ca-certificates") => "ca-certificates", - _ => return generic_package_name(&lower), - }; - Some(mapped.to_string()) -} - -fn normalize_title(title: &str) -> String { - let no_parens = title - .split_once('(') - .map(|(before, _)| before) - .unwrap_or(title); - no_parens - .trim() - .trim_end_matches('.') - .split_whitespace() - .collect::>() - .join(" ") - .to_ascii_lowercase() -} - -fn should_skip_title(title: &str) -> bool { - if title.starts_with("copy selected build variables and helper functions into target") { - return false; - } - - matches!( - title, - "introduction" - | "sources" - | "final checks" - | "reset target tree ownership to root" - | "create virtual filesystem link targets" - | "copy selected build variables and helper functions into target profile" - | "enter chroot environment" - | "create essential system files" - | "cleanup" - | "dinit service setup" - ) -} - -fn operation_kind_from_title(title: &str) -> Option { - let title = normalize_title(title); - if title.starts_with("copy selected build variables and helper functions into target") { - return Some(BookOperationKind::CopyBuildProfile); - } - - match title.as_str() { - "reset target tree ownership to root" => Some(BookOperationKind::ResetTargetTreeOwnership), - "create virtual filesystem link targets" => { - Some(BookOperationKind::CreateVirtualFilesystemLinkTargets) - } - "enter chroot environment" => Some(BookOperationKind::EnterChroot), - "create essential system files" => Some(BookOperationKind::CreateEssentialSystemFiles), - _ => None, - } -} - -fn operation_slug(kind: BookOperationKind) -> &'static str { - match kind { - BookOperationKind::ResetTargetTreeOwnership => "reset-ownership", - BookOperationKind::CreateVirtualFilesystemLinkTargets => "virtual-filesystems", - BookOperationKind::CopyBuildProfile => "copy-build-profile", - BookOperationKind::EnterChroot => "enter-chroot", - BookOperationKind::CreateEssentialSystemFiles => "essential-files", - } -} - -fn generic_package_name(title: &str) -> Option { - let mut words = Vec::new(); - for word in title.split_whitespace() { - let cleaned = word.trim_matches(|c: char| { - c == ',' || c == ';' || c == ':' || c == '.' || c == '(' || c == ')' - }); - if cleaned.is_empty() || !cleaned.chars().any(|c| c.is_ascii_alphanumeric()) { - continue; - } - if cleaned.eq_ignore_ascii_case("stage") - || cleaned.eq_ignore_ascii_case("pass") - || cleaned.eq_ignore_ascii_case("final") - || cleaned.eq_ignore_ascii_case("git") - || cleaned.eq_ignore_ascii_case("main") - || cleaned.eq_ignore_ascii_case("master") - || cleaned.eq_ignore_ascii_case("snapshot") - || cleaned.chars().next().is_some_and(|c| c.is_ascii_digit()) - { - break; - } - words.push(cleaned); - } - if words.is_empty() { - return None; - } - let package = words.join("-").replace('/', "-"); - package - .chars() - .any(|c| c.is_ascii_alphanumeric()) - .then_some(package) -} - -fn layer_for_package(chapter: u8, package: &str) -> String { - if package == "linux-api-headers" { - return DEVEL_LAYER.to_string(); - } - - if chapter == 5 || chapter == 6 { - return TEMP_LAYER.to_string(); - } - - if is_devel_package(package) { - DEVEL_LAYER.to_string() - } else { - BASE_LAYER.to_string() - } -} - -fn is_devel_package(package: &str) -> bool { - matches!( - package, - "byacc" - | "bmake" - | "cmake" - | "flex" - | "llvm" - | "make" - | "pkgconf" - | "python" - | "python-flit-core" - | "python-packaging" - | "python-setuptools" - | "python-wheel" - | "rustc" - | "meson" - | "samurai" - | "sqlite" - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_support::TestEnv; - use std::ffi::OsStr; - - const SAMPLE_TOC: &str = r#" - 5.1 Linux API Headers 7.0 - 5.3 llvm/clang pass 1 22.1.3 - 6.17 GNU Make 4.4.1 - 7.1 Introduction - â–ª 7.2 Reset Target Tree Ownership to root - â–ª 7.3 Create virtual filesystem link targets - â–ª 7.4 Copy selected build variables and helper functions into target - profile - â–ª 7.5 Enter chroot environment - â–ª 7.6 Create essential system files - 7.7 gettext-tiny 0.3.3 - 7.8 byacc 20260126 - 8.2 iana-etc 20260409 - 8.4 pigz stage 2 2.8 - 8.14.1 ca-certificates 2026-03-19 - â–ª 8.17 libffi 3.5.2 - â–ª 8.18 python 3.14.4 - â–ª 8.20 Python-Packaging 26.2 - â–ª 8.21 Python-Wheel 0.47.0 - â–ª 8.22 Python-Setuptools 82.0.1 - â–ª 8.23 Meson 1.11.1 - 8.29 CMake 4.3.2 - 8.34 LLVM final 22.1.3 - 9.2 Limine 11.4.1 - 9.3 dinit service setup - "#; - - #[test] - fn parses_book_packages_into_layers() { - let steps = - parse_book_steps(SAMPLE_TOC, "https://www.vertexlinux.net/lbi/book.pdf").unwrap(); - let packages = packages_from_steps(&steps); - let layers = packages_by_layer(&packages); - assert_eq!(layers[TEMP_LAYER], vec!["llvm-clang-pass1", "make"]); - assert_eq!( - layers[BASE_LAYER], - vec![ - "gettext-tiny", - "iana-etc", - "pigz", - "ca-certificates", - "libffi", - "limine" - ] - ); - assert_eq!( - layers[DEVEL_LAYER], - vec![ - "linux-api-headers", - "byacc", - "python", - "python-packaging", - "python-wheel", - "python-setuptools", - "meson", - "cmake", - "llvm" - ] - ); - assert_eq!(packages[0].recipe_id, "5-1-linux-api-headers"); - assert_eq!( - packages[0].page_url, - "https://www.vertexlinux.net/lbi/chapters/chapter5/linux-api-headers.html" - ); - assert_eq!(packages.last().unwrap().name, "limine"); - assert!(packages.iter().all(|package| package.chapter <= 9)); - } - - #[test] - fn bootstrap_state_base_layer_includes_filesystem_package() { - let steps = - parse_book_steps(SAMPLE_TOC, "https://www.vertexlinux.net/lbi/book.pdf").unwrap(); - let packages = packages_from_steps(&steps); - let layers = packages_by_layer(&packages); - - assert_eq!( - bootstrap_layer_packages_for_state(&layers, BASE_LAYER), - vec![ - "filesystem", - "gettext-tiny", - "iana-etc", - "pigz", - "ca-certificates", - "libffi", - "limine" - ] - ); - } - - #[test] - fn parses_book_steps_including_operational_sections() { - let steps = - parse_book_steps(SAMPLE_TOC, "https://www.vertexlinux.net/lbi/book.pdf").unwrap(); - let operations = steps - .iter() - .filter_map(|step| match step { - BookStep::Operation(operation) => { - Some((operation.section.as_str(), operation.kind)) - } - BookStep::Package(_) => None, - }) - .collect::>(); - - assert_eq!( - operations, - vec![ - ("7.2", BookOperationKind::ResetTargetTreeOwnership), - ("7.3", BookOperationKind::CreateVirtualFilesystemLinkTargets), - ("7.4", BookOperationKind::CopyBuildProfile), - ("7.5", BookOperationKind::EnterChroot), - ("7.6", BookOperationKind::CreateEssentialSystemFiles), - ] - ); - assert!(matches!(steps[0], BookStep::Package(_))); - assert!(matches!(steps[3], BookStep::Operation(_))); - } - - #[test] - fn parse_book_steps_preserves_book_order_for_reordered_sections() { - let steps = parse_book_steps( - r#" - 8.25.1 samurai stage 2 1.3 - 8.26 BSD-Diffutils stage 2 0.99.0 - 8.25.1 samurai stage 2 1.3 - "#, - "https://www.vertexlinux.net/lbi/book.pdf", - ) - .unwrap(); - let packages = packages_from_steps(&steps); - - assert_eq!( - packages - .iter() - .map(|package| package.name.as_str()) - .collect::>(), - vec!["samurai", "bsddiffutils"] - ); - } - - #[test] - fn fresh_book_fetch_url_adds_cache_buster_without_changing_path() { - let fetch_url = - fresh_book_fetch_url("https://www.vertexlinux.net/lbi/book.pdf?existing=1").unwrap(); - let parsed = Url::parse(&fetch_url).unwrap(); - - assert_eq!(parsed.scheme(), "https"); - assert_eq!(parsed.host_str(), Some("www.vertexlinux.net")); - assert_eq!(parsed.path(), "/lbi/book.pdf"); - let pairs = parsed.query_pairs().collect::>(); - assert!( - pairs - .iter() - .any(|(key, value)| key == "existing" && value == "1") - ); - assert!( - pairs - .iter() - .any(|(key, value)| key == BOOK_FETCH_CACHE_BUST_PARAM && !value.is_empty()) - ); - } - - #[test] - fn parses_lbi_b2sum_manifest_by_filename() { - let sums = parse_source_b2sums( - r#" -# Linux by Intent starter source checksums (BLAKE2b-512) -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa musl-1.2.6.tar.gz -BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB *linux-by-intent-patches.zip -"#, - ) - .unwrap(); - - assert_eq!( - sums["musl-1.2.6.tar.gz"], - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ); - assert_eq!( - sums["linux-by-intent-patches.zip"], - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ); - } - - #[test] - fn registers_filesystem_package_for_lbi_layout() { - let tmp = tempfile::tempdir().unwrap(); - let config = config::Config::for_rootfs(tmp.path()); - - register_filesystem_package(tmp.path(), &config).unwrap(); - - let db_path = config.installed_db_path(tmp.path()); - let installed = crate::db::get_installed_packages(&db_path).unwrap(); - assert!(installed.contains(FILESYSTEM_PACKAGE)); - - let files = crate::db::get_package_files(&db_path, FILESYSTEM_PACKAGE).unwrap(); - assert!(files.contains(&"etc".to_string())); - assert!(files.contains(&"system/configuration/passwd".to_string())); - - let groups = crate::db::get_package_groups(&db_path, FILESYSTEM_PACKAGE).unwrap(); - assert_eq!(groups, vec![BASE_LAYER.to_string()]); - } - - #[test] - fn page_recipe_preserves_input_order_and_strips_extract_scaffolding() { - let package = BookPackage { - chapter: 6, - section: "6.7".to_string(), - title: "musl libc pass 2 1.2.5".to_string(), - name: "musl-libc-pass2".to_string(), - version: "1.2.5".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://www.vertexlinux.net/lbi/chapters/chapter6/musl-libc-pass2.html" - .to_string(), - recipe_id: "6-7-musl-libc-pass2".to_string(), - }; - let html = r#" -

    Build musl for the temporary toolchain.

    -

    Source package: musl-1.2.5.tar.gz

    -

    Source package: mimalloc-v3.3.0.tar.gz

    -

    Source URLs: https://musl.libc.org/releases/musl-1.2.5.tar.gz and https://github.com/microsoft/mimalloc/archive/refs/tags/v3.3.0.tar.gz

    -

    Source package: musl-fix.patch

    -

    Extract:

    -
    cd "$LBI_SOURCES"
    -tar -xf musl-1.2.5.tar.gz
    -cd musl-1.2.5
    -

    Build:

    -
    patch -Np1 -i ../musl-fix.patch
    -tar -xf ../mimalloc-v3.3.0.tar.gz
    -./configure --prefix=/system
    -make
    -

    Install:

    -
    make DESTDIR="$LBI_ROOT" install
    -rm -rf musl-1.2.5
    -

    Licenses:

    • MIT
    - "#; - - let recipe = parse_page_recipe(html, &package).unwrap(); - - assert_eq!( - recipe.input_files, - vec![ - "musl-1.2.5.tar.gz", - "mimalloc-v3.3.0.tar.gz", - "musl-fix.patch" - ] - ); - assert_eq!( - recipe.source_urls, - vec![ - "https://musl.libc.org/releases/musl-1.2.5.tar.gz", - "https://github.com/microsoft/mimalloc/archive/refs/tags/v3.3.0.tar.gz" - ] - ); - assert_eq!(recipe.extract_dir.as_deref(), Some("musl-1.2.5")); - assert!( - !recipe - .commands - .iter() - .any(|cmd| cmd.contains("tar -xf musl")) - ); - assert!( - recipe - .commands - .iter() - .any(|cmd| cmd.contains("../musl-fix.patch")) - ); - assert!( - recipe - .commands - .iter() - .any(|cmd| cmd.contains("../mimalloc-v3.3.0.tar.gz")) - ); - assert_eq!(recipe.license, "MIT"); - } - - #[test] - fn page_recipe_ignores_source_note_archives_when_input_assumption_exists() { - let package = BookPackage { - chapter: 9, - section: "9.2".to_string(), - title: "Limine 11.4.1 binary release".to_string(), - name: "limine".to_string(), - version: "11.4.1".to_string(), - layer: BASE_LAYER.to_string(), - page_url: "https://www.vertexlinux.net/lbi/chapters/chapter9/limine.html".to_string(), - recipe_id: "9-2-limine".to_string(), - }; - let html = r#" -

    Install Limine's bootloader payloads.

    -
    -

    Input assumption: limine-5be26a73d7b7.tar.gz is already present in /sources.

    -

    Source URL: https://github.com/Limine-Bootloader/Limine/commit/5be26a73d7b7b4d4477d18be94e1d16e615adf56

    -

    Source note: this is the upstream v11.4.1-binary snapshot. Unlike the full limine-11.4.1.tar.xz source release, it does not need nasm.

    -
    -

    Extract and Enter the Source Tree

    -
    cd /sources
    -tar -xf limine-5be26a73d7b7.tar.gz
    -cd limine-5be26a73d7b7
    -

    Build the Limine Utility

    -
    make $LWI_MAKE_FLAGS CC=clang
    -

    Install Limine

    -
    install -Dm755 limine /system/binaries/limine
    -

    Licenses:

    • BSD 2-Clause
    - "#; - let manifest = parse_source_manifest( - "git+https://github.com/Limine-Bootloader/Limine.git#5be26a73d7b7b4d4477d18be94e1d16e615adf56 limine-5be26a73d7b7.tar.gz", - ); - - let recipe = parse_page_recipe(html, &package).unwrap(); - let source_url = - source_url_for_input(&recipe.input_files[0], &package, &recipe, &manifest).unwrap(); - - assert_eq!( - recipe.input_files, - vec!["limine-5be26a73d7b7.tar.gz".to_string()] - ); - assert_eq!( - source_url, - "https://github.com/Limine-Bootloader/Limine.git#5be26a73d7b7b4d4477d18be94e1d16e615adf56" - ); - } - - #[test] - fn page_recipe_preserves_book_subdir_after_archive_root() { - let package = BookPackage { - chapter: 5, - section: "5.5".to_string(), - title: "LLVM runtimes (libunwind, libcxxabi, libcxx) 22.1.3".to_string(), - name: "llvm-runtimes".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-runtimes.html".to_string(), - recipe_id: "5-5-llvm-runtimes".to_string(), - }; - let html = r#" -

    Build runtimes.

    -

    Source package: llvm-project-22.1.3.src.tar.xz

    -

    Source URLs: https://example.invalid/llvm-project-22.1.3.src.tar.xz

    -

    Extract:

    -
    cd "$LBI_SOURCES"
    -tar -xf llvm-project-22.1.3.src.tar.xz
    -cd llvm-project-22.1.3.src/runtimes
    -

    Configure:

    -
    lbi_cmake build-runtimes \
    -    -DLIBUNWIND_INSTALL_LIBRARY_DIR=/system/libraries
    - "#; - - let recipe = parse_page_recipe(html, &package).unwrap(); - - assert_eq!( - recipe.extract_dir.as_deref(), - Some("llvm-project-22.1.3.src") - ); - assert!( - recipe - .commands - .iter() - .any(|cmd| cmd.trim() == "cd runtimes") - ); - } - - #[test] - fn rewrites_absolute_system_paths_for_destdir_installs() { - let input = - "PREFIX=/system\ninstall -Dm755 foo /system/binaries/foo\nln -s /system/bin foo"; - let rewritten = rewrite_absolute_system_paths(input); - - assert!(rewritten.contains("PREFIX=/system")); - assert!(rewritten.contains("install -Dm755 foo $DESTDIR/system/binaries/foo")); - assert!(rewritten.contains("ln -s $DESTDIR/system/bin foo")); - } - - #[test] - fn rewrite_absolute_system_paths_keeps_configure_path_options() { - let input = "./configure --prefix=/system \\\n --bindir=/system/binaries \\\n --mandir=/system/documentation/man-pages"; - let rewritten = rewrite_absolute_system_paths(input); - - assert!(rewritten.contains("--prefix=/system")); - assert!(rewritten.contains("--bindir=/system/binaries")); - assert!(rewritten.contains("--mandir=/system/documentation/man-pages")); - assert!(!rewritten.contains("--prefix=$DESTDIR/system")); - assert!(!rewritten.contains("--bindir=$DESTDIR/system/binaries")); - } - - #[test] - fn rewrite_absolute_system_paths_does_not_double_prefix_quoted_destdir() { - let input = "\"$DESTDIR/system/systembinaries/pwconv\" -R /"; - let rewritten = rewrite_absolute_system_paths(input); - - assert_eq!(rewritten, input); - } - - #[test] - fn rewrite_absolute_system_paths_does_not_rewrite_systembinaries_segment() { - let input = "/system/systembinaries/pwconv"; - let rewritten = rewrite_absolute_system_paths(input); - - assert_eq!(rewritten, "$DESTDIR/system/systembinaries/pwconv"); - } - - #[test] - fn generated_oksh_keeps_prefixes_and_uses_resolved_cross_tools() { - let package = BookPackage { - chapter: 6, - section: "6.14".to_string(), - title: "oksh 7.8".to_string(), - name: "oksh".to_string(), - version: "7.8".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/oksh.html".to_string(), - recipe_id: "6-14-oksh".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["oksh-7.8.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/oksh-7.8.tar.gz".to_string()], - extract_dir: Some("oksh-7.8".to_string()), - commands: vec![ - r#"./configure \ - --no-thanks \ - --disable-curses \ - --prefix=/system \ - --bindir=/system/binaries \ - --mandir=/system/documentation/man-pages \ - --cc="$LBI_ROOT/system/tools/bin/$LBI_TARGET-clang" \ - --cflags="--target=$LBI_TARGET --sysroot=$LBI_ROOT $LWI_CFLAGS""# - .to_string(), - r#"make $LWI_MAKE_FLAGS \ - LDFLAGS="--target=$LBI_TARGET --sysroot=$LBI_ROOT $LBI_CUSTOM_LDFLAGS""# - .to_string(), - r#"make install DESTDIR="$LBI_ROOT" -ln -sf oksh "$LBI_ROOT/system/binaries/ksh""# - .to_string(), - ], - dependencies: Vec::new(), - license: "Public domain".to_string(), - description: "oksh shell".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-lbi-linux-musl", "x86_64"); - - assert!(build_script.contains("--prefix=/system")); - assert!(build_script.contains("--bindir=/system/binaries")); - assert!(build_script.contains("--mandir=/system/documentation/man-pages")); - assert!(!build_script.contains("--prefix=$DESTDIR/system")); - assert!(!build_script.contains("--bindir=$DESTDIR/system/binaries")); - assert!(build_script.contains(r#"--cc="$CC""#)); - assert!(build_script.contains("lbi_find_cross_tool ar llvm-ar")); - assert!(build_script.contains("export PATH=\"$LBI_SYSROOT/system/tools/bin:$PATH\"")); - assert!( - !build_script - .contains("$LBI_SYSROOT/system/tools/bin\" \"$LBI_SYSROOT/system/binaries") - ); - assert!(build_script.contains("ln -sf oksh \"$LBI_ROOT/system/binaries/ksh\"")); - } - - #[test] - fn generated_om4_parser_fix_preserves_c_include_headers() { - let package = BookPackage { - chapter: 6, - section: "6.2".to_string(), - title: "om4 6.7".to_string(), - name: "om4".to_string(), - version: "6.7".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/om4.html".to_string(), - recipe_id: "6-2-om4".to_string(), - }; - let html = r#" -

    Build om4.

    -

    Source package: om4-6.7.tar.gz

    -

    Source URLs: https://example.invalid/om4-6.7.tar.gz

    -

    Extract and Enter the Source Tree

    -
    cd "$LBI_SOURCES"
    -tar -xf om4-6.7.tar.gz
    -cd om4-6.7
    -

    Apply the Parser Compatibility Fix

    -
    grep -q '^#include <stdlib.h>$' parser.y || \
    -    sed -i '/^#include <stdint.h>$/a #include <stdlib.h>' parser.y
    -

    Build and Install om4

    -
    make -j1 CC="$LBI_ROOT/system/tools/bin/$LBI_TARGET-clang"
    -make CC="$LBI_ROOT/system/tools/bin/$LBI_TARGET-clang" install DESTDIR="$LBI_ROOT"
    -

    Licenses:

    • BSD-3-Clause
    - "#; - - let recipe = parse_page_recipe(html, &package).unwrap(); - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(build_script.contains("grep -q '^#include $' parser.y")); - assert!( - build_script.contains("sed -i '/^#include $/a #include ' parser.y") - ); - assert!(!build_script.contains("grep -q '^#include $' parser.y")); - } - - #[test] - fn rewrite_absolute_system_paths_keeps_cmake_define_values() { - let input = - "cmake -DLIBCXX_INSTALL_LIBRARY_DIR=/system/libraries -DCMAKE_INSTALL_PREFIX=/system"; - let rewritten = rewrite_absolute_system_paths(input); - - assert!(rewritten.contains("-DLIBCXX_INSTALL_LIBRARY_DIR=/system/libraries")); - assert!(rewritten.contains("-DCMAKE_INSTALL_PREFIX=/system")); - assert!(!rewritten.contains("$DESTDIR/system/libraries")); - } - - #[test] - fn chapter7_chroot_compiler_search_paths_stay_runtime_absolute() { - let package = BookPackage { - chapter: 7, - section: "7.7".to_string(), - title: "gettext-tiny 0.3.3".to_string(), - name: "gettext-tiny".to_string(), - version: "0.3.3".to_string(), - layer: BASE_LAYER.to_string(), - page_url: "https://example.invalid/chapter7/gettext-tiny.html".to_string(), - recipe_id: "7-7-gettext-tiny".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["gettext-tiny-0.3.3.tar.xz".to_string()], - source_urls: vec!["https://example.invalid/gettext-tiny-0.3.3.tar.xz".to_string()], - extract_dir: Some("gettext-tiny-0.3.3".to_string()), - commands: vec![ - r#"make $LWI_MAKE_FLAGS \ - LIBINTL=musl \ - CPPFLAGS="-I/system/headers" \ - CFLAGS="-I/system/headers" \ - LDFLAGS="-L/system/libraries" \ - CC="cc -B/system/libraries -B/system/libraries/clang/22/lib/linux""# - .to_string(), - ], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "gettext-tiny".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-lbi-linux-musl", "x86_64"); - - assert!( - build_script - .contains("CC=\"cc -B/system/libraries -B/system/libraries/clang/22/lib/linux\"") - ); - assert!(build_script.contains("LIBINTL=MUSL")); - assert!(!build_script.contains("LIBINTL=musl")); - assert!(!build_script.contains("-B$DESTDIR/system/libraries")); - } - - #[test] - fn rustc_bootstrap_toml_uses_live_chroot_tool_paths() { - let package = BookPackage { - chapter: 8, - section: "8.40".to_string(), - title: "rustc 1.95.0".to_string(), - name: "rustc".to_string(), - version: "1.95.0".to_string(), - layer: DEVEL_LAYER.to_string(), - page_url: "https://example.invalid/chapter8/rustc.html".to_string(), - recipe_id: "8-40-rustc".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["rustc-1.95.0-src.tar.xz".to_string()], - source_urls: vec![ - "https://static.rust-lang.org/dist/rustc-1.95.0-src.tar.xz".to_string(), - ], - extract_dir: Some("rustc-1.95.0-src".to_string()), - commands: vec![ - r#"cat > bootstrap.toml <> /etc/group")); - assert!(build_script.contains("\"$DESTDIR/system/systembinaries/grpconv\" -R /")); - assert!( - build_script.contains("\"$DESTDIR/system/systembinaries/useradd\" -D -R / --gid 999") - ); - assert!(build_script.contains("\"$DESTDIR/system/binaries/passwd\" -R / -d root")); - assert!(!build_script.contains("passwd\" -R / root")); - } - - #[test] - fn generated_cross_runtime_commands_use_sysroot_toolchain() { - let package = BookPackage { - chapter: 5, - section: "5.5".to_string(), - title: "LLVM runtimes (libunwind, libcxxabi, libcxx) 22.1.3".to_string(), - name: "llvm-runtimes".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-runtimes.html".to_string(), - recipe_id: "5-5-llvm-runtimes".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["llvm-project-22.1.3.src.tar.xz".to_string()], - source_urls: vec!["https://example.invalid/llvm-project-22.1.3.src.tar.xz".to_string()], - extract_dir: Some("llvm-project-22.1.3.src".to_string()), - commands: vec![ - r#"lbi_cmake build-runtimes \ - -DCMAKE_C_COMPILER="$LBI_ROOT/system/tools/bin/$LBI_TARGET-clang" \ - -DCMAKE_CXX_COMPILER="$LBI_ROOT/system/tools/bin/$LBI_TARGET-clang++" \ - -DCMAKE_SYSROOT="$LBI_ROOT" \ - -DCMAKE_FIND_ROOT_PATH="$LBI_ROOT;$LBI_ROOT/system" \ - -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ - -DLLVM_ENABLE_RUNTIMES="libunwind;libcxxabi;libcxx""# - .to_string(), - ], - dependencies: Vec::new(), - license: "Apache-2.0".to_string(), - description: "LLVM runtimes".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(build_script.contains("-DCMAKE_C_COMPILER=\"$CC\"")); - assert!(build_script.contains("-DCMAKE_CXX_COMPILER=\"$CXX\"")); - assert!(build_script.contains("-DCMAKE_SYSROOT=\"$LBI_SYSROOT\"")); - assert!( - build_script.contains("-DCMAKE_FIND_ROOT_PATH=\"$LBI_SYSROOT;$LBI_SYSROOT/system\"") - ); - assert!(build_script.contains("-DCMAKE_SHARED_LINKER_FLAGS=\"-nostartfiles\"")); - assert!(build_script.contains("-DCMAKE_MODULE_LINKER_FLAGS=\"-nostartfiles\"")); - assert!(!build_script.contains("destdir/llvm-runtimes/system/tools")); - } - - #[test] - fn llvm_clang_pass2_generated_script_uses_ccache() { - let package = BookPackage { - chapter: 5, - section: "5.6".to_string(), - title: "llvm/clang pass 2 22.1.3".to_string(), - name: "llvm-clang-pass2".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-clang-pass2.html".to_string(), - recipe_id: "5-6-llvm-clang-pass2".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["llvm-project-22.1.3.src.tar.xz".to_string()], - source_urls: vec!["https://example.invalid/llvm-project-22.1.3.src.tar.xz".to_string()], - extract_dir: Some("llvm-project-22.1.3.src".to_string()), - commands: vec![ - r#"cmake -G Ninja "../llvm" \ - -DCMAKE_INSTALL_PREFIX=$LBI_ROOT/system/tools \ - -DLLVM_NATIVE_TOOL_DIR="$LBI_ROOT/system/tools/bin" \ - -DLLVM_ENABLE_RUNTIMES="compiler-rt""# - .to_string(), - "ninja".to_string(), - ], - dependencies: Vec::new(), - license: "Apache-2.0".to_string(), - description: "LLVM pass2".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(build_script.contains("export LBI_CCACHE")); - assert!(build_script.contains( - "cmake -G Ninja \"../llvm\" \\\n -DCMAKE_C_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\" - )); - assert!(build_script.contains("-DCMAKE_CXX_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); - assert!(build_script.contains("-DCMAKE_ASM_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); - assert!(!build_script.contains("-DLLVM_CCACHE_BUILD=ON")); - assert!(build_script.contains("-DLLVM_NATIVE_TOOL_DIR=\"$LBI_SYSROOT/system/tools/bin\"")); - assert!( - build_script - .contains("-DLLVM_CONFIG_PATH=\"$LBI_SYSROOT/system/tools/bin/llvm-config\"") - ); - assert!(build_script.contains("-DLLVM_ENABLE_RUNTIMES=\"compiler-rt\"")); - } - - #[test] - fn llvm_clang_pass2_generated_script_fixes_clang_resource_layout() { - let package = BookPackage { - chapter: 6, - section: "6.22".to_string(), - title: "llvm/clang pass 2 22.1.3".to_string(), - name: "llvm-clang-pass2".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/llvm-clang-pass2.html".to_string(), - recipe_id: "6-22-llvm-clang-pass2".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["llvm-project-22.1.3.src.tar.xz".to_string()], - source_urls: vec!["https://example.invalid/llvm-project-22.1.3.src.tar.xz".to_string()], - extract_dir: Some("llvm-project-22.1.3.src".to_string()), - commands: vec![ - r#"DESTDIR="$LBI_ROOT" cmake --install build-compiler-rt-pass2 - -mkdir -p "$LBI_ROOT/system/lib/clang/22/lib/$LBI_TARGET" - -if [ -f "$LBI_ROOT/system/tools/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" ]; then - ln -sf "/system/tools/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" \ - "$LBI_ROOT/system/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" -fi - -CRTBEGIN_OBJ=$(find "$LBI_ROOT/system/libraries/clang" \ - -type f \( -name 'crtbeginS.o' -o -name 'clang_rt.crtbegin*.o' \) | head -n1) -CRTEND_OBJ=$(find "$LBI_ROOT/system/libraries/clang" \ - -type f \( -name 'crtendS.o' -o -name 'clang_rt.crtend*.o' \) | head -n1) - -CRT_DIR=$(dirname "$CRTBEGIN_OBJ") - -if [ -n "$CRTBEGIN_OBJ" ] && [ -n "$CRTEND_OBJ" ]; then - ln -sf "$(basename "$CRTBEGIN_OBJ")" "$CRT_DIR/crtbeginS.o" - ln -sf "$(basename "$CRTEND_OBJ")" "$CRT_DIR/crtendS.o" - - ln -sf "${CRTBEGIN_OBJ#$LBI_ROOT/system}" "$LBI_ROOT/system/libraries/crtbeginS.o" - ln -sf "${CRTEND_OBJ#$LBI_ROOT/system}" "$LBI_ROOT/system/libraries/crtendS.o" -fi"# - .to_string(), - ], - dependencies: Vec::new(), - license: "Apache-2.0".to_string(), - description: "LLVM pass2".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-lbi-linux-musl", "x86_64"); - - assert!( - build_script.contains("builtins_name=\"libclang_rt.builtins-${compiler_rt_arch}.a\"") - ); - assert!(build_script.contains("\"$LBI_ROOT/system/libraries/clang/22/lib/$LBI_TARGET\"")); - assert!(build_script.contains("ln -sf \"../linux/$builtins_name\"")); - assert!(build_script.contains("\"$LBI_ROOT/system/lib/clang\"")); - assert!(build_script.contains("rm -rf \"$LBI_ROOT/system/lib/clang/22\"")); - assert!(!build_script.contains("\"$LBI_ROOT/system/lib/clang/22/lib/linux\"")); - assert!(!build_script.contains( - "cp -R \"$LBI_ROOT/system/libraries/clang/22/.\" \"$LBI_ROOT/system/lib/clang/22/\"" - )); - assert!(build_script.contains("2>/dev/null || true; } | head -n1")); - assert!(build_script.contains( - "install -m644 \"$CRTBEGIN_OBJ\" \"$LBI_ROOT/system/libraries/crtbeginS.o\"" - )); - assert!( - !build_script.contains( - "$DESTDIR/system/tools/lib/clang/22/lib/$LBI_TARGET/libclang_rt.builtins.a" - ) - ); - assert!( - !build_script - .contains("CRT_DIR=$(dirname \"$CRTBEGIN_OBJ\")\n\nif [ -n \"$CRTBEGIN_OBJ\"") - ); - } - - #[test] - fn llvm_clang_pass2_driver_configs_keep_runtime_paths() { - let package = BookPackage { - chapter: 6, - section: "6.22".to_string(), - title: "llvm/clang pass 2 22.1.3".to_string(), - name: "llvm-clang-pass2".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/llvm-clang-pass2.html".to_string(), - recipe_id: "6-22-llvm-clang-pass2".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["llvm-project-22.1.3.src.tar.xz".to_string()], - source_urls: vec!["https://example.invalid/llvm-project-22.1.3.src.tar.xz".to_string()], - extract_dir: Some("llvm-project-22.1.3.src".to_string()), - commands: vec![ - r#"cat > "$LBI_ROOT/system/configuration/clang/clang++.cfg" </dev/null".to_string(), - ], - dependencies: Vec::new(), - license: "ISC".to_string(), - description: "mandoc".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(!build_script.contains("MANPAGER=cat man mandoc >/dev/null")); - assert!(build_script.contains( - "MANPATH=\"$DESTDIR/system/documentation/man-pages\" MANPAGER=cat \"$DESTDIR/system/binaries/man\" mandoc >/dev/null" - )); - assert!(build_script.contains( - "$DESTDIR/system/systembinaries/makewhatis $DESTDIR/system/documentation/man-pages" - )); - } - - #[test] - fn chapter6_file_uses_host_magic_compiler() { - let package = BookPackage { - chapter: 6, - section: "6.9".to_string(), - title: "File 5.47".to_string(), - name: "file".to_string(), - version: "5.47".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/file.html".to_string(), - recipe_id: "6-9-file".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["file-5.47.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/file-5.47.tar.gz".to_string()], - extract_dir: Some("file-5.47".to_string()), - commands: vec![ - r#"CC="$LBI_ROOT/system/tools/bin/$LBI_TARGET-clang" \ -lbi_configure \ - --host="$LBI_TARGET""# - .to_string(), - "make $LWI_MAKE_FLAGS".to_string(), - r#"make install DESTDIR="$LBI_ROOT""#.to_string(), - ], - dependencies: Vec::new(), - license: "BSD-2-Clause".to_string(), - description: "file".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(build_script.contains("rm -rf build-host-file")); - assert!(build_script.contains("CC=\"${BUILD_CC:-cc}\" \\")); - assert!(build_script.contains( - "../configure --prefix=\"$PWD/host-tools\" --disable-shared --enable-static --disable-libseccomp" - )); - assert!(build_script.contains("make ${LWI_MAKE_FLAGS:-} -C src file")); - assert!( - build_script - .contains("make $LWI_MAKE_FLAGS FILE_COMPILE=\"$PWD/build-host-file/src/file\"") - ); - assert!(build_script.contains("CC=\"$CC\"")); - assert!(build_script.contains("--host=\"$LBI_TARGET\"")); - } - - #[test] - fn source_manifest_uses_output_names() { - let manifest = parse_source_manifest( - r#" - https://example.invalid/upstream.tar.gz renamed.tar.gz - git+https://example.invalid/project.git#deadbeef project-git.tar.gz - "#, - ); - - assert_eq!(manifest[0].output_name, "renamed.tar.gz"); - assert_eq!(manifest[0].url, "https://example.invalid/upstream.tar.gz"); - assert_eq!(manifest[1].output_name, "project-git.tar.gz"); - assert_eq!( - manifest[1].url, - "https://example.invalid/project.git#deadbeef" - ); - } - - #[test] - fn source_url_matches_manifest_url_filename_for_renamed_generic_archive() { - let package = BookPackage { - chapter: 8, - section: "8.26".to_string(), - title: "BSD-Diffutils stage 2 0.99.0".to_string(), - name: "bsddiffutils".to_string(), - version: "0.99.0".to_string(), - layer: BASE_LAYER.to_string(), - page_url: "https://www.vertexlinux.net/lbi/chapters/chapter8/bsddiffutils-stage2.html" - .to_string(), - recipe_id: "8-26-bsddiffutils".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["bsddiff-34e64c08674b.tar.gz".to_string()], - source_urls: Vec::new(), - extract_dir: None, - commands: vec!["make".to_string()], - dependencies: Vec::new(), - license: "BSD-2-Clause".to_string(), - description: "BSD diff utilities".to_string(), - }; - let manifest = parse_source_manifest( - r#" - https://github.com/other/project/archive/refs/heads/main.zip other-main.zip - git+https://github.com/chimera-linux/bsddiff.git#34e64c08674ba6f96da41075a17be60944e61e33 bsddiff-34e64c08674b.tar.gz - "#, - ); - - let url = source_url_for_input("bsddiff-34e64c08674b.tar.gz", &package, &recipe, &manifest) - .unwrap(); - - assert_eq!( - url, - "https://github.com/chimera-linux/bsddiff.git#34e64c08674ba6f96da41075a17be60944e61e33" - ); - } - - #[test] - fn page_recipe_strips_book_unzip_scaffolding() { - let package = BookPackage { - chapter: 6, - section: "6.8".to_string(), - title: "BSD-Diffutils 0.99.0".to_string(), - name: "bsddiffutils".to_string(), - version: "0.99.0".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/bsddiffutils.html".to_string(), - recipe_id: "6-8-bsddiffutils".to_string(), - }; - let html = r#" -

    BSD diff utilities.

    -

    Input assumption: bsddiff-34e64c08674b.tar.gz

    -

    Extract and Enter the Source Tree

    -
    cd "$LBI_SOURCES"
    -tar -xf bsddiff-34e64c08674b.tar.gz
    -cd bsddiff-34e64c08674b
    -

    Build BSD-Diffutils

    -
    meson compile -C build -j "$(nproc)"
    -

    Licenses:

    • BSD-2-Clause
    - "#; - - let recipe = parse_page_recipe(html, &package).unwrap(); - - assert_eq!(recipe.extract_dir.as_deref(), Some("bsddiff-34e64c08674b")); - assert!( - !recipe - .commands - .iter() - .any(|cmd| cmd.contains("bsddiff-34e64c08674b.tar.gz")) - ); - assert!( - !recipe - .commands - .iter() - .any(|cmd| cmd.trim() == "cd bsddiff-34e64c08674b") - ); - assert!( - recipe - .commands - .iter() - .any(|cmd| cmd.trim() == "meson compile -C build -j \"$(nproc)\"") - ); - } - - #[test] - fn generated_bsddiffutils_supports_meson_helpers_and_compatibility_edits() { - let package = BookPackage { - chapter: 8, - section: "8.26".to_string(), - title: "BSD-Diffutils stage 2 0.99.0".to_string(), - name: "bsddiffutils".to_string(), - version: "0.99.0".to_string(), - layer: BASE_LAYER.to_string(), - page_url: "https://example.invalid/chapter8/bsddiffutils-stage2.html".to_string(), - recipe_id: "8-26-bsddiffutils".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["bsddiff-34e64c08674b.tar.gz".to_string()], - source_urls: vec![ - "git+https://github.com/chimera-linux/bsddiff.git#34e64c08674ba6f96da41075a17be60944e61e33" - .to_string(), - ], - extract_dir: Some("bsddiff-34e64c08674b".to_string()), - commands: vec![ - r#"sed -i \ - "s|'strtonum.c', 'warnc.c', 'xmalloc.c'|'strtonum.c', 'warnc.c', 'xmalloc.c', 'fgetln.c'|" \ - compat/meson.build - -sed -i \ - '/#include /a char *fgetln(FILE *, size_t *);' \ - diff/diff.c - -sed -i \ - '/#include /a char *fgetln(FILE *, size_t *);' \ - diff3/diff3prog.c - -sed -i \ - '/include_directories: \[sysdefs\],/a \ link_with: [libcompat],' \ - diff3/meson.build"# - .to_string(), - "lbi_meson build -Dbuildtype=release".to_string(), - "meson compile -C build -j \"$(nproc)\"".to_string(), - ], - dependencies: vec!["meson".to_string()], - license: "BSD-2-Clause".to_string(), - description: "BSD diff utilities".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(build_script.contains("lbi_meson()")); - assert!(build_script.contains("--libexecdir=/system/systembinaries")); - assert!(build_script.contains("'fgetln.c'")); - assert!(build_script.contains("link_with: [libcompat]")); - assert!(build_script.contains("diff/diff.c > diff/diff.c.new")); - assert!(build_script.contains("diff3/diff3prog.c > diff3/diff3prog.c.new")); - assert!(build_script.contains("diff3/meson.build > diff3/meson.build.new")); - assert!(!build_script.contains("/a char *fgetln(FILE *, size_t *);")); - assert!(!build_script.contains("/a \\ link_with: [libcompat],")); - assert!(build_script.contains("lbi_meson build -Dbuildtype=release")); - assert!(build_script.contains("meson compile -C build -j \"${LWI_MAKE_JOBS}\"")); - assert!(!build_script.contains("$(nproc)")); - } - - fn test_recipe(chapter: u8) -> GeneratedRecipe { - let name = format!("pkg{chapter}"); - GeneratedRecipe { - package: BookPackage { - chapter, - section: format!("{chapter}.1"), - title: format!("Package {chapter} 1.0"), - name: name.clone(), - version: "1.0".to_string(), - layer: layer_for_package(chapter, &name), - page_url: format!("https://example.invalid/chapter{chapter}/{name}.html"), - recipe_id: format!("{chapter}-1-{name}"), - }, - spec_path: PathBuf::from(format!("/tmp/{name}.toml")), - progress_path: PathBuf::from(format!("/tmp/{name}.done")), - } - } - - #[cfg(unix)] - #[test] - fn bootstrap_completion_accepts_recorded_symlink_payloads() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - let mut recipe = test_recipe(8); - recipe.package.name = "shadow".to_string(); - recipe.progress_path = tmp.path().join("shadow.done"); - - fs::create_dir_all(recipe.progress_path.parent().unwrap()).unwrap(); - fs::write(&recipe.progress_path, "package = \"shadow\"\n").unwrap(); - fs::create_dir_all(sysroot.join("system/systembinaries")).unwrap(); - std::os::unix::fs::symlink("vipw", sysroot.join("system/systembinaries/vigr")).unwrap(); - - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - drop(rusqlite::Connection::open(&db_path).unwrap()); - crate::db::get_all_replaces(&db_path).unwrap(); - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute( - "INSERT INTO packages (name, version, revision) VALUES ('shadow', '1.0', 1)", - [], - ) - .unwrap(); - conn.execute( - "INSERT INTO files (package_id, path) SELECT id, 'system/systembinaries/vigr' FROM packages WHERE name = 'shadow'", - [], - ) - .unwrap(); - - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn bsddiffutils_prepare_hands_off_sbase_cmp_files() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - fs::create_dir_all(sysroot.join("system/binaries")).unwrap(); - fs::create_dir_all(sysroot.join("system/documentation/man-pages/man1")).unwrap(); - fs::write(sysroot.join("system/binaries/cmp"), "sbase cmp").unwrap(); - fs::write( - sysroot.join("system/documentation/man-pages/man1/cmp.1"), - "sbase cmp man", - ) - .unwrap(); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - INSERT INTO packages (id, name) VALUES (1, 'sbase'), (2, 'other'); - INSERT INTO files (package_id, path) VALUES - (1, 'system/binaries/cmp'), - (1, 'system/documentation/man-pages/man1/cmp.1'), - (1, 'system/binaries/cat'), - (2, 'system/binaries/keep'); - "#, - ) - .unwrap(); - - let package = BookPackage { - chapter: 6, - section: "6.8".to_string(), - title: "BSD-Diffutils 0.99.0".to_string(), - name: "bsddiffutils".to_string(), - version: "0.99.0".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/bsddiffutils.html".to_string(), - recipe_id: "6-8-bsddiffutils".to_string(), - }; - - prepare_bootstrap_package_install(sysroot, &config, &package).unwrap(); - - assert!(!sysroot.join("system/binaries/cmp").exists()); - assert!( - !sysroot - .join("system/documentation/man-pages/man1/cmp.1") - .exists() - ); - let remaining_sbase_files: Vec = conn - .prepare( - "SELECT f.path FROM files f JOIN packages p ON p.id = f.package_id WHERE p.name = 'sbase' ORDER BY f.path", - ) - .unwrap() - .query_map([], |row| row.get(0)) - .unwrap() - .collect::>() - .unwrap(); - assert_eq!(remaining_sbase_files, vec!["system/binaries/cat"]); - } - - #[test] - fn bsdgrep_prepare_hands_off_sbase_grep_files() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - fs::create_dir_all(sysroot.join("system/binaries")).unwrap(); - fs::create_dir_all(sysroot.join("system/documentation/man-pages/man1")).unwrap(); - fs::write(sysroot.join("system/binaries/grep"), "sbase grep").unwrap(); - fs::write( - sysroot.join("system/documentation/man-pages/man1/grep.1"), - "sbase grep man", - ) - .unwrap(); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - INSERT INTO packages (id, name) VALUES (1, 'sbase'), (2, 'other'); - INSERT INTO files (package_id, path) VALUES - (1, 'system/binaries/grep'), - (1, 'system/documentation/man-pages/man1/grep.1'), - (1, 'system/binaries/cat'), - (2, 'system/binaries/keep'); - "#, - ) - .unwrap(); - - let package = BookPackage { - chapter: 6, - section: "6.10".to_string(), - title: "bsdgrep master snapshot".to_string(), - name: "bsdgrep".to_string(), - version: "master".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/bsdgrep.html".to_string(), - recipe_id: "6-10-bsdgrep".to_string(), - }; - - prepare_bootstrap_package_install(sysroot, &config, &package).unwrap(); - - assert!(!sysroot.join("system/binaries/grep").exists()); - assert!( - !sysroot - .join("system/documentation/man-pages/man1/grep.1") - .exists() - ); - let remaining_sbase_files: Vec = conn - .prepare( - "SELECT f.path FROM files f JOIN packages p ON p.id = f.package_id WHERE p.name = 'sbase' ORDER BY f.path", - ) - .unwrap() - .query_map([], |row| row.get(0)) - .unwrap() - .collect::>() - .unwrap(); - assert_eq!(remaining_sbase_files, vec!["system/binaries/cat"]); - } - - #[test] - fn bootstrap_package_is_complete_detects_missing_payload_files() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - - let recipe = GeneratedRecipe { - package: BookPackage { - chapter: 6, - section: "6.17".to_string(), - title: "GNU Make 4.4.1".to_string(), - name: "make".to_string(), - version: "4.4.1".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter6/make.html".to_string(), - recipe_id: "6-17-make".to_string(), - }, - spec_path: tmp.path().join("make.toml"), - progress_path: tmp.path().join("make.done"), - }; - fs::write(&recipe.progress_path, "done").unwrap(); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - INSERT INTO packages (id, name) VALUES (1, 'make'); - INSERT INTO files (package_id, path) VALUES - (1, 'system/binaries/make'), - (1, 'system/documentation/info/dir'); - "#, - ) - .unwrap(); - - assert!(!bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - - fs::create_dir_all(sysroot.join("system/binaries")).unwrap(); - fs::write(sysroot.join("system/binaries/make"), "binary").unwrap(); - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn bootstrap_package_is_complete_requires_bmake_payload() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - - let recipe = GeneratedRecipe { - package: BookPackage { - chapter: 8, - section: "8.20".to_string(), - title: "bmake 20260406".to_string(), - name: "bmake".to_string(), - version: "20260406".to_string(), - layer: DEVEL_LAYER.to_string(), - page_url: "https://example.invalid/chapter8/bmake.html".to_string(), - recipe_id: "8-20-bmake".to_string(), - }, - spec_path: tmp.path().join("bmake.toml"), - progress_path: tmp.path().join("bmake.done"), - }; - fs::write(&recipe.progress_path, "recipe_revision = 2\n").unwrap(); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - INSERT INTO packages (id, name) VALUES (1, 'bmake'); - INSERT INTO files (package_id, path) VALUES - (1, 'system/share/licenses/bmake/COPYING'); - "#, - ) - .unwrap(); - fs::create_dir_all(sysroot.join("system/share/licenses/bmake")).unwrap(); - fs::write( - sysroot.join("system/share/licenses/bmake/COPYING"), - "license", - ) - .unwrap(); - - assert!(!bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - - fs::create_dir_all(sysroot.join("system/binaries")).unwrap(); - fs::create_dir_all(sysroot.join("system/share/mk")).unwrap(); - fs::write(sysroot.join("system/binaries/bmake"), "binary").unwrap(); - fs::write(sysroot.join("system/share/mk/sys.mk"), "mk").unwrap(); - - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn bootstrap_package_is_complete_requires_llvm_pass1_native_tools() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - - let recipe = GeneratedRecipe { - package: BookPackage { - chapter: 5, - section: "5.3".to_string(), - title: "llvm/clang pass 1 22.1.3".to_string(), - name: "llvm-clang-pass1".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-clang-pass1.html".to_string(), - recipe_id: "5-3-llvm-clang-pass1".to_string(), - }, - spec_path: tmp.path().join("llvm-clang-pass1.toml"), - progress_path: tmp.path().join("llvm-clang-pass1.done"), - }; - fs::write(&recipe.progress_path, "done\n").unwrap(); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - INSERT INTO packages (id, name) VALUES (1, 'llvm-clang-pass1'); - INSERT INTO files (package_id, path) VALUES - (1, 'system/tools/bin/clang'); - "#, - ) - .unwrap(); - fs::create_dir_all(sysroot.join("system/tools/bin")).unwrap(); - fs::write(sysroot.join("system/tools/bin/clang"), "binary").unwrap(); - - assert!(!bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - - for tool in ["llvm-config", "llvm-tblgen", "clang-tblgen"] { - fs::write(sysroot.join("system/tools/bin").join(tool), "binary").unwrap(); - } - - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn bootstrap_package_is_complete_reinstalls_stale_bmake_recipe_revision() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - - let recipe = GeneratedRecipe { - package: BookPackage { - chapter: 8, - section: "8.20".to_string(), - title: "bmake 20260406".to_string(), - name: "bmake".to_string(), - version: "20260406".to_string(), - layer: DEVEL_LAYER.to_string(), - page_url: "https://example.invalid/chapter8/bmake.html".to_string(), - recipe_id: "8-20-bmake".to_string(), - }, - spec_path: tmp.path().join("bmake.toml"), - progress_path: tmp.path().join("bmake.done"), - }; - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - INSERT INTO packages (id, name) VALUES (1, 'bmake'); - INSERT INTO files (package_id, path) VALUES - (1, 'system/binaries/bmake'), - (1, 'system/share/mk/sys.mk'); - "#, - ) - .unwrap(); - fs::create_dir_all(sysroot.join("system/binaries")).unwrap(); - fs::create_dir_all(sysroot.join("system/share/mk")).unwrap(); - fs::write(sysroot.join("system/binaries/bmake"), "binary").unwrap(); - fs::write(sysroot.join("system/share/mk/sys.mk"), "mk").unwrap(); - - fs::write(&recipe.progress_path, "done\n").unwrap(); - assert!(!bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - - fs::write(&recipe.progress_path, "recipe_revision = 2\n").unwrap(); - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn bootstrap_package_is_complete_treats_replaced_package_as_done() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let db_path = config.installed_db_path(sysroot); - fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - - let recipe = GeneratedRecipe { - package: BookPackage { - chapter: 5, - section: "5.2".to_string(), - title: "musl libc headers 1.2.6".to_string(), - name: "musl-libc-headers".to_string(), - version: "1.2.6".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/musl-libc-headers.html".to_string(), - recipe_id: "5-2-musl-libc-headers".to_string(), - }, - spec_path: tmp.path().join("musl-libc-headers.toml"), - progress_path: tmp.path().join("musl-libc-headers.done"), - }; - fs::write(&recipe.progress_path, "done").unwrap(); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - conn.execute_batch( - r#" - CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT NOT NULL); - CREATE TABLE files (package_id INTEGER NOT NULL, path TEXT NOT NULL); - CREATE TABLE replaces ( - id INTEGER PRIMARY KEY, - package_id INTEGER NOT NULL, - replaces_name TEXT NOT NULL, - UNIQUE(package_id, replaces_name) - ); - INSERT INTO packages (id, name) VALUES (1, 'musl-libc-pass2'); - INSERT INTO replaces (package_id, replaces_name) VALUES - (1, 'musl-libc-headers'); - "#, - ) - .unwrap(); - - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn bootstrap_package_is_complete_treats_retired_package_as_done() { - let tmp = tempfile::tempdir().unwrap(); - let sysroot = tmp.path(); - let config = config::Config::for_rootfs(sysroot); - let recipe = GeneratedRecipe { - package: BookPackage { - chapter: 5, - section: "5.3".to_string(), - title: "llvm/clang pass 1 22.1.3".to_string(), - name: "llvm-clang-pass1".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-clang-pass1.html".to_string(), - recipe_id: "5-3-llvm-clang-pass1".to_string(), - }, - spec_path: tmp.path().join("llvm-clang-pass1.toml"), - progress_path: tmp.path().join("llvm-clang-pass1.done"), - }; - fs::write(&recipe.progress_path, "done").unwrap(); - let retired_path = retired_package_progress_path(&config, &recipe.package.name); - fs::create_dir_all(retired_path.parent().unwrap()).unwrap(); - fs::write(retired_path, "retired = true\n").unwrap(); - - assert!(bootstrap_package_is_complete(sysroot, &config, &recipe).unwrap()); - } - - #[test] - fn chapter7_boundary_is_detected_before_chapter8() { - let chapter7 = test_recipe(7); - let next_chapter7 = test_recipe(7); - let chapter8 = test_recipe(8); - - assert!(!step_completes_chapter( - &BookStep::Package(chapter7.package.clone()), - Some(&BookStep::Package(next_chapter7.package)), - 7, - )); - assert!(step_completes_chapter( - &BookStep::Package(chapter7.package), - Some(&BookStep::Package(chapter8.package)), - 7, - )); - } - - #[test] - fn temp_layer_filter_removes_chapter7_retired_packages() { - let filtered = filter_retired_layer_packages( - TEMP_LAYER, - vec![ - "llvm-clang-pass1".to_string(), - "make".to_string(), - "musl-libc-headers".to_string(), - ], - ); - - assert_eq!( - filtered, - vec!["make".to_string(), "musl-libc-headers".to_string()] - ); - assert_eq!( - filter_retired_layer_packages(BASE_LAYER, vec!["llvm-clang-pass1".to_string()]), - vec!["llvm-clang-pass1".to_string()] - ); - } - - #[test] - fn chapter6_install_invocation_uses_cross_prefix_and_bootstrap_path() { - let recipe = test_recipe(6); - let invocation = bootstrap_install_invocation( - Path::new("/target"), - &recipe, - BootstrapBuildMode::Cross, - "x86_64-unknown-linux-musl", - Path::new("/bin/depot"), - ) - .unwrap(); - - assert!(invocation.args.windows(2).any(|pair| { - pair[0] == OsStr::new("--cross-prefix") - && pair[1] == OsStr::new("x86_64-unknown-linux-musl") - })); - let path = invocation - .env - .iter() - .find(|(key, _)| key == OsStr::new("PATH")) - .map(|(_, value)| value.to_string_lossy().into_owned()) - .unwrap(); - assert!(path.starts_with("/target/system/tools/bin:")); - assert!( - !path - .split(':') - .any(|entry| entry == "/target/system/binaries") - ); - assert!( - invocation - .env - .iter() - .any(|(key, _)| key == OsStr::new("LWI_MAKE_FLAGS")) - ); - assert!( - invocation - .env - .iter() - .any(|(key, _)| key == OsStr::new("LWI_MAKE_JOBS")) - ); - assert!(invocation.env.iter().any(|(key, value)| { - key == OsStr::new(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS) && value == OsStr::new("1") - })); - assert!(invocation.env.iter().any(|(key, value)| { - key == OsStr::new("DEPOT_LBI_SYSROOT") && value == OsStr::new("/target") - })); - } - - #[test] - fn chapter5_post_pass1_install_invocation_uses_cross_mode() { - let mut recipe = test_recipe(5); - recipe.package.section = "5.4".to_string(); - recipe.package.name = "musl-libc-pass2".to_string(); - recipe.package.recipe_id = "5-4-musl-libc-pass2".to_string(); - - let mode = build_mode_for_package(&recipe.package); - let invocation = bootstrap_install_invocation( - Path::new("/target"), - &recipe, - mode, - "x86_64-unknown-linux-musl", - Path::new("/bin/depot"), - ) - .unwrap(); - - assert_eq!(mode, BootstrapBuildMode::Cross); - assert!(invocation.args.windows(2).any(|pair| { - pair[0] == OsStr::new("--cross-prefix") - && pair[1] == OsStr::new("x86_64-unknown-linux-musl") - })); - let path = invocation - .env - .iter() - .find(|(key, _)| key == OsStr::new("PATH")) - .map(|(_, value)| value.to_string_lossy().into_owned()) - .unwrap(); - assert!(path.starts_with("/target/system/tools/bin:")); - assert!( - !path - .split(':') - .any(|entry| entry == "/target/system/binaries") - ); - } - - #[test] - fn chapter7_and_8_install_invocations_use_chroot_mode() { - let recipe = test_recipe(8); - let invocation = bootstrap_install_invocation( - Path::new("/target"), - &recipe, - BootstrapBuildMode::Chroot, - "x86_64-unknown-linux-musl", - Path::new("/bin/depot"), - ) - .unwrap(); - - assert!( - !invocation - .args - .iter() - .any(|arg| arg == OsStr::new("--cross-prefix")) - ); - assert!(invocation.env.iter().any(|(key, value)| { - key == OsStr::new("DEPOT_LBI_CHROOT") && value == OsStr::new("1") - })); - assert!(invocation.env.iter().any(|(key, value)| { - key == OsStr::new("DEPOT_LBI_CHROOT_ROOT") && value == OsStr::new("/target") - })); - } - - #[test] - fn bootstrap_chroot_tool_env_prefers_target_tools() { - let env = bootstrap_chroot_tool_env(); - assert!( - env.iter() - .any(|(key, value)| *key == "AR" && *value == "ar") - ); - assert!( - env.iter() - .any(|(key, value)| *key == "RANLIB" && *value == "ranlib") - ); - assert!(env.iter().any(|(key, value)| { - *key == "PATH" && value.split(':').any(|entry| entry == "/system/binaries") - })); - assert!(env.iter().any(|(key, value)| { - *key == "PATH" - && value - .split(':') - .any(|entry| entry == BOOTSTRAP_CHROOT_SHIM_DIR) - })); - } - - #[test] - fn bootstrap_chroot_mount_guard_cleans_created_file_targets() { - let tmp = tempfile::tempdir().unwrap(); - let target = tmp.path().join("etc/resolv.conf"); - - { - let mut guard = BootstrapChrootMountGuard::default(); - guard.prepare_file_mount_target(&target).unwrap(); - assert!(target.is_file()); - } - - assert!(!target.exists()); - } - - #[test] - fn bootstrap_makeflags_prefers_explicit_env_override() { - let mut env = TestEnv::new(); - env.set_var("LWI_MAKE_FLAGS", "-j37 --output-sync=target"); - assert_eq!( - bootstrap_parallel_makeflags(), - "-j37 --output-sync=target".to_string() - ); - } - - #[test] - fn bootstrap_make_jobs_prefers_explicit_env_override() { - let mut env = TestEnv::new(); - env.set_var("LWI_MAKE_JOBS", "37"); - assert_eq!(bootstrap_parallel_make_jobs(), "37"); - } - - #[test] - fn generated_recipe_passthrough_exports_lwi_parallel_env() { - let tmp = tempfile::tempdir().unwrap(); - let spec_path = tmp.path().join("pkg.toml"); - let build_path = tmp.path().join("build.sh"); - let package = test_recipe(7).package; - let recipe = PageRecipe { - input_files: vec!["pkg-1.0.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/pkg-1.0.tar.gz".to_string()], - extract_dir: None, - commands: vec!["make".to_string()], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "Example package".to_string(), - }; - - write_generated_recipe( - &spec_path, - &build_path, - &package, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - - let spec = fs::read_to_string(spec_path).unwrap(); - assert!(spec.contains("\"LWI_MAKE_FLAGS\"")); - assert!(spec.contains("\"LWI_MAKE_JOBS\"")); - assert!(spec.contains("\"DEPOT_LBI_SYSROOT\"")); - } - - #[test] - fn generated_recipe_preserves_static_archives_only_for_toolchain_packages() { - let tmp = tempfile::tempdir().unwrap(); - let recipe = PageRecipe { - input_files: vec!["pkg-1.0.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/pkg-1.0.tar.gz".to_string()], - extract_dir: None, - commands: vec!["make".to_string()], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "Example package".to_string(), - }; - - let names = [ - ("llvm-clang-pass2", true), - ("llvm", true), - ("musl-libc-pass2", true), - ("musl", true), - ("rustc", true), - ("bmake", false), - ]; - - for (idx, (name, should_preserve)) in names.into_iter().enumerate() { - let mut package = test_recipe(8).package; - package.name = name.to_string(); - package.title = format!("{name} 1.0"); - package.recipe_id = format!("8-{idx}-{name}"); - let spec_path = tmp.path().join(format!("{name}.toml")); - write_generated_recipe( - &spec_path, - &tmp.path().join(format!("{name}-build.sh")), - &package, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - - let spec = fs::read_to_string(spec_path).unwrap(); - assert_eq!( - spec.contains("no_delete_static = true"), - should_preserve, - "{name} static archive cleanup policy should match" - ); - } - } - - #[test] - fn generated_recipe_uses_lbi_blake2_source_checksums() { - let tmp = tempfile::tempdir().unwrap(); - let spec_path = tmp.path().join("pkg.toml"); - let build_path = tmp.path().join("build.sh"); - let package = test_recipe(7).package; - let recipe = PageRecipe { - input_files: vec!["pkg-1.0.tar.gz".to_string(), "pkg-fix.patch".to_string()], - source_urls: vec![ - "https://example.invalid/pkg-1.0.tar.gz".to_string(), - "https://example.invalid/patches/pkg-fix.patch".to_string(), - ], - extract_dir: None, - commands: vec!["make".to_string()], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "Example package".to_string(), - }; - let manifest = SourceManifest { - entries: Vec::new(), - blake2b_512: BTreeMap::from([ - ( - "pkg-1.0.tar.gz".to_string(), - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), - ), - ( - "pkg-fix.patch".to_string(), - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), - ), - ]), - }; - - write_generated_recipe( - &spec_path, - &build_path, - &package, - &recipe, - &manifest, - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - - let spec = fs::read_to_string(spec_path).unwrap(); - assert!(spec.contains("sha256 = \"b2sum:aaaaaaaa")); - assert!(spec.contains("sha256 = \"b2sum:bbbbbbbb")); - assert!(!spec.contains("sha256 = \"skip\"")); - } - - #[test] - fn generated_specs_record_bootstrap_stage_replacements() { - let tmp = tempfile::tempdir().unwrap(); - let recipe = PageRecipe { - input_files: vec!["pkg-1.0.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/pkg-1.0.tar.gz".to_string()], - extract_dir: None, - commands: vec!["make".to_string()], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "Example package".to_string(), - }; - - let mut musl_pass2 = test_recipe(5).package; - musl_pass2.name = "musl-libc-pass2".to_string(); - musl_pass2.title = "musl libc pass 2 1.2.6".to_string(); - let musl_pass2_spec = tmp.path().join("musl-pass2.toml"); - write_generated_recipe( - &musl_pass2_spec, - &tmp.path().join("musl-pass2-build.sh"), - &musl_pass2, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - let musl_pass2_spec = fs::read_to_string(musl_pass2_spec).unwrap(); - assert!(musl_pass2_spec.contains("replaces = [\"musl-libc-headers\"]")); - - let mut musl_final = test_recipe(8).package; - musl_final.name = "musl".to_string(); - musl_final.title = "musl libc final pass 1.2.6".to_string(); - let musl_final_spec = tmp.path().join("musl-final.toml"); - write_generated_recipe( - &musl_final_spec, - &tmp.path().join("musl-final-build.sh"), - &musl_final, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - let musl_final_spec = fs::read_to_string(musl_final_spec).unwrap(); - assert!( - musl_final_spec - .contains("replaces = [\"musl-libc-pass2\", \"musl-libc-headers\", \"musl-libc\"]") - ); - - let mut llvm_pass2 = test_recipe(6).package; - llvm_pass2.name = "llvm-clang-pass2".to_string(); - llvm_pass2.title = "llvm/clang pass 2 22.1.3".to_string(); - let llvm_pass2_spec = tmp.path().join("llvm-pass2.toml"); - write_generated_recipe( - &llvm_pass2_spec, - &tmp.path().join("llvm-pass2-build.sh"), - &llvm_pass2, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - let llvm_pass2_spec = fs::read_to_string(llvm_pass2_spec).unwrap(); - assert!(llvm_pass2_spec.contains("replaces = [\"llvm-runtimes\"]")); - assert!(!llvm_pass2_spec.contains("llvm-clang-pass1")); - - let mut llvm_final = test_recipe(8).package; - llvm_final.name = "llvm".to_string(); - llvm_final.title = "llvm final 22.1.3".to_string(); - let llvm_final_spec = tmp.path().join("llvm-final.toml"); - write_generated_recipe( - &llvm_final_spec, - &tmp.path().join("llvm-final-build.sh"), - &llvm_final, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - let llvm_final_spec = fs::read_to_string(llvm_final_spec).unwrap(); - assert!(llvm_final_spec.contains( - "replaces = [\"llvm-clang-pass2\", \"llvm-clang-pass1\", \"llvm-runtimes\"]" - )); - } - - #[test] - fn byacc_generated_script_exposes_unprefixed_yacc() { - let mut package = test_recipe(8).package; - package.name = "byacc".to_string(); - package.title = "byacc stage 2 20260126".to_string(); - package.version = "20260126".to_string(); - package.recipe_id = "8-12-byacc".to_string(); - let recipe = PageRecipe { - input_files: vec!["byacc-20260126.tgz".to_string()], - source_urls: vec!["https://example.invalid/byacc-20260126.tgz".to_string()], - extract_dir: Some("byacc-20260126".to_string()), - commands: vec![ - "lbi_configure --with-manpage-format=normal".to_string(), - "make $LWI_MAKE_FLAGS".to_string(), - "make ${LWI_MAKE_FLAGS:-} install".to_string(), - ], - dependencies: Vec::new(), - license: "BSD-3-Clause".to_string(), - description: "byacc".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-lbi-linux-musl", "x86_64"); - - assert!( - build_script.contains(r#"ln -sf "$LBI_TARGET-yacc" "$DESTDIR/system/binaries/yacc""#) - ); - assert!(build_script.contains( - r#"ln -sf "$LBI_TARGET-yacc.1" "$DESTDIR/system/documentation/man-pages/man1/yacc.1""# - )); - assert_eq!( - bootstrap_required_payload_paths("byacc"), - &["system/binaries/yacc"] - ); - assert_eq!(bootstrap_recipe_revision("byacc"), Some(1)); - } - - #[test] - fn generated_python_bootstrap_specs_use_custom_chroot_script() { - let tmp = tempfile::tempdir().unwrap(); - let recipe = PageRecipe { - input_files: vec!["flit_core-3.12.0.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/flit_core-3.12.0.tar.gz".to_string()], - extract_dir: Some("flit_core-3.12.0".to_string()), - commands: vec![ - "pip3 wheel -w dist --no-cache-dir --no-build-isolation --no-deps $PWD" - .to_string(), - "pip3 install --root=\"${DESTDIR:-/}\" --prefix=/system --no-index --find-links dist flit_core" - .to_string(), - ], - dependencies: vec!["python".to_string(), "pip".to_string()], - license: "BSD-3-Clause".to_string(), - description: "Build and install flit_core.".to_string(), - }; - - let mut package = test_recipe(8).package; - package.name = "python-flit-core".to_string(); - package.title = "Python-Flit-Core 3.12.0".to_string(); - package.version = "3.12.0".to_string(); - package.recipe_id = "8-19-python-flit-core".to_string(); - let spec_path = tmp.path().join("python-flit-core.toml"); - let build_path = tmp.path().join("build.sh"); - write_generated_recipe( - &spec_path, - &build_path, - &package, - &recipe, - &SourceManifest::default(), - "x86_64-lbi-linux-musl", - "x86_64", - ) - .unwrap(); - - let spec = fs::read_to_string(spec_path).unwrap(); - let build_script = fs::read_to_string(build_path).unwrap(); - assert!(spec.contains("type = \"custom\"")); - assert!(!spec.contains("type = \"python\"")); - assert!(spec.contains("DEPOT_LBI_CHROOT")); - assert!(build_script.contains("internal bootstrap-chroot")); - assert!(build_script.contains("pip3 wheel -w dist")); - } - - #[test] - fn llvm_clang_pass1_generated_script_preserves_compiler_rt_builtins() { - let tmp = tempfile::tempdir().unwrap(); - let spec_path = tmp.path().join("llvm-clang-pass1.toml"); - let build_path = tmp.path().join("build.sh"); - let package = BookPackage { - chapter: 5, - section: "5.3".to_string(), - title: "llvm/clang pass 1 22.1.3".to_string(), - name: "llvm-clang-pass1".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-clang-pass1.html".to_string(), - recipe_id: "5-3-llvm-clang-pass1".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["llvm-project-22.1.3.src.tar.xz".to_string()], - source_urls: vec!["https://example.invalid/llvm-project-22.1.3.src.tar.xz".to_string()], - extract_dir: Some("llvm-project-22.1.3.src".to_string()), - commands: vec![ - "mkdir -p build-llvm\ncd build-llvm".to_string(), - r#"cmake -G Ninja "../llvm" \ - -DCMAKE_INSTALL_PREFIX=$LBI_ROOT/system/tools \ - -DLLVM_ENABLE_RUNTIMES="compiler-rt" \ - -DCOMPILER_RT_BUILD_BUILTINS=ON \ - -DCOMPILER_RT_BUILD_SANITIZERS=OFF \ - -DCOMPILER_RT_BUILD_XRAY=OFF \ - -DCOMPILER_RT_BUILD_LIBFUZZER=OFF \ - -DCOMPILER_RT_BUILD_PROFILE=OFF \ - -DCLANG_DEFAULT_RTLIB=compiler-rt \ - -DDEFAULT_SYSROOT=$LBI_ROOT"# - .to_string(), - ], - dependencies: Vec::new(), - license: "Apache-2.0".to_string(), - description: "LLVM pass1".to_string(), - }; - - write_generated_recipe( - &spec_path, - &build_path, - &package, - &recipe, - &SourceManifest::default(), - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - - let build_script = fs::read_to_string(build_path).unwrap(); - assert!(build_script.contains("export LBI_SYSROOT=\"${DEPOT_LBI_SYSROOT:-$LBI_ROOT}\"")); - assert!(build_script.contains("export LWI_CXXFLAGS=\"${LWI_CXXFLAGS:-$LWI_CFLAGS}\"")); - assert!(build_script.contains("rm -rf build-llvm")); - assert!(build_script.contains("LBI_CCACHE=\"$(command -v ccache)\"")); - assert!(build_script.contains( - "cmake -G Ninja \"../llvm\" \\\n -DCMAKE_C_COMPILER_LAUNCHER=\"$LBI_CCACHE\" \\" - )); - assert!(build_script.contains("-DCMAKE_CXX_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); - assert!(build_script.contains("-DCMAKE_ASM_COMPILER_LAUNCHER=\"$LBI_CCACHE\"")); - assert!(!build_script.contains("-DLLVM_CCACHE_BUILD=ON")); - assert!(build_script.contains("LLVM_ENABLE_RUNTIMES=\"compiler-rt\"")); - assert!(build_script.contains("COMPILER_RT_BUILD_BUILTINS=ON")); - assert!(build_script.contains("COMPILER_RT_DEFAULT_TARGET_ONLY=ON")); - assert!( - build_script.contains( - "BUILTINS_CMAKE_ARGS=\"-DCMAKE_C_FLAGS=--sysroot=$LBI_SYSROOT;-DCMAKE_ASM_FLAGS=--sysroot=$LBI_SYSROOT\"" - ) - ); - assert!(build_script.contains("COMPILER_RT_BUILD_SANITIZERS=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_XRAY=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_LIBFUZZER=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_PROFILE=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_CRT=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_MEMPROF=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_ORC=OFF")); - assert!(build_script.contains("COMPILER_RT_BUILD_CTX_PROFILE=OFF")); - assert!(build_script.contains("COMPILER_RT_INCLUDE_TESTS=OFF")); - assert!(build_script.contains("CLANG_DEFAULT_RTLIB=compiler-rt")); - assert!(build_script.contains("-DDEFAULT_SYSROOT=$LBI_SYSROOT")); - assert!(build_script.contains("unset DESTDIR")); - assert!(build_script.contains("-DCMAKE_INSTALL_PREFIX=$LBI_ROOT/system/tools")); - assert!( - !build_script - .contains("export CC=\"${CC:-$LBI_SYSROOT/system/tools/bin/$LBI_TARGET-clang}\"") - ); - } - - #[test] - fn chapter5_post_pass1_scripts_default_to_cross_toolchain() { - let package = BookPackage { - chapter: 5, - section: "5.4".to_string(), - title: "musl libc pass 2 1.2.6".to_string(), - name: "musl-libc-pass2".to_string(), - version: "1.2.6".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/musl-libc-pass2.html".to_string(), - recipe_id: "5-4-musl-libc-pass2".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["musl-1.2.6.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/musl-1.2.6.tar.gz".to_string()], - extract_dir: Some("musl-1.2.6".to_string()), - commands: vec![ - "lbi_configure --target=\"$LBI_TARGET\" --with-malloc=mimalloc\nmake $LWI_MAKE_FLAGS".to_string(), - "ln -snf ./libc.so \\\n \"$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1\"\n\nls -lh \"$LBI_ROOT/usr/lib/ld-musl-${LBI_ARCH}.so.1\"".to_string(), - ], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "musl pass2".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert_eq!(build_mode_for_package(&package), BootstrapBuildMode::Cross); - assert!(build_script.contains("export CC=\"${CC:-$(lbi_find_cross_tool clang)}\"")); - assert!(build_script.contains("export CXX=\"${CXX:-$(lbi_find_cross_tool clang++)}\"")); - assert!(build_script.contains("libclang_rt.builtins-${compiler_rt_arch}.a")); - assert!(build_script.contains("export LIBCC")); - assert!(build_script.contains("make $LWI_MAKE_FLAGS")); - assert!(!build_script.contains("make ${LWI_MAKE_FLAGS:-} $LWI_MAKE_FLAGS")); - assert!( - build_script.contains("ls -lh \"$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1\"") - ); - assert!(!build_script.contains("$LBI_ROOT/usr/lib/ld-musl-${LBI_ARCH}.so.1")); - assert!(!build_script.contains("ln -snf")); - assert!( - build_script.contains("rm -f \"$LBI_ROOT/system/libraries/ld-musl-${LBI_ARCH}.so.1\"") - ); - assert!(build_script.contains("ln -sf ./libc.so")); - assert!(build_script.contains("rm -f \"$LBI_ROOT/lib/ld-musl-${LBI_ARCH}.so.1\"")); - assert!(build_script.contains("rmdir \"$LBI_ROOT/lib\" 2>/dev/null || true")); - assert!(build_script.contains("export LWI_MAKE_JOBS=\"$jobs\"")); - assert!(build_script.contains("export LWI_MAKE_FLAGS=\"-j${LWI_MAKE_JOBS}\"")); - - let final_package = BookPackage { - chapter: 8, - section: "8.6".to_string(), - title: "musl libc final pass 1.2.6".to_string(), - name: "musl".to_string(), - version: "1.2.6".to_string(), - layer: BASE_LAYER.to_string(), - page_url: "https://example.invalid/chapter8/musl-libc-final-pass.html".to_string(), - recipe_id: "8-6-musl".to_string(), - }; - let final_script = generated_build_script( - &final_package, - &recipe, - "x86_64-unknown-linux-musl", - "x86_64", - ); - assert!(!final_script.contains("ln -snf")); - assert!(final_script.contains("ln -sf ./libc.so")); - } - - #[test] - fn lbi_helpers_default_to_target_tuple() { - let package = BookPackage { - chapter: 8, - section: "8.99".to_string(), - title: "Example 1.0".to_string(), - name: "example".to_string(), - version: "1.0".to_string(), - layer: BASE_LAYER.to_string(), - page_url: "https://example.invalid/chapter8/example.html".to_string(), - recipe_id: "8-99-example".to_string(), - }; - let recipe = PageRecipe { - input_files: vec!["example-1.0.tar.gz".to_string()], - source_urls: vec!["https://example.invalid/example-1.0.tar.gz".to_string()], - extract_dir: Some("example-1.0".to_string()), - commands: vec!["lbi_configure\nmake $LWI_MAKE_FLAGS".to_string()], - dependencies: Vec::new(), - license: "MIT".to_string(), - description: "Example".to_string(), - }; - - let build_script = - generated_build_script(&package, &recipe, "x86_64-unknown-linux-musl", "x86_64"); - - assert!(build_script.contains(" --target=\"$LBI_TARGET\" \\\n")); - assert!(build_script.contains(" --host=\"$LBI_TARGET\" \\\n")); - assert!(!build_script.contains(" --build=\"$LBI_TARGET\" \\\n")); - assert!(build_script.contains("-DCMAKE_C_COMPILER_TARGET=\"$LBI_TARGET\"")); - assert!(build_script.contains("-DCMAKE_CXX_COMPILER_TARGET=\"$LBI_TARGET\"")); - assert!( - build_script - .contains("meson setup \"$build_dir\" --cross-file \"$lbi_meson_cross_file\"") - ); - assert!( - build_script.contains("c_args = ['--target=$LBI_TARGET', '--sysroot=$LBI_SYSROOT']") - ); - assert!( - build_script.contains("cpp_args = ['--target=$LBI_TARGET', '--sysroot=$LBI_SYSROOT']") - ); - } - - #[test] - fn copied_build_profile_does_not_default_build_tuple() { - let tmp = tempfile::tempdir().unwrap(); - - copy_build_profile(tmp.path(), "x86_64-unknown-linux-musl", "x86_64").unwrap(); - - let profile = fs::read_to_string(tmp.path().join("etc/profile.d/lbi-build.sh")).unwrap(); - assert!(profile.contains(" --target=\"$LBI_TARGET\" \\\n")); - assert!(profile.contains(" --host=\"$LBI_TARGET\" \\\n")); - assert!(!profile.contains(" --build=\"$LBI_TARGET\" \\\n")); - } - - #[test] - fn rewrite_make_flags_does_not_duplicate_existing_parallel_flags() { - assert_eq!( - rewrite_make_flags("make $LWI_MAKE_FLAGS\nmake install"), - "make $LWI_MAKE_FLAGS\nmake ${LWI_MAKE_FLAGS:-} install" - ); - assert_eq!( - rewrite_make_flags("make ${MAKEFLAGS:-} all"), - "make ${MAKEFLAGS:-} all" - ); - } - - #[test] - fn rewrite_parallel_job_counts_uses_bootstrap_job_variable() { - assert_eq!( - rewrite_parallel_job_counts("meson compile -C build -j \"$(nproc)\""), - "meson compile -C build -j \"${LWI_MAKE_JOBS}\"" - ); - assert_eq!( - rewrite_parallel_job_counts("ninja -j$(nproc)"), - "ninja -j${LWI_MAKE_JOBS}" - ); - } - - #[test] - fn rewrite_lbi_command_removes_chroot_nproc_dependency() { - assert_eq!( - rewrite_lbi_command("meson compile -C build -j \"$(nproc)\"", None), - "meson compile -C build -j \"${LWI_MAKE_JOBS}\"" - ); - } - - #[test] - fn cross_toolchain_defaults_start_after_pass1() { - let pre_pass1 = BookPackage { - chapter: 5, - section: "5.2".to_string(), - title: "musl libc headers 1.2.6".to_string(), - name: "musl-libc-headers".to_string(), - version: "1.2.6".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/musl-libc-headers.html".to_string(), - recipe_id: "5-2-musl-libc-headers".to_string(), - }; - let pass1 = BookPackage { - chapter: 5, - section: "5.3".to_string(), - title: "llvm/clang pass 1 22.1.3".to_string(), - name: "llvm-clang-pass1".to_string(), - version: "22.1.3".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/llvm-clang-pass1.html".to_string(), - recipe_id: "5-3-llvm-clang-pass1".to_string(), - }; - let post_pass1 = BookPackage { - chapter: 5, - section: "5.4".to_string(), - title: "example package".to_string(), - name: "example".to_string(), - version: "1.0".to_string(), - layer: TEMP_LAYER.to_string(), - page_url: "https://example.invalid/chapter5/example.html".to_string(), - recipe_id: "5-4-example".to_string(), - }; - - assert!(!use_cross_toolchain_by_default(&pre_pass1)); - assert!(!use_cross_toolchain_by_default(&pass1)); - assert!(use_cross_toolchain_by_default(&post_pass1)); - } - - #[cfg(unix)] - #[test] - fn root_transition_skips_reexec_when_already_root() { - assert_eq!( - root_transition(true, Some(OsStr::new(""))).unwrap(), - RootTransition::AlreadyRoot - ); - } - - #[cfg(unix)] - #[test] - fn root_transition_prefers_sudo_then_doas() { - use std::os::unix::fs::PermissionsExt; - - let tmp = tempfile::tempdir().unwrap(); - let bin = tmp.path().join("bin"); - fs::create_dir_all(&bin).unwrap(); - for name in ["sudo", "doas"] { - let path = bin.join(name); - fs::write(&path, "#!/bin/sh\n").unwrap(); - let mut perms = fs::metadata(&path).unwrap().permissions(); - perms.set_mode(0o755); - fs::set_permissions(path, perms).unwrap(); - } - let action = root_transition(false, Some(bin.as_os_str())).unwrap(); - - assert_eq!(action, RootTransition::Reexec(bin.join("sudo"))); - } - - #[test] - fn root_transition_errors_without_helper() { - let err = root_transition(false, Some(OsStr::new(""))).unwrap_err(); - assert!(err.to_string().contains("neither sudo nor doas")); - } - - #[test] - fn parses_body_style_section_lines() { - assert_eq!( - parse_section_line("5.5. LLVM runtimes (libunwind, libcxxabi, libcxx) 22.1.3"), - Some(( - "5.5".to_string(), - "LLVM runtimes (libunwind, libcxxabi, libcxx) 22.1.3".to_string() - )) - ); - } - - #[test] - fn parses_bulleted_toc_section_lines() { - assert_eq!( - parse_section_line("\u{c} ▪ 8.17 libffi 3.5.2"), - Some(("8.17".to_string(), "libffi 3.5.2".to_string())) - ); - assert_eq!( - parse_section_line(" • 8.23 Meson 1.11.1"), - Some(("8.23".to_string(), "Meson 1.11.1".to_string())) - ); - } - - #[test] - fn skips_chapter_heading_section_lines() { - assert_eq!(parse_section_line("5. Cross-Compilation Setup"), None); - assert_eq!( - parse_section_line("8. Compiling the Remaining utilities for the system"), - None - ); - } - - #[test] - fn normalizes_known_titles() { - assert_eq!( - package_name_from_title("BSD-Diffutils stage 2 0.99.0").as_deref(), - Some("bsddiffutils") - ); - assert_eq!( - package_name_from_title("patch stage 2 0.99.1").as_deref(), - Some("bsdpatch") - ); - assert_eq!( - package_name_from_title("uutils-coreutils 0.8.0").as_deref(), - Some("uutils-coreutils") - ); - assert_eq!( - package_name_from_title("musl libc final pass 1.2.6").as_deref(), - Some("musl") - ); - assert_eq!( - package_name_from_title("LLVM final 22.1.3").as_deref(), - Some("llvm") - ); - assert_eq!( - package_name_from_title("bzip2 1.0.8").as_deref(), - Some("bzip2") - ); - assert_eq!(package_name_from_title("m4 1.4.20").as_deref(), Some("m4")); - assert_eq!(package_name_from_title("."), None); - } - - #[cfg(unix)] - #[test] - fn initializes_lbi_layout_for_fresh_bootstrap() { - let tmp = tempfile::tempdir().unwrap(); - let config = config::Config::for_rootfs(tmp.path()); - - ensure_lbi_layout_for_fresh_bootstrap( - tmp.path(), - &config, - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - - let state = system_state::load(&config).unwrap(); - assert_eq!(state.stage.as_deref(), Some("layout")); - assert!(tmp.path().join("etc/depot.d/build.toml").exists()); - assert!(tmp.path().join("system/binaries").is_dir()); - } - - #[test] - fn essential_system_files_include_lwi_account_defaults() { - let tmp = tempfile::tempdir().unwrap(); - - create_essential_system_files(tmp.path()).unwrap(); - - let passwd = fs::read_to_string(tmp.path().join("etc/passwd")).unwrap(); - let group = fs::read_to_string(tmp.path().join("etc/group")).unwrap(); - assert!(passwd.contains("root:x:0:0:root:/system/charlie:/bin/oksh")); - assert!(passwd.contains("messagebus:x:18:18:D-Bus Message Daemon User")); - assert!(group.contains("users:x:999:")); - assert!(group.contains("wheel:x:97:")); - } - - #[test] - fn skips_lbi_layout_initialization_when_resuming() { - let tmp = tempfile::tempdir().unwrap(); - let config = config::Config::for_rootfs(tmp.path()); - system_state::set_stage(&config, "bootstrap-layers".to_string()).unwrap(); - - ensure_lbi_layout_for_fresh_bootstrap( - tmp.path(), - &config, - "x86_64-unknown-linux-musl", - "x86_64", - ) - .unwrap(); - - assert!(!tmp.path().join("etc/depot.d/build.toml").exists()); - assert_eq!( - fs::read_link(tmp.path().join("usr/include")).unwrap(), - PathBuf::from("../system/headers") - ); - assert_eq!( - fs::read_link(tmp.path().join("dev")).unwrap(), - PathBuf::from("system/devices") - ); - let state = system_state::load(&config).unwrap(); - assert_eq!(state.stage.as_deref(), Some("bootstrap-layers")); - } -} diff --git a/src/builder/autotools.rs b/src/builder/autotools.rs index 22ef485..90925ce 100755 --- a/src/builder/autotools.rs +++ b/src/builder/autotools.rs @@ -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, diff --git a/src/builder/bin.rs b/src/builder/bin.rs index 6d3adf4..de70c57 100644 --- a/src/builder/bin.rs +++ b/src/builder/bin.rs @@ -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(()) } diff --git a/src/builder/cmake.rs b/src/builder/cmake.rs index 4cd9fdf..47d5d1a 100755 --- a/src/builder/cmake.rs +++ b/src/builder/cmake.rs @@ -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 { + 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 { diff --git a/src/builder/custom.rs b/src/builder/custom.rs index 7d9b6f2..c6f9537 100755 --- a/src/builder/custom.rs +++ b/src/builder/custom.rs @@ -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; diff --git a/src/builder/makefile.rs b/src/builder/makefile.rs index 64e17ad..977e0f4 100644 --- a/src/builder/makefile.rs +++ b/src/builder/makefile.rs @@ -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; diff --git a/src/builder/meson.rs b/src/builder/meson.rs index 6e91801..663fb92 100755 --- a/src/builder/meson.rs +++ b/src/builder/meson.rs @@ -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, diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 1810d59..986121c 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -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 = 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 { + 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> { 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() - } else { - "--disable-static".to_string() - }], + BuildType::Autotools => { + if enabled { + vec!["--enable-static".to_string()] + } else { + 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() - ) - })?; - } - #[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(()) + crate::fs_copy::copy_tree_preserving_links( + &staging_destdir.join(&lib_rel), + &destdir.join("usr/lib32"), + ) } fn lib32_stage_source_rel(staging_destdir: &Path) -> Result { 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(()) + } } diff --git a/src/builder/perl.rs b/src/builder/perl.rs index 81786bc..e3b5983 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -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)); diff --git a/src/builder/rust.rs b/src/builder/rust.rs index f6a2fd7..b0995a8 100755 --- a/src/builder/rust.rs +++ b/src/builder/rust.rs @@ -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 { + 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")); + } +} diff --git a/src/cli.rs b/src/cli.rs index b4c6d6a..6d371da 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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, - - /// Target architecture component used by build defaults - #[arg(long)] - pub arch: Option, - - /// 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, -} - #[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, - /// 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, - }, - /// 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, - }, - /// 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 }, #[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, diff --git a/src/commands.rs b/src/commands.rs index 0cd6f0e..53fe661 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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)?, diff --git a/src/commands/misc.rs b/src/commands/misc.rs index 8bec41c..8e6c4a8 100644 --- a/src/commands/misc.rs +++ b/src/commands/misc.rs @@ -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()?; diff --git a/src/commands/misc/internal.rs b/src/commands/misc/internal.rs index 55450dd..f879459 100644 --- a/src/commands/misc/internal.rs +++ b/src/commands/misc/internal.rs @@ -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())); diff --git a/src/commands/set.rs b/src/commands/set.rs index 22da194..bd81ec5 100644 --- a/src/commands/set.rs +++ b/src/commands/set.rs @@ -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); } diff --git a/src/config.rs b/src/config.rs index 3f24451..7ca654e 100755 --- a/src/config.rs +++ b/src/config.rs @@ -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") - } + abs_rootfs.join("var/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") ); } diff --git a/src/db/mod.rs b/src/db/mod.rs index 10a5452..1421f35 100755 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -844,6 +844,30 @@ pub fn get_installed_packages(db_path: &Path) -> Result Result> { + 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 = 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> { if !db_path.exists() { @@ -1038,6 +1062,27 @@ pub fn get_package_version(db_path: &Path, name: &str) -> Result> 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> { + 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> { 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] diff --git a/src/deps.rs b/src/deps.rs index ee17531..4592ef8 100755 --- a/src/deps.rs +++ b/src/deps.rs @@ -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 { 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( diff --git a/src/fakeroot.rs b/src/fakeroot.rs index 6d452b6..1f01dea 100755 --- a/src/fakeroot.rs +++ b/src/fakeroot.rs @@ -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") + })); + } +} diff --git a/src/fs_copy.rs b/src/fs_copy.rs new file mode 100644 index 0000000..fddb9e1 --- /dev/null +++ b/src/fs_copy.rs @@ -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 { + (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, +} + +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(()) + } +} diff --git a/src/interrupts.rs b/src/interrupts.rs index f87faca..400823a 100644 --- a/src/interrupts.rs +++ b/src/interrupts.rs @@ -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)) diff --git a/src/main.rs b/src/main.rs index b57366c..a47b160 100755 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/src/package/packager.rs b/src/package/packager.rs index 89a9b66..f6c346f 100644 --- a/src/package/packager.rs +++ b/src/package/packager.rs @@ -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 { + (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 = 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 mut file = fs::File::open(path)?; - tar.append_file(rel_path, &mut file)?; + 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); + } } diff --git a/src/package/spec.rs b/src/package/spec.rs deleted file mode 100755 index 880a935..0000000 --- a/src/package/spec.rs +++ /dev/null @@ -1,5274 +0,0 @@ -//! Package specification structures and TOML parsing - -use anyhow::{Context, Result}; -use serde::Deserialize; -use serde::Deserializer; -use serde::Serialize; -use serde::Serializer; -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, - #[serde(default)] - pub alternatives: Alternatives, - /// Manual (local) sources to copy before fetching remote sources. - #[serde(default)] - pub manual_sources: Vec, - #[serde(default, deserialize_with = "deserialize_sources")] - pub source: Vec, - 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, - /// 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, - - /// 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 { - // 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>, - ) -> 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 { - 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 { - 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 { - 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()) - } - - /// Apply system configuration overrides and appends - pub fn apply_config(&mut self, config: &crate::config::Config) { - // Apply build overrides from /etc/depot.d/build.toml - self.apply_toml_overrides(&config.build_overrides, "build"); - - // Apply appends from /etc/depot.d/build.toml (e.g. build.flags.cflags += ["-O3"]) - for (key, values) in &config.appends { - let key = normalize_append_key(key); - if let Some(subkey) = key.strip_prefix("build.flags.") { - self.apply_append(subkey, values); - } else if let Some(subkey) = key.strip_prefix("build.") { - self.apply_append(subkey, values); - } - } - } - - fn apply_toml_overrides(&mut self, overrides: &toml::Value, _prefix: &str) { - // Support both [build.flags] and top-level [build] fields - if let Some(table) = overrides.as_table() { - self.apply_flags_table(table); - } - if let Some(table) = overrides.get("flags").and_then(|f| f.as_table()) { - self.apply_flags_table(table); - } - } - - fn apply_default_string(target: &mut String, default: &str, value: &toml::Value) { - if let Some(s) = value.as_str() - && (target.trim().is_empty() || target == default) - { - *target = s.to_string(); - } - } - - fn apply_flags_table(&mut self, table: &toml::map::Map) { - let default_flags = BuildFlags::default(); - for (k, v) in table { - // match case-insensitively for common keys (allow CXX/Cc etc.) - match k.to_lowercase().as_str() { - "cflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.cflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.cflags = vec![s.to_string()]; - } - } - "replace_cflags" | "replace-cflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_cflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cflags = vec![s.to_string()]; - } - } - "cflags-lib32" | "cflags_lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.cflags_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.cflags_lib32 = vec![s.to_string()]; - } - } - "replace_cflags-lib32" | "replace_cflags_lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_cflags_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cflags_lib32 = vec![s.to_string()]; - } - } - "cxxflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.cxxflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.cxxflags = vec![s.to_string()]; - } - } - "replace_cxxflags" | "replace-cxxflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_cxxflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cxxflags = vec![s.to_string()]; - } - } - "cxxflags-lib32" | "cxxflags_lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.cxxflags_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.cxxflags_lib32 = vec![s.to_string()]; - } - } - "replace_cxxflags-lib32" | "replace_cxxflags_lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_cxxflags_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cxxflags_lib32 = vec![s.to_string()]; - } - } - "ldflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.ldflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.ldflags = vec![s.to_string()]; - } - } - "fuse_ld" | "fuse-ld" => { - Self::apply_default_string( - &mut self.build.flags.fuse_ld, - &default_flags.fuse_ld, - v, - ); - } - "tool_dir" | "tool-dir" | "tools_dir" | "tools-dir" => { - Self::apply_default_string( - &mut self.build.flags.tool_dir, - &default_flags.tool_dir, - v, - ); - } - "replace_ldflags" | "replace-ldflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_ldflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_ldflags = vec![s.to_string()]; - } - } - "ltoflags" | "lto_flags" | "lto-flags" => { - if let Some(arr) = v.as_array() { - self.build.flags.ltoflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.ltoflags = vec![s.to_string()]; - } - } - "rustltoflags" | "rust_ltoflags" | "rust-ltoflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.rustltoflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.rustltoflags = vec![s.to_string()]; - } - } - "replace_ltoflags" | "replace_lto-flags" | "replace_lto_flags" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_ltoflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_ltoflags = vec![s.to_string()]; - } - } - "rustflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.rustflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.rustflags = vec![s.to_string()]; - } - } - "replace_rustflags" | "replace-rustflags" => { - if let Some(arr) = v.as_array() { - self.build.flags.replace_rustflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_rustflags = vec![s.to_string()]; - } - } - "keep" => { - if let Some(arr) = v.as_array() { - self.build.flags.keep = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.keep = vec![s.to_string()]; - } - } - "split_docs" | "split-docs" => { - if let Some(b) = toml_value_as_boolish(v) { - self.build.flags.split_docs = b; - } - } - "doc_dirs" | "doc-dirs" => { - if let Some(arr) = v.as_array() { - self.build.flags.doc_dirs = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.doc_dirs = vec![s.to_string()]; - } - } - "cc" => { - Self::apply_default_string(&mut self.build.flags.cc, &default_flags.cc, v); - } - "cxx" => { - Self::apply_default_string(&mut self.build.flags.cxx, &default_flags.cxx, v); - } - "ar" => { - Self::apply_default_string(&mut self.build.flags.ar, &default_flags.ar, v); - } - "ranlib" => { - Self::apply_default_string( - &mut self.build.flags.ranlib, - &default_flags.ranlib, - v, - ); - } - "strip" => { - Self::apply_default_string( - &mut self.build.flags.strip, - &default_flags.strip, - v, - ); - } - "ld" => { - Self::apply_default_string(&mut self.build.flags.ld, &default_flags.ld, v); - } - "nm" => { - Self::apply_default_string(&mut self.build.flags.nm, &default_flags.nm, v); - } - "objcopy" => { - Self::apply_default_string( - &mut self.build.flags.objcopy, - &default_flags.objcopy, - v, - ); - } - "objdump" => { - Self::apply_default_string( - &mut self.build.flags.objdump, - &default_flags.objdump, - v, - ); - } - "readelf" => { - Self::apply_default_string( - &mut self.build.flags.readelf, - &default_flags.readelf, - v, - ); - } - "cpp" => { - Self::apply_default_string(&mut self.build.flags.cpp, &default_flags.cpp, v); - } - "prefix" => { - Self::apply_default_string( - &mut self.build.flags.prefix, - &default_flags.prefix, - v, - ); - } - "bindir" => { - Self::apply_default_string( - &mut self.build.flags.bindir, - &default_flags.bindir, - v, - ); - } - "sbindir" => { - Self::apply_default_string( - &mut self.build.flags.sbindir, - &default_flags.sbindir, - v, - ); - } - "libdir" => { - Self::apply_default_string( - &mut self.build.flags.libdir, - &default_flags.libdir, - v, - ); - } - "libexecdir" => { - Self::apply_default_string( - &mut self.build.flags.libexecdir, - &default_flags.libexecdir, - v, - ); - } - "sysconfdir" => { - Self::apply_default_string( - &mut self.build.flags.sysconfdir, - &default_flags.sysconfdir, - v, - ); - } - "localstatedir" => { - Self::apply_default_string( - &mut self.build.flags.localstatedir, - &default_flags.localstatedir, - v, - ); - } - "sharedstatedir" => { - Self::apply_default_string( - &mut self.build.flags.sharedstatedir, - &default_flags.sharedstatedir, - v, - ); - } - "includedir" => { - Self::apply_default_string( - &mut self.build.flags.includedir, - &default_flags.includedir, - v, - ); - } - "datarootdir" => { - Self::apply_default_string( - &mut self.build.flags.datarootdir, - &default_flags.datarootdir, - v, - ); - } - "datadir" => { - Self::apply_default_string( - &mut self.build.flags.datadir, - &default_flags.datadir, - v, - ); - } - "mandir" => { - Self::apply_default_string( - &mut self.build.flags.mandir, - &default_flags.mandir, - v, - ); - } - "infodir" => { - Self::apply_default_string( - &mut self.build.flags.infodir, - &default_flags.infodir, - v, - ); - } - "chost" => { - Self::apply_default_string( - &mut self.build.flags.chost, - &default_flags.chost, - v, - ); - } - "cbuild" => { - Self::apply_default_string( - &mut self.build.flags.cbuild, - &default_flags.cbuild, - v, - ); - } - "carch" => { - Self::apply_default_string( - &mut self.build.flags.carch, - &default_flags.carch, - v, - ); - } - "makeflags" | "make_flags" | "make-flags" => { - if let Some(s) = v.as_str() { - self.build.flags.makeflags = s.to_string(); - } else if let Some(arr) = v.as_array() { - self.build.flags.makeflags = arr - .iter() - .filter_map(|x| x.as_str()) - .map(str::trim) - .filter(|x| !x.is_empty()) - .collect::>() - .join(" "); - } - } - "make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_vars = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_vars = - s.split_whitespace().map(String::from).collect(); - } - } - "make_exec" | "make-exec" => { - if let Some(s) = v.as_str() { - self.build.flags.make_exec = s.to_string(); - } - } - "make_target" | "make-target" | "make_build_target" | "make-build-target" => { - if let Some(s) = v.as_str() { - self.build.flags.make_target = s.to_string(); - } - } - "make_targets" | "make-targets" | "make_build_targets" | "make-build-targets" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_targets = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_targets = - s.split_whitespace().map(String::from).collect(); - } - } - "make_dirs" | "make-dirs" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_dirs = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_dirs = - s.split_whitespace().map(String::from).collect(); - } - } - "make_test_vars" | "make-test-vars" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_test_vars = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_test_vars = - s.split_whitespace().map(String::from).collect(); - } - } - "make_test_target" | "make-test-target" => { - if let Some(s) = v.as_str() { - self.build.flags.make_test_target = s.to_string(); - } - } - "make_test_targets" | "make-test-targets" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_test_targets = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_test_targets = - s.split_whitespace().map(String::from).collect(); - } - } - "make_test_dirs" | "make-test-dirs" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_test_dirs = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_test_dirs = - s.split_whitespace().map(String::from).collect(); - } - } - "make_install_vars" | "make-install-vars" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_install_vars = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_install_vars = - s.split_whitespace().map(String::from).collect(); - } - } - "make_install_target" | "make-install-target" => { - if let Some(s) = v.as_str() { - self.build.flags.make_install_target = s.to_string(); - } - } - "make_install_targets" | "make-install-targets" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_install_targets = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_install_targets = - s.split_whitespace().map(String::from).collect(); - } - } - "make_install_dirs" | "make-install-dirs" => { - if let Some(arr) = v.as_array() { - self.build.flags.make_install_dirs = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.make_install_dirs = - s.split_whitespace().map(String::from).collect(); - } - } - "passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env" - | "export-env" => { - if let Some(arr) = v.as_array() { - self.build.flags.passthrough_env = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.passthrough_env = - s.split_whitespace().map(String::from).collect(); - } - } - "env_vars" | "env-vars" => { - if let Some(arr) = v.as_array() { - self.build.flags.env_vars = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.env_vars = vec![s.to_string()]; - } - } - "no_flags" | "no-flags" => { - if let Some(b) = v.as_bool() { - self.build.flags.no_flags = b; - } - } - "use_lto" | "use-lto" => { - if let Some(b) = toml_value_as_boolish(v) { - self.build.flags.use_lto = b; - } - } - "no_strip" | "no-strip" => { - if let Some(b) = v.as_bool() { - self.build.flags.no_strip = b; - } - } - "no_delete_static" | "no-delete-static" => { - if let Some(b) = v.as_bool() { - self.build.flags.no_delete_static = b; - } - } - "no_compress_man" - | "no-compress-man" - | "no_compress_manpages" - | "no-compress-manpages" => { - if let Some(b) = v.as_bool() { - self.build.flags.no_compress_man = b; - } - } - "skip_tests" | "skip-tests" => { - if let Some(b) = v.as_bool() { - self.build.flags.skip_tests = b; - } - } - "build_32" | "build-32" => { - if let Some(b) = toml_value_as_boolish(v) { - self.build.flags.build_32 = b; - } - } - "lib32_only" | "lib32-only" => { - if let Some(b) = toml_value_as_boolish(v) { - self.build.flags.lib32_only = b; - } - } - "host_build" | "host-build" => { - if let Some(b) = toml_value_as_boolish(v) { - self.build.flags.host_build = b; - } - } - "configure_lib32" | "configure-lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.configure_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.configure_lib32 = vec![s.to_string()]; - } - } - "config_setting" | "config_settings" | "config-setting" | "config-settings" => { - if let Some(arr) = v.as_array() { - self.build.flags.config_settings = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.config_settings = vec![s.to_string()]; - } - } - "configure_file" | "configure-file" => { - if let Some(s) = v.as_str() { - self.build.flags.configure_file = s.to_string(); - } - } - "post_configure" | "post-configure" => { - if let Some(arr) = v.as_array() { - self.build.flags.post_configure = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.post_configure = vec![s.to_string()]; - } - } - "post_configure_lib32" | "post_configure-lib32" | "post-configure-lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.post_configure_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.post_configure_lib32 = vec![s.to_string()]; - } - } - "post_compile" | "post-compile" => { - if let Some(arr) = v.as_array() { - self.build.flags.post_compile = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.post_compile = vec![s.to_string()]; - } - } - "post_compile_lib32" | "post_compile-lib32" | "post-compile-lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.post_compile_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.post_compile_lib32 = vec![s.to_string()]; - } - } - "post_install" | "post-install" => { - if let Some(arr) = v.as_array() { - self.build.flags.post_install = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.post_install = vec![s.to_string()]; - } - } - "post_install_lib32" | "post_install-lib32" | "post-install-lib32" => { - if let Some(arr) = v.as_array() { - self.build.flags.post_install_lib32 = arr - .iter() - .filter_map(|x| x.as_str()) - .map(String::from) - .collect(); - } else if let Some(s) = v.as_str() { - self.build.flags.post_install_lib32 = vec![s.to_string()]; - } - } - // Add more fields as needed - _ => {} - } - } - } - - fn apply_append(&mut self, key: &str, values: &[toml::Value]) { - let key = normalize_append_key(key); - match key.as_str() { - "cflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .cflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.cflags.push(s.to_string()); - } - } - } - "replace_cflags" | "replace-cflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_cflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cflags.push(s.to_string()); - } - } - } - "cflags-lib32" | "cflags_lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .cflags_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.cflags_lib32.push(s.to_string()); - } - } - } - "replace_cflags-lib32" | "replace_cflags_lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_cflags_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cflags_lib32.push(s.to_string()); - } - } - } - "cxxflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .cxxflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.cxxflags.push(s.to_string()); - } - } - } - "replace_cxxflags" | "replace-cxxflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_cxxflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cxxflags.push(s.to_string()); - } - } - } - "cxxflags-lib32" | "cxxflags_lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .cxxflags_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.cxxflags_lib32.push(s.to_string()); - } - } - } - "replace_cxxflags-lib32" | "replace_cxxflags_lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_cxxflags_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_cxxflags_lib32.push(s.to_string()); - } - } - } - "ldflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .ldflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.ldflags.push(s.to_string()); - } - } - } - "replace_ldflags" | "replace-ldflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_ldflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_ldflags.push(s.to_string()); - } - } - } - "ltoflags" | "lto_flags" | "lto-flags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .ltoflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.ltoflags.push(s.to_string()); - } - } - } - "rustltoflags" | "rust_ltoflags" | "rust-ltoflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .rustltoflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.rustltoflags.push(s.to_string()); - } - } - } - "replace_ltoflags" | "replace_lto-flags" | "replace_lto_flags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_ltoflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_ltoflags.push(s.to_string()); - } - } - } - "keep" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .keep - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.keep.push(s.to_string()); - } - } - } - "doc_dirs" | "doc-dirs" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .doc_dirs - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.doc_dirs.push(s.to_string()); - } - } - } - "configure" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .configure - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.configure.push(s.to_string()); - } - } - } - "configure_lib32" | "configure-lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .configure_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.configure_lib32.push(s.to_string()); - } - } - } - "config_setting" | "config_settings" | "config-setting" | "config-settings" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .config_settings - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.config_settings.push(s.to_string()); - } - } - } - "configure_file" | "configure-file" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.configure_file = s.to_string(); - } - } - "post_configure" | "post-configure" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .post_configure - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.post_configure.push(s.to_string()); - } - } - } - "post_configure_lib32" | "post_configure-lib32" | "post-configure-lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .post_configure_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.post_configure_lib32.push(s.to_string()); - } - } - } - "post_compile" | "post-compile" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .post_compile - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.post_compile.push(s.to_string()); - } - } - } - "post_compile_lib32" | "post_compile-lib32" | "post-compile-lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .post_compile_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.post_compile_lib32.push(s.to_string()); - } - } - } - "post_install" | "post-install" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .post_install - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.post_install.push(s.to_string()); - } - } - } - "post_install_lib32" | "post_install-lib32" | "post-install-lib32" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .post_install_lib32 - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.post_install_lib32.push(s.to_string()); - } - } - } - "cargs" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .cargs - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.cargs.push(s.to_string()); - } - } - } - "rustflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .rustflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.rustflags.push(s.to_string()); - } - } - } - "replace_rustflags" | "replace-rustflags" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .replace_rustflags - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.replace_rustflags.push(s.to_string()); - } - } - } - "cc" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.cc = s.to_string(); - } - } - "cxx" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.cxx = s.to_string(); - } - } - "ar" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.ar = s.to_string(); - } - } - "ranlib" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.ranlib = s.to_string(); - } - } - "strip" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.strip = s.to_string(); - } - } - "ld" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.ld = s.to_string(); - } - } - "nm" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.nm = s.to_string(); - } - } - "objcopy" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.objcopy = s.to_string(); - } - } - "objdump" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.objdump = s.to_string(); - } - } - "readelf" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.readelf = s.to_string(); - } - } - "cpp" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.cpp = s.to_string(); - } - } - "prefix" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.prefix = s.to_string(); - } - } - "bindir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.bindir = s.to_string(); - } - } - "sbindir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.sbindir = s.to_string(); - } - } - "libdir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.libdir = s.to_string(); - } - } - "libexecdir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.libexecdir = s.to_string(); - } - } - "sysconfdir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.sysconfdir = s.to_string(); - } - } - "localstatedir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.localstatedir = s.to_string(); - } - } - "sharedstatedir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.sharedstatedir = s.to_string(); - } - } - "includedir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.includedir = s.to_string(); - } - } - "datarootdir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.datarootdir = s.to_string(); - } - } - "datadir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.datadir = s.to_string(); - } - } - "mandir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.mandir = s.to_string(); - } - } - "infodir" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.infodir = s.to_string(); - } - } - "chost" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.chost = s.to_string(); - } - } - "cbuild" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.cbuild = s.to_string(); - } - } - "carch" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.carch = s.to_string(); - } - } - "makeflags" | "make_flags" | "make-flags" | "MAKEFLAGS" => { - for v in values { - if let Some(arr) = v.as_array() { - let joined = arr - .iter() - .filter_map(|x| x.as_str()) - .map(str::trim) - .filter(|x| !x.is_empty()) - .collect::>() - .join(" "); - append_whitespace_separated(&mut self.build.flags.makeflags, &joined); - } else if let Some(s) = v.as_str() { - append_whitespace_separated(&mut self.build.flags.makeflags, s); - } - } - } - "make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_vars - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_vars - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_exec" | "make-exec" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.make_exec = s.to_string(); - } - } - "make_target" | "make-target" | "make_build_target" | "make-build-target" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.make_target = s.to_string(); - } - } - "make_targets" | "make-targets" | "make_build_targets" | "make-build-targets" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_targets - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_targets - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_dirs" | "make-dirs" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_dirs - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_dirs - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_test_vars" | "make-test-vars" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_test_vars - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_test_vars - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_test_target" | "make-test-target" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.make_test_target = s.to_string(); - } - } - "make_test_targets" | "make-test-targets" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_test_targets - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_test_targets - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_test_dirs" | "make-test-dirs" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_test_dirs - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_test_dirs - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_install_vars" | "make-install-vars" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_install_vars - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_install_vars - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_install_target" | "make-install-target" => { - if let Some(s) = values.last().and_then(|v| v.as_str()) { - self.build.flags.make_install_target = s.to_string(); - } - } - "make_install_targets" | "make-install-targets" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_install_targets - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_install_targets - .extend(s.split_whitespace().map(String::from)); - } - } - } - "make_install_dirs" | "make-install-dirs" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .make_install_dirs - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .make_install_dirs - .extend(s.split_whitespace().map(String::from)); - } - } - } - "passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env" - | "export-env" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .passthrough_env - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build - .flags - .passthrough_env - .extend(s.split_whitespace().map(String::from)); - } - } - } - "env_vars" | "env-vars" => { - for v in values { - if let Some(arr) = v.as_array() { - self.build - .flags - .env_vars - .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); - } else if let Some(s) = v.as_str() { - self.build.flags.env_vars.push(s.to_string()); - } - } - } - "no_flags" | "no-flags" => { - if let Some(b) = values.last().and_then(|v| v.as_bool()) { - self.build.flags.no_flags = b; - } - } - "use_lto" | "use-lto" => { - if let Some(b) = values.last().and_then(toml_value_as_boolish) { - self.build.flags.use_lto = b; - } - } - "no_strip" | "no-strip" => { - if let Some(b) = values.last().and_then(|v| v.as_bool()) { - self.build.flags.no_strip = b; - } - } - "no_delete_static" | "no-delete-static" => { - if let Some(b) = values.last().and_then(|v| v.as_bool()) { - self.build.flags.no_delete_static = b; - } - } - "no_compress_man" - | "no-compress-man" - | "no_compress_manpages" - | "no-compress-manpages" => { - if let Some(b) = values.last().and_then(|v| v.as_bool()) { - self.build.flags.no_compress_man = b; - } - } - "skip_tests" | "skip-tests" => { - if let Some(b) = values.last().and_then(toml_value_as_boolish) { - self.build.flags.skip_tests = b; - } - } - "build_32" | "build-32" => { - if let Some(b) = values.last().and_then(toml_value_as_boolish) { - self.build.flags.build_32 = b; - } - } - "lib32_only" | "lib32-only" => { - if let Some(b) = values.last().and_then(toml_value_as_boolish) { - self.build.flags.lib32_only = b; - } - } - "split_docs" | "split-docs" => { - if let Some(b) = values.last().and_then(toml_value_as_boolish) { - self.build.flags.split_docs = b; - } - } - _ => {} - } - } -} - -fn preprocess_spec_toml_appends( - input: &str, -) -> Result<(String, std::collections::HashMap>)> { - let mut base_text = String::new(); - let mut appends = std::collections::HashMap::new(); - let mut current_table: Option = None; - let mut in_array_table = false; - - for line in input.lines() { - let trimmed = line.trim(); - - if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 { - current_table = Some(normalize_append_key(trimmed[2..trimmed.len() - 2].trim())); - in_array_table = true; - base_text.push_str(line); - base_text.push('\n'); - continue; - } - if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 { - current_table = Some(normalize_append_key(trimmed[1..trimmed.len() - 1].trim())); - in_array_table = false; - base_text.push_str(line); - base_text.push('\n'); - continue; - } - - if trimmed.is_empty() || trimmed.starts_with('#') { - base_text.push_str(line); - base_text.push('\n'); - continue; - } - - if let Some(plus_idx) = trimmed.find("+=") { - if in_array_table { - anyhow::bail!( - "'+=' is not supported inside array-of-table sections ({})", - current_table.as_deref().unwrap_or("") - ); - } - let key = normalize_append_key(trimmed[..plus_idx].trim()); - let val_str = trimmed[plus_idx + 2..].trim(); - let val: toml::Value = toml::from_str::(&format!("v = {}", val_str)) - .context("Failed to parse append value")? - .get("v") - .cloned() - .unwrap(); - - let full_key = if key.contains('.') { - key - } else if let Some(table) = current_table.as_deref() { - format!("{}.{}", table, key) - } else { - key - }; - - appends.entry(full_key).or_insert_with(Vec::new).push(val); - // Preserve line numbering for parser diagnostics. - base_text.push('\n'); - continue; - } - - base_text.push_str(line); - base_text.push('\n'); - } - - Ok((base_text, appends)) -} - -fn normalize_append_key(raw: &str) -> String { - raw.split('.') - .map(str::trim) - .filter(|part| !part.is_empty()) - .map(|part| { - let stripped = if (part.starts_with('"') && part.ends_with('"')) - || (part.starts_with('\'') && part.ends_with('\'')) - { - &part[1..part.len() - 1] - } else { - part - }; - stripped.trim().to_ascii_lowercase() - }) - .collect::>() - .join(".") -} - -#[cfg(test)] -mod spec_tests { - use super::*; - - #[test] - fn parse_single_source_table() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo-$version.tar.gz" -sha256 = "skip" -extract_dir = "foo-$version" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.package.name, "foo"); - assert_eq!(spec.sources().len(), 1); - assert_eq!( - spec.expand_vars(&spec.sources()[0].url), - "https://example.com/foo-1.0.tar.gz" - ); - assert!(spec.sources()[0].patches.is_empty()); - assert!(spec.sources()[0].post_extract.is_empty()); - assert!(spec.sources()[0].cherry_pick.is_empty()); - assert_eq!(spec.spec_dir, tmp.path()); - } - - #[test] - fn parse_source_array() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[[source]] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[[source]] -url = "https://example.com/bar.tar.gz" -sha256 = "skip" -extract_dir = "bar" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.sources().len(), 2); - assert_eq!(spec.sources()[0].extract_dir, "foo"); - assert_eq!(spec.sources()[1].extract_dir, "bar"); - } - - #[test] - fn parse_source_without_sha256_defaults_to_skip() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -extract_dir = "foo" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.sources()[0].sha256, "skip"); - } - - #[test] - fn parse_git_source_with_cherry_pick() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.git#main" -sha256 = "skip" -extract_dir = "foo" -cherry_pick = ["deadbeef", "cafebabe"] - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.sources()[0].cherry_pick, - vec!["deadbeef".to_string(), "cafebabe".to_string()] - ); - } - - #[test] - fn parse_package_dependencies_overrides() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "llvm" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/llvm.tar.gz" -sha256 = "skip" -extract_dir = "llvm" - -[build] -type = "custom" - -[dependencies] -runtime = ["base"] -groups = ["toolchain"] - -[package_dependencies.clang] -runtime = ["llvm-libs", "llvm-libgcc"] -groups = ["compiler"] - -[package_dependencies.llvm-libs] -runtime = ["llvm-libgcc", "zstd"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.dependencies_for_output("llvm").runtime, - vec!["base".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("llvm").groups, - vec!["toolchain".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("clang").runtime, - vec!["llvm-libs".to_string(), "llvm-libgcc".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("clang").groups, - vec!["compiler".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("llvm-libs").runtime, - vec!["llvm-libgcc".to_string(), "zstd".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("llvm-libs").groups, - Vec::::new() - ); - } - - #[test] - fn parse_lib32_dependencies_override() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "llvm" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/llvm.tar.gz" -sha256 = "skip" -extract_dir = "llvm" - -[build] -type = "custom" - -[dependencies] -runtime = ["base"] -groups = ["toolchain"] - -[dependencies.lib32] -build = ["gcc-multilib"] -runtime = ["lib32-zlib"] -test = ["bats"] -groups = ["lib32-toolchain"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.dependencies_for_output("llvm").runtime, - vec!["base".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("llvm").groups, - vec!["toolchain".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("lib32-llvm").build, - vec!["gcc-multilib".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("lib32-llvm").runtime, - vec!["lib32-zlib".to_string(), "llvm".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("lib32-llvm").test, - vec!["bats".to_string()] - ); - assert_eq!( - spec.dependencies_for_output("lib32-llvm").groups, - vec!["lib32-toolchain".to_string()] - ); - } - - #[test] - fn parse_package_alternatives_overrides() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "llvm" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/llvm.tar.gz" -sha256 = "skip" -extract_dir = "llvm" - -[build] -type = "custom" - -[alternatives] -provides = ["toolchain"] -conflicts = ["gcc"] - -[package_alternatives.clang] -provides = ["cc", "c++", "gcc"] -conflicts = ["clang-legacy"] - -[package_alternatives.llvm] -provides = ["binutils"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.alternatives_for_output("llvm").provides, - vec!["binutils".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("llvm").conflicts, - Vec::::new() - ); - assert_eq!( - spec.alternatives_for_output("clang").provides, - vec!["cc".to_string(), "c++".to_string(), "gcc".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("clang").conflicts, - vec!["clang-legacy".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("other").provides, - vec!["toolchain".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("other").conflicts, - vec!["gcc".to_string()] - ); - } - - #[test] - fn parse_lib32_alternatives_override() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "llvm" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/llvm.tar.gz" -sha256 = "skip" -extract_dir = "llvm" - -[build] -type = "custom" - -[alternatives] -provides = ["toolchain"] -replaces = ["clang"] - -[alternatives.lib32] -provides = ["lib32-toolchain"] -conflicts = ["lib32-gcc"] -replaces = ["lib32-clang"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.alternatives_for_output("llvm").replaces, - vec!["clang".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("lib32-llvm").provides, - vec!["lib32-toolchain".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("lib32-llvm").conflicts, - vec!["lib32-gcc".to_string()] - ); - assert_eq!( - spec.alternatives_for_output("lib32-llvm").replaces, - vec!["lib32-clang".to_string()] - ); - } - - #[test] - fn lib32_output_does_not_fallback_to_primary_alternatives() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "llvm" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -build_32 = true - -[alternatives] -provides = ["toolchain"] -conflicts = ["gcc"] -replaces = ["clang"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - let lib32 = spec.alternatives_for_output("lib32-llvm"); - - assert!(lib32.provides.is_empty()); - assert!(lib32.conflicts.is_empty()); - assert!(lib32.replaces.is_empty()); - } - - #[test] - fn parse_python_build_type() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "python" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(matches!(spec.build.build_type, BuildType::Python)); - } - - #[test] - fn parse_perl_build_type() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "perl" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(matches!(spec.build.build_type, BuildType::Perl)); - } - - #[test] - fn parse_python_config_settings_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "python" - -[build.flags] -config-setting = ["editable_mode=compat", "setup-args=--plat-name=x86_64"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.config_settings, - vec![ - "editable_mode=compat".to_string(), - "setup-args=--plat-name=x86_64".to_string() - ] - ); - } - - #[test] - fn parse_multiple_licenses() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = ["MIT", "Apache-2.0"] - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.package.license, - vec!["MIT".to_string(), "Apache-2.0".to_string()] - ); - } - - #[test] - fn parse_rejects_empty_sources() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - // `source = []` is not accepted (must have at least one entry) - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -source = [] - -[build] -type = "custom" -"#, - ) - .unwrap(); - - assert!(PackageSpec::from_file(&path).is_err()); - } - - #[test] - fn parse_allows_metapackage_without_sources() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo-meta" -version = "1.0" -description = "metapackage" -homepage = "https://example.com" -license = "MIT" - -[build] -type = "meta" - -[dependencies] -runtime = ["foo", "bar"] -groups = ["base"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.source.is_empty()); - assert!(spec.manual_sources.is_empty()); - assert!(spec.is_metapackage()); - assert_eq!(spec.dependencies.runtime, vec!["foo", "bar"]); - assert_eq!(spec.dependencies.groups, vec!["base"]); - } - - #[test] - fn parse_manual_source_with_url() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[[manual_sources]] -url = "https://example.com/manual.patch" -sha256 = "skip" -dest = "patches/manual.patch" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.manual_sources.len(), 1); - assert_eq!( - spec.manual_sources[0].url.as_deref(), - Some("https://example.com/manual.patch") - ); - assert_eq!(spec.manual_sources[0].file, None); - } - - #[test] - fn parse_manual_source_rejects_missing_file_and_url() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[[manual_sources]] -sha256 = "skip" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let err = PackageSpec::from_file(&path).expect_err("spec should be rejected"); - assert!( - err.to_string() - .contains("must specify one of 'file', 'files', 'url', or 'urls'") - ); - } - - #[test] - fn parse_manual_source_rejects_file_and_url_together() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[[manual_sources]] -file = "manual.patch" -url = "https://example.com/manual.patch" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let err = PackageSpec::from_file(&path).expect_err("spec should be rejected"); - assert!( - err.to_string() - .contains("cannot mix local ('file'/'files') and remote ('url'/'urls') entries") - ); - } - - #[test] - fn parse_manual_source_with_files_array() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[[manual_sources]] -files = ["other", "system-auth"] - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.manual_sources.len(), 1); - assert_eq!(spec.manual_sources[0].files, vec!["other", "system-auth"]); - assert!(spec.manual_sources[0].urls.is_empty()); - } - - #[test] - fn test_apply_config() { - let mut spec = mk_spec("foo", "1.0"); - let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); - - // Mock some overrides and appends - config.build_overrides = toml::from_str( - r#" -[flags] -cc = "my-cc" -cxx = "my-cxx" -ar = "my-ar" -ranlib = "my-ranlib" -strip = "my-strip" -ld = "ld.lld" -nm = "my-nm" -objcopy = "my-objcopy" -objdump = "my-objdump" -readelf = "my-readelf" -CPP = "clang-cpp" -tool_dir = "/opt/toolchain/bin" -cflags = ["-O2"] -replace_cflags = ["-O2=>-O3"] -cxxflags = ["-O2", "-pipe"] -replace_cxxflags = ["-pipe=>-fPIC"] -passthrough_env = ["RUSTFLAGS"] -env_vars = ["SETUPTOOLS_SCM_PRETEND_VERSION=$version"] -bindir = "/opt/bin" -sbindir = "/opt/sbin" -libdir = "/opt/lib64" -sysconfdir = "/opt/etc" -datarootdir = "/opt/share-root" -makeflags = "-j8" -make_vars = ["V=1"] -make_dirs = ["lib"] -make_test_dirs = ["tests"] -make_install_dirs = ["lib"] -ltoflags = ["-flto=auto"] -RUSTLTOFLAGS = ["-Clinker-plugin-lto"] -replace_ltoflags = ["auto=>thin"] -rustflags = ["-C", "debuginfo=2"] -replace_rustflags = ["debuginfo=2=>opt-level=2"] -use_lto = true -no_flags = true -no_strip = true -no_delete_static = true -no_compress_man = true -skip_tests = true -keep = ["etc/locale.gen"] -configure_file = "configure.gnu" -config-setting = ["editable_mode=compat"] -post_configure = ["echo configured"] -"#, - ) - .unwrap(); - config.appends.insert( - "build.flags.cflags".to_string(), - vec![toml::Value::String("-g".to_string())], - ); - config.appends.insert( - "build.flags.replace_cflags".to_string(), - vec![toml::Value::String( - "-D_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".to_string(), - )], - ); - config.appends.insert( - "build.flags.cxxflags".to_string(), - vec![toml::Value::String("-stdlib=libc++".to_string())], - ); - config.appends.insert( - "build.flags.replace_cxxflags".to_string(), - vec![toml::Value::String( - "-stdlib=libc++=>-stdlib=libstdc++".to_string(), - )], - ); - config.appends.insert( - "build.flags.rustflags".to_string(), - vec![toml::Value::Array(vec![ - toml::Value::String("-C".to_string()), - toml::Value::String("opt-level=3".to_string()), - ])], - ); - config.appends.insert( - "build.flags.replace_rustflags".to_string(), - vec![toml::Value::String("opt-level=3=>opt-level=z".to_string())], - ); - config.appends.insert( - "build.flags.keep".to_string(), - vec![toml::Value::Array(vec![toml::Value::String( - "etc/locale.gen".to_string(), - )])], - ); - config.appends.insert( - "build.flags.ltoflags".to_string(), - vec![toml::Value::Array(vec![toml::Value::String( - "-fno-fat-lto-objects".to_string(), - )])], - ); - config.appends.insert( - "build.flags.replace_ltoflags".to_string(), - vec![toml::Value::String( - "-fno-fat-lto-objects=>-flto-jobs=8".to_string(), - )], - ); - config.appends.insert( - "build.flags.use_lto".to_string(), - vec![toml::Value::Boolean(false)], - ); - config.appends.insert( - "build.flags.no_strip".to_string(), - vec![toml::Value::Boolean(false)], - ); - config.appends.insert( - "build.flags.no_compress_man".to_string(), - vec![toml::Value::Boolean(false)], - ); - config.appends.insert( - "build.flags.no_delete_static".to_string(), - vec![toml::Value::Boolean(false)], - ); - config.appends.insert( - "build.flags.passthrough_env".to_string(), - vec![toml::Value::String("CARGO_HOME".to_string())], - ); - config.appends.insert( - "build.flags.env_vars".to_string(), - vec![toml::Value::String( - "SOURCE_DATE_EPOCH=1700000000".to_string(), - )], - ); - config.appends.insert( - "build.flags.make_test_vars".to_string(), - vec![toml::Value::String("TESTS=smoke".to_string())], - ); - config.appends.insert( - "build.flags.makeflags".to_string(), - vec![toml::Value::String("--output-sync=target".to_string())], - ); - config.appends.insert( - "build.flags.make_dirs".to_string(), - vec![toml::Value::String("libelf".to_string())], - ); - config.appends.insert( - "build.flags.make_test_dirs".to_string(), - vec![toml::Value::String("fuzz".to_string())], - ); - config.appends.insert( - "build.flags.make_install_dirs".to_string(), - vec![toml::Value::String("tools".to_string())], - ); - config.appends.insert( - "build.flags.make_install_vars".to_string(), - vec![toml::Value::String("DESTDIR=/tmp/pkg".to_string())], - ); - config.appends.insert( - "build.flags.configure_file".to_string(), - vec![toml::Value::String("build-aux/configure".to_string())], - ); - config.appends.insert( - "build.flags.libexecdir".to_string(), - vec![toml::Value::String("/opt/libexec".to_string())], - ); - config.appends.insert( - "build.flags.datadir".to_string(), - vec![toml::Value::String("/opt/share-data".to_string())], - ); - config.appends.insert( - "build.flags.config-setting".to_string(), - vec![toml::Value::String( - "setup-args=--plat-name=x86_64".to_string(), - )], - ); - config.appends.insert( - "build.flags.post_configure".to_string(), - vec![toml::Value::String("touch configured.stamp".to_string())], - ); - - spec.apply_config(&config); - - assert_eq!(spec.build.flags.cc, "my-cc"); - assert_eq!(spec.build.flags.cxx, "my-cxx"); - assert_eq!(spec.build.flags.ar, "my-ar"); - assert_eq!(spec.build.flags.ranlib, "my-ranlib"); - assert_eq!(spec.build.flags.strip, "my-strip"); - assert_eq!(spec.build.flags.ld, "ld.lld"); - assert_eq!(spec.build.flags.nm, "my-nm"); - assert_eq!(spec.build.flags.objcopy, "my-objcopy"); - assert_eq!(spec.build.flags.objdump, "my-objdump"); - assert_eq!(spec.build.flags.readelf, "my-readelf"); - assert_eq!(spec.build.flags.cpp, "clang-cpp"); - assert_eq!(spec.build.flags.tool_dir, "/opt/toolchain/bin"); - assert!(spec.build.flags.cflags.contains(&"-O2".to_string())); - assert!(spec.build.flags.cflags.contains(&"-g".to_string())); - assert!( - spec.build - .flags - .replace_cflags - .contains(&"-O2=>-O3".to_string()) - ); - assert!( - spec.build - .flags - .replace_cflags - .contains(&"-D_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".to_string()) - ); - assert!(spec.build.flags.cxxflags.contains(&"-O2".to_string())); - assert!(spec.build.flags.cxxflags.contains(&"-pipe".to_string())); - assert!( - spec.build - .flags - .cxxflags - .contains(&"-stdlib=libc++".to_string()) - ); - assert!( - spec.build - .flags - .replace_cxxflags - .contains(&"-pipe=>-fPIC".to_string()) - ); - assert!( - spec.build - .flags - .replace_cxxflags - .contains(&"-stdlib=libc++=>-stdlib=libstdc++".to_string()) - ); - assert!(spec.build.flags.rustflags.contains(&"-C".to_string())); - assert!( - spec.build - .flags - .rustflags - .contains(&"debuginfo=2".to_string()) - ); - assert!( - spec.build - .flags - .rustflags - .contains(&"opt-level=3".to_string()) - ); - assert!( - spec.build - .flags - .replace_rustflags - .contains(&"debuginfo=2=>opt-level=2".to_string()) - ); - assert!( - spec.build - .flags - .replace_rustflags - .contains(&"opt-level=3=>opt-level=z".to_string()) - ); - assert!( - spec.build - .flags - .ltoflags - .contains(&"-flto=auto".to_string()) - ); - assert!( - spec.build - .flags - .rustltoflags - .contains(&"-Clinker-plugin-lto".to_string()) - ); - assert!( - spec.build - .flags - .ltoflags - .contains(&"-fno-fat-lto-objects".to_string()) - ); - assert!( - spec.build - .flags - .replace_ltoflags - .contains(&"auto=>thin".to_string()) - ); - assert!( - spec.build - .flags - .replace_ltoflags - .contains(&"-fno-fat-lto-objects=>-flto-jobs=8".to_string()) - ); - assert!(!spec.build.flags.use_lto); - assert!(spec.build.flags.no_flags); - assert!(!spec.build.flags.no_strip); - assert!(!spec.build.flags.no_delete_static); - assert!(!spec.build.flags.no_compress_man); - assert!( - spec.build - .flags - .keep - .contains(&"etc/locale.gen".to_string()) - ); - assert!( - spec.build - .flags - .passthrough_env - .contains(&"RUSTFLAGS".to_string()) - ); - assert!( - spec.build - .flags - .passthrough_env - .contains(&"CARGO_HOME".to_string()) - ); - assert!( - spec.build - .flags - .env_vars - .contains(&"SETUPTOOLS_SCM_PRETEND_VERSION=$version".to_string()) - ); - assert!( - spec.build - .flags - .env_vars - .contains(&"SOURCE_DATE_EPOCH=1700000000".to_string()) - ); - assert_eq!(spec.build.flags.bindir, "/opt/bin"); - assert_eq!(spec.build.flags.sbindir, "/opt/sbin"); - assert_eq!(spec.build.flags.libdir, "/opt/lib64"); - assert_eq!(spec.build.flags.libexecdir, "/opt/libexec"); - assert_eq!(spec.build.flags.sysconfdir, "/opt/etc"); - assert_eq!(spec.build.flags.datarootdir, "/opt/share-root"); - assert_eq!(spec.build.flags.datadir, "/opt/share-data"); - assert_eq!(spec.build.flags.makeflags, "-j8 --output-sync=target"); - assert!(spec.build.flags.make_vars.contains(&"V=1".to_string())); - assert!(spec.build.flags.make_dirs.contains(&"lib".to_string())); - assert!(spec.build.flags.make_dirs.contains(&"libelf".to_string())); - assert!(spec.build.flags.skip_tests); - assert!( - spec.build - .flags - .make_test_vars - .contains(&"TESTS=smoke".to_string()) - ); - assert!( - spec.build - .flags - .make_test_dirs - .contains(&"tests".to_string()) - ); - assert!( - spec.build - .flags - .make_test_dirs - .contains(&"fuzz".to_string()) - ); - assert!( - spec.build - .flags - .make_install_vars - .contains(&"DESTDIR=/tmp/pkg".to_string()) - ); - assert!( - spec.build - .flags - .make_install_dirs - .contains(&"lib".to_string()) - ); - assert!( - spec.build - .flags - .make_install_dirs - .contains(&"tools".to_string()) - ); - assert_eq!(spec.build.flags.configure_file, "build-aux/configure"); - assert!( - spec.build - .flags - .config_settings - .contains(&"editable_mode=compat".to_string()) - ); - assert!( - spec.build - .flags - .config_settings - .contains(&"setup-args=--plat-name=x86_64".to_string()) - ); - assert!( - spec.build - .flags - .post_configure - .contains(&"echo configured".to_string()) - ); - assert!( - spec.build - .flags - .post_configure - .contains(&"touch configured.stamp".to_string()) - ); - } - - #[test] - fn test_apply_config_preserves_package_scalar_tool_and_layout_overrides() { - let mut spec = mk_spec("foo", "1.0"); - spec.build.flags.ld = "ld.lld".to_string(); - spec.build.flags.libdir = "/package/lib".to_string(); - spec.build.flags.sysconfdir = "/package/etc".to_string(); - let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); - config.build_overrides = toml::from_str( - r#" -ld = "/config/bin/ld" -fuse_ld = "/config/bin/ld.lld" -ranlib = "/config/bin/ranlib" -libdir = "/config/lib" -sysconfdir = "/config/etc" -"#, - ) - .unwrap(); - - spec.apply_config(&config); - - assert_eq!(spec.build.flags.ld, "ld.lld"); - assert_eq!(spec.build.flags.libdir, "/package/lib"); - assert_eq!(spec.build.flags.sysconfdir, "/package/etc"); - assert_eq!(spec.build.flags.ranlib, "/config/bin/ranlib"); - assert_eq!(spec.build.flags.fuse_ld, "/config/bin/ld.lld"); - } - - #[test] - fn parse_no_flags_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -no_flags = true -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.no_flags); - } - - #[test] - fn parse_tool_commands_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -cc = "/tools/bin/cc" -cxx = "/tools/bin/c++" -ar = "/tools/bin/ar" -ranlib = "/tools/bin/ranlib" -strip = "/tools/bin/strip" -ld = "/tools/bin/ld" -fuse_ld = "/usr/bin/ld.lld" -nm = "/tools/bin/nm" -objcopy = "/tools/bin/objcopy" -objdump = "/tools/bin/objdump" -readelf = "/tools/bin/readelf" -cpp = "/tools/bin/cpp" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.build.flags.cc, "/tools/bin/cc"); - assert_eq!(spec.build.flags.cxx, "/tools/bin/c++"); - assert_eq!(spec.build.flags.ar, "/tools/bin/ar"); - assert_eq!(spec.build.flags.ranlib, "/tools/bin/ranlib"); - assert_eq!(spec.build.flags.strip, "/tools/bin/strip"); - assert_eq!(spec.build.flags.ld, "/tools/bin/ld"); - assert_eq!(spec.build.flags.fuse_ld, "/usr/bin/ld.lld"); - assert_eq!(spec.build.flags.nm, "/tools/bin/nm"); - assert_eq!(spec.build.flags.objcopy, "/tools/bin/objcopy"); - assert_eq!(spec.build.flags.objdump, "/tools/bin/objdump"); - assert_eq!(spec.build.flags.readelf, "/tools/bin/readelf"); - assert_eq!(spec.build.flags.cpp, "/tools/bin/cpp"); - } - - #[test] - fn parse_ltoflags_and_use_lto_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -ltoflags = ["-flto=auto", "-fuse-linker-plugin"] -use_lto = false -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.ltoflags, - vec!["-flto=auto".to_string(), "-fuse-linker-plugin".to_string()] - ); - assert!(!spec.build.flags.use_lto); - } - - #[test] - fn parse_ltoflags_and_use_lto_aliases_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -LTOFLAGS = "-flto=auto" -"use-lto" = false -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.build.flags.ltoflags, vec!["-flto=auto".to_string()]); - assert!(!spec.build.flags.use_lto); - } - - #[test] - fn parse_no_strip_no_delete_static_and_no_compress_man_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -no_strip = true -"no-delete-static" = true -no-compress-man = true -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.no_strip); - assert!(spec.build.flags.no_delete_static); - assert!(spec.build.flags.no_compress_man); - } - - #[test] - fn parse_no_flags_alias_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -"no-flags" = true -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.no_flags); - } - - #[test] - fn parse_skip_tests_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[build.flags] -skip_tests = true -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.skip_tests); - } - - #[test] - fn parse_skip_tests_alias_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[build.flags] -"skip-tests" = true -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.skip_tests); - } - - #[test] - fn reject_unknown_nested_keys_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[dependencies] -runtime = ["glibc"] -skip_tests = true -"#, - ) - .unwrap(); - - let err = PackageSpec::from_file(&path).expect_err("expected unknown nested key to fail"); - assert!( - err.to_string() - .contains("unknown key: dependencies.skip_tests") - ); - } - - #[test] - fn parse_configure_file_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[build.flags] -configure_file = "build-aux/configure" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.build.flags.configure_file, "build-aux/configure"); - } - - #[test] - fn parse_install_dirs_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "cmake" - -[build.flags] -bindir = "/custom/bin" -sbindir = "/custom/sbin" -libdir = "/custom/lib64" -libexecdir = "/custom/libexec" -sysconfdir = "/custom/etc" -localstatedir = "/custom/var" -sharedstatedir = "/custom/var/lib" -includedir = "/custom/include" -datarootdir = "/custom/share-root" -datadir = "/custom/share" -mandir = "/custom/share/man" -infodir = "/custom/share/info" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.build.flags.bindir, "/custom/bin"); - assert_eq!(spec.build.flags.sbindir, "/custom/sbin"); - assert_eq!(spec.build.flags.libdir, "/custom/lib64"); - assert_eq!(spec.build.flags.libexecdir, "/custom/libexec"); - assert_eq!(spec.build.flags.sysconfdir, "/custom/etc"); - assert_eq!(spec.build.flags.localstatedir, "/custom/var"); - assert_eq!(spec.build.flags.sharedstatedir, "/custom/var/lib"); - assert_eq!(spec.build.flags.includedir, "/custom/include"); - assert_eq!(spec.build.flags.datarootdir, "/custom/share-root"); - assert_eq!(spec.build.flags.datadir, "/custom/share"); - assert_eq!(spec.build.flags.mandir, "/custom/share/man"); - assert_eq!(spec.build.flags.infodir, "/custom/share/info"); - } - - #[test] - fn parse_lib32_build_flags_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[build.flags] -"build-32" = "true" -"lib32-only" = "yes" -"CFLAGS-lib32" = ["-mstackrealign"] -"CXXFLAGS-lib32" = ["-fno-rtti"] -"configure-lib32" = ["--disable-static"] -"post_configure-lib32" = ["echo configured lib32"] -"post_compile-lib32" = ["echo compiled lib32"] -"post_install-lib32" = ["echo lib32"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.build_32); - assert!(spec.build.flags.lib32_only); - assert!(spec.builds_lib32_output()); - assert!(spec.builds_only_lib32_output()); - assert_eq!(spec.build.flags.cflags_lib32, vec!["-mstackrealign"]); - assert_eq!(spec.build.flags.cxxflags_lib32, vec!["-fno-rtti"]); - assert_eq!(spec.build.flags.configure_lib32, vec!["--disable-static"]); - assert_eq!( - spec.build.flags.post_configure_lib32, - vec!["echo configured lib32"] - ); - assert_eq!( - spec.build.flags.post_compile_lib32, - vec!["echo compiled lib32"] - ); - assert_eq!(spec.build.flags.post_install_lib32, vec!["echo lib32"]); - } - - #[test] - fn multilib_builds_skip_automatic_tests() { - let mut spec = mk_spec("foo", "1.0"); - assert!(!spec.should_skip_automatic_tests()); - - spec.build.flags.build_32 = true; - assert!(spec.should_skip_automatic_tests()); - - spec.build.flags.build_32 = false; - spec.build.flags.skip_tests = true; - assert!(spec.should_skip_automatic_tests()); - } - - #[test] - fn parse_post_configure_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "cmake" - -[build.flags] -post_configure = ["cmake -L . > cmake-options.txt"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.post_configure, - vec!["cmake -L . > cmake-options.txt".to_string()] - ); - } - - #[test] - fn parse_keep_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -keep = ["etc/locale.gen", "etc/resolv.conf"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.keep, - vec!["etc/locale.gen".to_string(), "etc/resolv.conf".to_string()] - ); - } - - #[test] - fn parse_split_docs_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -split_docs = true -doc_dirs = ["/opt/docs", "usr/share/devhelp"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert!(spec.build.flags.split_docs); - assert_eq!( - spec.build.flags.doc_dirs, - vec!["/opt/docs".to_string(), "usr/share/devhelp".to_string()] - ); - } - - #[test] - fn parse_build_flags_appends_from_spec_file() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -replace_cflags = ["-O2=>-O3"] -replace_rustflags = ["debuginfo=2=>opt-level=2"] -cxxflags = ["-O2"] -cxxflags += [ "-Wno-gnu-statement-expression-from-macro-expansion" ] -ldflags += "-Wl,--as-needed" -replace_cflags += [ "_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2" ] -replace_rustflags += "opt-level=3=>opt-level=z" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.cxxflags, - vec![ - "-O2".to_string(), - "-Wno-gnu-statement-expression-from-macro-expansion".to_string() - ] - ); - assert_eq!( - spec.build.flags.ldflags, - vec!["-Wl,--as-needed".to_string()] - ); - assert_eq!( - spec.build.flags.replace_cflags, - vec![ - "-O2=>-O3".to_string(), - "_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".to_string() - ] - ); - assert_eq!( - spec.build.flags.replace_rustflags, - vec![ - "debuginfo=2=>opt-level=2".to_string(), - "opt-level=3=>opt-level=z".to_string() - ] - ); - } - - #[test] - fn parse_build_flags_appends_accepts_quoted_and_uppercase_keys() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -"cflags" += ["-fPIC"] -CXXFLAGS += ["-stdlib=libc++"] -"LDFLAGS" += "-Wl,--as-needed" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.build.flags.cflags, vec!["-fPIC".to_string()]); - assert_eq!( - spec.build.flags.cxxflags, - vec!["-stdlib=libc++".to_string()] - ); - assert_eq!( - spec.build.flags.ldflags, - vec!["-Wl,--as-needed".to_string()] - ); - } - - #[test] - fn apply_config_reads_build_flag_appends_from_rootfs_build_toml() { - let tmp = tempfile::tempdir().unwrap(); - let config_path = tmp.path().join("etc/depot.d/build.toml"); - std::fs::create_dir_all(config_path.parent().unwrap()).unwrap(); - std::fs::write( - &config_path, - r#" -[flags] -cflags += ["-g"] -CXXFLAGS += ["-stdlib=libc++"] -LDFLAGS += "-Wl,--as-needed" -"#, - ) - .unwrap(); - - let config = crate::config::Config::for_rootfs(tmp.path()); - assert_eq!( - config.appends.get("build.flags.cflags").unwrap()[0] - .as_array() - .unwrap()[0] - .as_str(), - Some("-g") - ); - assert_eq!( - config.appends.get("build.flags.cxxflags").unwrap()[0] - .as_array() - .unwrap()[0] - .as_str(), - Some("-stdlib=libc++") - ); - assert_eq!( - config.appends.get("build.flags.ldflags").unwrap()[0].as_str(), - Some("-Wl,--as-needed") - ); - let mut spec = mk_spec("foo", "1.0"); - spec.apply_config(&config); - - assert!(spec.build.flags.cflags.contains(&"-g".to_string())); - assert!( - spec.build - .flags - .cxxflags - .contains(&"-stdlib=libc++".to_string()) - ); - assert!( - spec.build - .flags - .ldflags - .contains(&"-Wl,--as-needed".to_string()) - ); - } - - #[test] - fn parse_passthrough_env_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "custom" - -[build.flags] -passthrough_env = ["RUSTFLAGS", "CARGO_HOME"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.passthrough_env, - vec!["RUSTFLAGS".to_string(), "CARGO_HOME".to_string()] - ); - } - - #[test] - fn parse_env_vars_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "python" - -[build.flags] -env_vars = ["SETUPTOOLS_SCM_PRETEND_VERSION=$version", "PYO3_CONFIG_FILE=$specdir/pyo3.toml"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.env_vars, - vec![ - "SETUPTOOLS_SCM_PRETEND_VERSION=$version".to_string(), - "PYO3_CONFIG_FILE=$specdir/pyo3.toml".to_string() - ] - ); - } - - #[test] - fn parse_test_dependencies_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - - [dependencies] - build = ["make"] - test = ["python", "bats"] - optional = ["gtk-doc"] - "#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.dependencies.test, - vec!["python".to_string(), "bats".to_string()] - ); - assert_eq!(spec.dependencies.optional, vec!["gtk-doc".to_string()]); - } - - #[test] - fn parse_make_var_overrides_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[build.flags] -make_vars = ["V=1", "CC=clang"] -make_exec = "ninja" -make_target = "bootstrap" -make_targets = ["stage1", "stage2"] -make_dirs = ["lib", "libelf"] -make_test_vars = ["TESTS=unit"] -make_test_target = "test" -make_test_targets = ["test-unit", "test-integration"] -make_test_dirs = ["tests"] -make_install_vars = ["STRIPPROG=true"] -make_install_target = "install/strip" -make_install_targets = ["install-runtime", "install-devel"] -make_install_dirs = ["lib", "apps"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!( - spec.build.flags.make_vars, - vec!["V=1".to_string(), "CC=clang".to_string()] - ); - assert_eq!(spec.build.flags.make_exec, "ninja"); - assert_eq!(spec.build.flags.make_target, "bootstrap"); - assert_eq!( - spec.build.flags.make_targets, - vec!["stage1".to_string(), "stage2".to_string()] - ); - assert_eq!( - spec.build.flags.make_dirs, - vec!["lib".to_string(), "libelf".to_string()] - ); - assert_eq!( - spec.build.flags.make_test_vars, - vec!["TESTS=unit".to_string()] - ); - assert_eq!(spec.build.flags.make_test_target, "test".to_string()); - assert_eq!( - spec.build.flags.make_test_targets, - vec!["test-unit".to_string(), "test-integration".to_string()] - ); - assert_eq!(spec.build.flags.make_test_dirs, vec!["tests".to_string()]); - assert_eq!( - spec.build.flags.make_install_vars, - vec!["STRIPPROG=true".to_string()] - ); - assert_eq!( - spec.build.flags.make_install_target, - "install/strip".to_string() - ); - assert_eq!( - spec.build.flags.make_install_targets, - vec!["install-runtime".to_string(), "install-devel".to_string()] - ); - assert_eq!( - spec.build.flags.make_install_dirs, - vec!["lib".to_string(), "apps".to_string()] - ); - } - - #[test] - fn parse_makeflags_from_spec() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[source] -url = "https://example.com/foo.tar.gz" -sha256 = "skip" -extract_dir = "foo" - -[build] -type = "autotools" - -[build.flags] -MAKEFLAGS = ["-j12", "--output-sync=target"] -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - assert_eq!(spec.build.flags.makeflags, "-j12 --output-sync=target"); - } - - #[test] - fn test_chost_cbuild_overrides() { - let mut spec = mk_spec("foo", "1.0"); - let config = crate::config::Config { - cache_dir: "/tmp".into(), - build_dir: "/tmp".into(), - db_dir: "/tmp".into(), - build_overrides: toml::from_str( - r#" -chost = "x86_64-sfg-linux-gnu" -cbuild = "x86_64-pc-linux-gnu" -"#, - ) - .unwrap(), - package_overrides: toml::Value::Table(toml::map::Map::new()), - appends: std::collections::HashMap::new(), - repo_settings: crate::config::RepoSettings::default(), - source_repos: std::collections::BTreeMap::new(), - binary_repos: std::collections::BTreeMap::new(), - mirrors: std::collections::HashMap::new(), - repo_clone_dir: PathBuf::from("/tmp"), - package_cache_dir: PathBuf::from("/tmp"), - install_test_deps: false, - }; - - spec.apply_config(&config); - assert_eq!(spec.build.flags.chost, "x86_64-sfg-linux-gnu"); - assert_eq!(spec.build.flags.cbuild, "x86_64-pc-linux-gnu"); - } - - #[test] - fn test_default_and_override_carch() { - let mut spec = mk_spec("foo", "1.0"); - // Default should be host arch - assert_eq!(spec.build.flags.carch, std::env::consts::ARCH.to_string()); - - // Override via config - let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); - config.build_overrides = toml::from_str( - r#"[flags] -carch = "armv7" -"#, - ) - .unwrap(); - spec.apply_config(&config); - assert_eq!(spec.build.flags.carch, "armv7"); - } - - #[test] - fn test_package_filename() { - let mut spec = mk_spec("foo", "1.0"); - spec.package.revision = 2; - assert_eq!( - spec.package_filename("x86_64"), - "foo-1.0-2-x86_64.depot.pkg.tar.zst" - ); - } - - #[test] - fn parse_packages_array() { - let tmp = tempfile::tempdir().unwrap(); - let path = tmp.path().join("pkg.toml"); - - std::fs::write( - &path, - r#" -[package] -name = "foo" -version = "1.0" -description = "d" -homepage = "h" -license = "MIT" - -[[packages]] -name = "foo-dev" -version = "1.0" -description = "development files" -homepage = "h" -license = "MIT" - -[[source]] -url = "https://example.com/foo-1.0.tar.gz" -sha256 = "skip" -extract_dir = "foo-1.0" - -[build] -type = "custom" -"#, - ) - .unwrap(); - - let spec = PackageSpec::from_file(&path).unwrap(); - let outputs = spec.outputs(); - assert_eq!(outputs.len(), 2); - assert_eq!(outputs[0].name, "foo"); - assert_eq!(outputs[1].name, "foo-dev"); - } - - #[test] - fn docs_output_uses_runtime_dependency_on_parent_package() { - let mut spec = mk_spec("foo", "1.0"); - spec.build.flags.split_docs = true; - let docs_name = PackageSpec::docs_package_name("foo"); - - let deps = spec.dependencies_for_output(&docs_name); - assert_eq!(deps.runtime, vec!["foo".to_string()]); - - let alternatives = spec.alternatives_for_output(&docs_name); - assert!(alternatives.provides.is_empty()); - assert!(alternatives.conflicts.is_empty()); - } - - #[test] - fn docs_package_for_output_derives_name_and_description() { - let mut spec = mk_spec("foo", "1.0"); - spec.build.flags.split_docs = true; - - let docs = spec.docs_package_for_output(&spec.package); - assert_eq!(docs.name, "foo-docs"); - assert_eq!(docs.description, "Documentation for foo"); - assert_eq!(docs.version, "1.0"); - } - - fn mk_spec(name: &str, version: &str) -> PackageSpec { - PackageSpec { - package: PackageInfo { - name: name.into(), - real_name: None, - version: version.into(), - revision: 1, - description: "d".into(), - homepage: "h".into(), - abi_breaking: false, - license: vec!["MIT".into()], - }, - packages: Vec::new(), - alternatives: Alternatives::default(), - manual_sources: Vec::new(), - source: vec![Source { - url: "h".into(), - sha256: "s".into(), - extract_dir: "e".into(), - patches: Vec::new(), - post_extract: Vec::new(), - cherry_pick: Vec::new(), - }], - build: Build { - build_type: BuildType::Custom, - flags: BuildFlags::default(), - }, - dependencies: Dependencies::default(), - package_alternatives: Default::default(), - package_dependencies: Default::default(), - spec_dir: PathBuf::from("."), - } - } -} - -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(()) - } -} - -/// Package metadata -#[derive(Debug, Deserialize, serde::Serialize, Clone)] -pub struct PackageInfo { - pub name: String, - /// Stable package stream name used to associate renamed ABI-split packages. - #[serde(default, alias = "real-name", skip_serializing_if = "Option::is_none")] - pub real_name: Option, - pub version: String, - /// Maintenance revision of the package (defaults to 1) - #[serde(default = "default_revision")] - pub revision: u32, - pub description: String, - pub homepage: String, - /// When true, renamed updates retain versioned shared libraries from the old package. - #[serde(default, alias = "abi-breaking")] - pub abi_breaking: bool, - #[serde( - deserialize_with = "deserialize_licenses", - serialize_with = "serialize_licenses" - )] - pub license: Vec, -} - -impl PackageInfo { - /// Return the stable package stream name, defaulting to the package name. - pub fn effective_real_name(&self) -> &str { - self.real_name.as_deref().unwrap_or(&self.name) - } -} - -fn default_revision() -> u32 { - 1 -} - -fn deserialize_licenses<'de, D>(deserializer: D) -> std::result::Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrArray { - String(String), - Array(Vec), - } - - match StringOrArray::deserialize(deserializer)? { - StringOrArray::String(s) => Ok(vec![s]), - StringOrArray::Array(v) => Ok(v), - } -} - -fn serialize_licenses(licenses: &[String], serializer: S) -> std::result::Result -where - S: Serializer, -{ - if licenses.len() == 1 { - serializer.serialize_str(&licenses[0]) - } else { - licenses.serialize(serializer) - } -} - -impl PackageSpec { - /// Generate the standard package filename: ---.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 - ) - } -} - -/// Nested alternatives override group used for output-specific variants such as `lib32-*`. -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -pub struct AlternativeGroup { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub provides: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub conflicts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub replaces: Vec, -} - -/// Package alternatives such as virtual provides, install conflicts, and replacements. -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -pub struct Alternatives { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub provides: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub conflicts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub replaces: Vec, - /// Optional alternatives override used only for the generated `lib32-*` companion package. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lib32: Option, -} - -impl Alternatives { - /// Return the optional lib32-specific alternatives override set. - pub fn lib32_alternatives(&self) -> Option { - self.lib32.as_ref().map(|group| Alternatives { - provides: group.provides.clone(), - conflicts: group.conflicts.clone(), - replaces: group.replaces.clone(), - lib32: None, - }) - } -} - -/// Source tarball information -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct Source { - pub url: String, - /// Checksum for the source (e.g. `sha256:...`, `sha512:...`, `sha1:...`, `md5:...`, `b2:...`, `b2sum:...`, or raw SHA256 hex). - /// Defaults to `skip` when omitted. - #[serde(default = "default_source_sha256")] - pub sha256: String, - /// Directory name after extraction (supports $name, $version) - pub extract_dir: String, - - /// Patch files or URLs to apply after extraction. - /// - /// Example: - /// patches = ["fix-build.patch", ""] - #[serde(default)] - pub patches: Vec, - - /// Commands to run after extraction (and after patches), executed in the source directory. - /// - /// Example: - /// post_extract = ["autoreconf -fi"] - #[serde(default)] - pub post_extract: Vec, - - /// Optional list of git commit hashes/revs to cherry-pick after checkout. - /// - /// This is only valid for git sources (`*.git` URL or `url#rev` git form). - /// Example: - /// cherry_pick = ["a1b2c3d4", "deadbeef"] - #[serde( - default, - alias = "cherry-pick", - deserialize_with = "deserialize_string_or_array" - )] - pub cherry_pick: Vec, -} - -/// Manual source copied before standard source fetching. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct ManualSource { - /// Filename in the spec directory (local manual source mode). - #[serde(default)] - pub file: Option, - /// Multiple filenames in the spec directory (local manual source mode). - #[serde(default)] - pub files: Vec, - /// Remote URL to fetch or clone (remote manual source mode). - #[serde(default)] - pub url: Option, - /// Multiple remote URLs to fetch or clone (remote manual source mode). - #[serde(default)] - pub urls: Vec, - /// Checksum (optional, use "skip" to bypass verification). - #[serde(default)] - pub sha256: Option, - /// Destination path relative to build work directory. - /// Defaults to `file` for local mode, a derived filename for archive URLs, - /// or the repository directory name for git URLs. - #[serde(default)] - pub dest: Option, -} - -fn default_source_sha256() -> String { - "skip".to_string() -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum OneOrManySources { - One(Source), - Many(Vec), -} - -fn deserialize_sources<'de, D>(deserializer: D) -> std::result::Result, D::Error> -where - D: Deserializer<'de>, -{ - // Try to deserialize; if the field is missing/null, return empty vec - let parsed = Option::::deserialize(deserializer)?; - match parsed { - Some(OneOrManySources::One(s)) => Ok(vec![s]), - Some(OneOrManySources::Many(v)) => Ok(v), - None => Ok(Vec::new()), - } -} - -/// Build configuration -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct Build { - #[serde(rename = "type")] - pub build_type: BuildType, - #[serde(default)] - pub flags: BuildFlags, -} - -/// Supported build systems -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy)] -#[serde(rename_all = "lowercase")] -pub enum BuildType { - Autotools, - CMake, - Meson, - Perl, - Custom, - Python, - Rust, - Makefile, - Bin, - Meta, -} - -/// Build flags and toolchain configuration -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct BuildFlags { - /// Extra flags exported to `CFLAGS`. - #[serde(default, deserialize_with = "deserialize_string_or_array")] - pub cflags: Vec, - /// Ordered replacement rules applied to `cflags` before export. - /// - /// Each entry may use `old=>new`. Plain `old=new` is also accepted and - /// disambiguated against the current flag set when possible. - #[serde( - default, - alias = "replace-cflags", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_cflags: Vec, - /// Extra flags exported to `CFLAGS` only for the lib32 build variant. - #[serde( - default, - alias = "cflags-lib32", - alias = "cflags_lib32", - alias = "CFLAGS-lib32", - alias = "CFLAGS_lib32", - deserialize_with = "deserialize_string_or_array" - )] - pub cflags_lib32: Vec, - /// Ordered replacement rules applied to lib32-only `cflags`. - #[serde( - default, - alias = "replace-cflags-lib32", - alias = "replace_cflags-lib32", - alias = "replace_cflags_lib32", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_cflags_lib32: Vec, - /// Extra flags exported to `CXXFLAGS`. - #[serde(default, deserialize_with = "deserialize_string_or_array")] - pub cxxflags: Vec, - /// Ordered replacement rules applied to `cxxflags` before export. - #[serde( - default, - alias = "replace-cxxflags", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_cxxflags: Vec, - /// Extra flags exported to `CXXFLAGS` only for the lib32 build variant. - #[serde( - default, - alias = "cxxflags-lib32", - alias = "cxxflags_lib32", - alias = "CXXFLAGS-lib32", - alias = "CXXFLAGS_lib32", - deserialize_with = "deserialize_string_or_array" - )] - pub cxxflags_lib32: Vec, - /// Ordered replacement rules applied to lib32-only `cxxflags`. - #[serde( - default, - alias = "replace-cxxflags-lib32", - alias = "replace_cxxflags-lib32", - alias = "replace_cxxflags_lib32", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_cxxflags_lib32: Vec, - /// Extra flags exported to `LDFLAGS`. - #[serde(default, deserialize_with = "deserialize_string_or_array")] - pub ldflags: Vec, - /// Linker selected through compiler drivers with `-fuse-ld=`. - #[serde(default, alias = "fuse-ld")] - pub fuse_ld: String, - /// Ordered replacement rules applied to `ldflags` before export. - #[serde( - default, - alias = "replace-ldflags", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_ldflags: Vec, - /// Link-time optimization flags exported to `LTOFLAGS`. - /// - /// When `use_lto` is true (default), these flags are also appended to - /// `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`. - #[serde( - default, - alias = "lto-flags", - alias = "lto_flags", - alias = "LTOFLAGS", - deserialize_with = "deserialize_string_or_array" - )] - pub ltoflags: Vec, - /// Rust LTO flags exported to `RUSTLTOFLAGS`. - /// - /// When `use_lto` is true (default), these flags are also appended to - /// `RUSTFLAGS`. - #[serde( - default, - alias = "rust-ltoflags", - alias = "rust_ltoflags", - alias = "RUSTLTOFLAGS", - deserialize_with = "deserialize_string_or_array" - )] - pub rustltoflags: Vec, - /// Ordered replacement rules applied to `ltoflags` before export/injection. - #[serde( - default, - alias = "replace-ltoflags", - alias = "replace_lto-flags", - alias = "replace_lto_flags", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_ltoflags: Vec, - /// Keep existing files and install package-provided replacement as `.depotnew`. - #[serde(default, deserialize_with = "deserialize_string_or_array")] - pub keep: Vec, - /// Split documentation trees into a derived `-docs` output during staging. - #[serde( - default, - alias = "split-docs", - deserialize_with = "deserialize_boolish" - )] - pub split_docs: bool, - /// Additional documentation directories to move into `-docs`. - #[serde( - default, - alias = "doc-dirs", - alias = "doc_dirs", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub doc_dirs: Vec, - /// Disable automatic LTOFLAGS injection into CFLAGS/CXXFLAGS/LDFLAGS. - #[serde( - default = "default_use_lto", - alias = "use-lto", - deserialize_with = "deserialize_boolish" - )] - pub use_lto: bool, - /// Disable exporting CFLAGS/CXXFLAGS/LDFLAGS for this package build. - #[serde(default, alias = "no-flags")] - pub no_flags: bool, - /// Disable automatic stripping of ELF files during staging. - #[serde(default, alias = "no-strip")] - pub no_strip: bool, - /// Disable automatic deletion of static libraries (`*.a`) during staging. - #[serde( - default, - alias = "no-delete-static", - alias = "no_remove_static", - alias = "no-remove-static" - )] - pub no_delete_static: bool, - /// Disable automatic zstd compression of man pages during staging. - #[serde( - default, - alias = "no-compress-man", - alias = "no_compress_manpages", - alias = "no-compress-manpages" - )] - pub no_compress_man: bool, - /// Skip automatic build-system test execution (e.g. Autotools `make check`/`make test`). - /// - /// Automatic tests are also skipped for multilib (`build_32` / `lib32_only`) builds. - #[serde(default, alias = "skip-tests")] - pub skip_tests: bool, - /// Run an additional lib32 build pass and emit a `lib32-*` package. - #[serde( - default, - alias = "build-32", - alias = "build_32", - deserialize_with = "deserialize_boolish" - )] - pub build_32: bool, - /// Build/install only the generated `lib32-*` companion package output. - #[serde( - default, - alias = "lib32-only", - alias = "lib32_only", - deserialize_with = "deserialize_boolish" - )] - pub lib32_only: bool, - /// Perform an additional native host-side helper build when the active target arch differs. - #[serde( - default, - alias = "host-build", - alias = "host_build", - deserialize_with = "deserialize_boolish" - )] - pub host_build: bool, - #[serde(default)] - pub configure: Vec, - /// PEP 517 config settings for Python builds (each entry is `KEY=VALUE` or `KEY`). - #[serde( - default, - alias = "config-setting", - alias = "config-settings", - alias = "config_setting", - alias = "config_settings", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub config_settings: Vec, - /// Configure arguments used only for the lib32 build variant (replaces `configure` when set). - #[serde(default, alias = "configure-lib32", alias = "configure_lib32")] - pub configure_lib32: Vec, - /// Autotools configure script path, relative to source root or absolute. - #[serde(default, alias = "configure-file")] - pub configure_file: String, - /// Directory containing the configured compiler, linker, and binutils tools. - #[serde(default, alias = "tool-dir", alias = "tools_dir", alias = "tools-dir")] - pub tool_dir: String, - /// C compiler - #[serde(default = "default_cc")] - pub cc: String, - /// C++ compiler - #[serde(default = "default_cxx")] - pub cxx: String, - /// Archiver - #[serde(default = "default_ar")] - pub ar: String, - /// Archive indexer exported as `RANLIB` when configured. - #[serde(default)] - pub ranlib: String, - /// Strip executable exported as `STRIP` when configured. - #[serde(default)] - pub strip: String, - /// Linker executable or linker flavor override for supported builders. - #[serde(default)] - pub ld: String, - /// Symbol table dumper exported as `NM` when configured. - #[serde(default)] - pub nm: String, - /// Object copy tool exported as `OBJCOPY` when configured. - #[serde(default)] - pub objcopy: String, - /// Object dump tool exported as `OBJDUMP` when configured. - #[serde(default)] - pub objdump: String, - /// ELF reader exported as `READELF` when configured. - #[serde(default)] - pub readelf: String, - /// C preprocessor executable exported as `CPP` when configured. - #[serde(default, alias = "CPP")] - pub cpp: String, - /// Dynamic loader path - #[serde(default)] - pub libc: String, - /// Root filesystem for installation (per-package override) - #[serde(default = "default_rootfs")] - #[allow(dead_code)] - pub rootfs: String, - /// Commands to run after configure/setup step, before compile/build step. - #[serde(default, alias = "post-configure")] - pub post_configure: Vec, - /// Commands to run after configure/setup for the lib32 build variant. - #[serde( - default, - alias = "post-configure-lib32", - alias = "post_configure-lib32", - alias = "post_configure_lib32" - )] - pub post_configure_lib32: Vec, - /// Commands to run after compile (after make, before make install). - #[serde(default, alias = "post-compile")] - pub post_compile: Vec, - /// Commands to run after compile for the lib32 build variant. - #[serde( - default, - alias = "post-compile-lib32", - alias = "post_compile-lib32", - alias = "post_compile_lib32" - )] - pub post_compile_lib32: Vec, - /// Commands to run after install (after make install) - #[serde(default, alias = "post-install")] - pub post_install: Vec, - /// Commands to run after the lib32 install step (replaces `post_install` when set). - #[serde( - default, - alias = "post-install-lib32", - alias = "post_install-lib32", - alias = "post_install_lib32" - )] - pub post_install_lib32: Vec, - - /// Specific commands for 'makefile' build type - #[serde(default)] - pub makefile_commands: Vec, - #[serde(default)] - pub makefile_install_commands: Vec, - - /// Installation prefix (default: /usr) - #[serde(default = "default_prefix")] - pub prefix: String, - - /// Target architecture triple (CHOST equivalent) - #[serde(default)] - pub chost: String, - - /// Build architecture triple (CBUILD equivalent) - #[serde(default)] - pub cbuild: String, - - /// CPU architecture short name (CARCH equivalent), e.g. "x86_64", "aarch64" - #[serde(default = "default_carch")] - pub carch: String, - /// MAKEFLAGS environment variable passed to build commands. - #[serde( - default, - alias = "make-flags", - alias = "make_flags", - alias = "MAKEFLAGS", - deserialize_with = "deserialize_string_or_array_joined" - )] - pub makeflags: String, - /// Variable overrides passed directly to `make` (compile step), e.g. ["V=1", "CC=clang"]. - #[serde( - default, - alias = "make-vars", - alias = "make_build_vars", - alias = "make-build-vars", - deserialize_with = "deserialize_string_or_array" - )] - pub make_vars: Vec, - /// Make-like executable for build/test/install phases (default: `make`), e.g. `ninja`. - #[serde(default, alias = "make-exec")] - pub make_exec: String, - /// Target for the compile/build phase (e.g. `all`, `bootstrap`). - #[serde( - default, - alias = "make-target", - alias = "make_build_target", - alias = "make-build-target" - )] - pub make_target: String, - /// Targets for the compile/build phase (e.g. `["all", "bootstrap"]`). - #[serde( - default, - alias = "make-targets", - alias = "make_build_targets", - alias = "make-build-targets", - deserialize_with = "deserialize_string_or_array" - )] - pub make_targets: Vec, - /// Subdirectories (relative to build directory) where `make` should run. - #[serde( - default, - alias = "make-dirs", - deserialize_with = "deserialize_string_or_array" - )] - pub make_dirs: Vec, - /// Variable overrides passed directly to `make check` / `make test`. - #[serde( - default, - alias = "make-test-vars", - deserialize_with = "deserialize_string_or_array" - )] - pub make_test_vars: Vec, - /// Target for the test phase, passed to the make-like executable. - #[serde(default, alias = "make-test-target")] - pub make_test_target: String, - /// Targets for the test phase, passed to the make-like executable. - #[serde( - default, - alias = "make-test-targets", - deserialize_with = "deserialize_string_or_array" - )] - pub make_test_targets: Vec, - /// Subdirectories (relative to build directory) where test targets should run. - #[serde( - default, - alias = "make-test-dirs", - deserialize_with = "deserialize_string_or_array" - )] - pub make_test_dirs: Vec, - /// Variable overrides passed directly to `make install`. - #[serde( - default, - alias = "make-install-vars", - deserialize_with = "deserialize_string_or_array" - )] - pub make_install_vars: Vec, - /// Target for the install phase (default: `install`). - #[serde(default, alias = "make-install-target")] - pub make_install_target: String, - /// Targets for the install phase. - #[serde( - default, - alias = "make-install-targets", - deserialize_with = "deserialize_string_or_array" - )] - pub make_install_targets: Vec, - /// Subdirectories (relative to build directory) where `make install` should run. - #[serde( - default, - alias = "make-install-dirs", - deserialize_with = "deserialize_string_or_array" - )] - pub make_install_dirs: Vec, - /// Additional host environment variable names to export unchanged to build commands. - /// Example: ["RUSTFLAGS", "CARGO_HOME"]. - #[serde( - default, - alias = "passthrough-env", - alias = "pass_env", - alias = "pass-env", - alias = "export_env", - alias = "export-env", - deserialize_with = "deserialize_string_or_array" - )] - pub passthrough_env: Vec, - /// Explicit environment variable assignments exported to build commands. - /// Each entry must be `KEY=VALUE`. - #[serde( - default, - alias = "env-vars", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub env_vars: Vec, - - // Rust-specific fields - /// Rust build profile: "debug" or "release" (default: release) - #[serde(default = "default_profile")] - pub profile: String, - /// Rust target triple (e.g., x86_64-unknown-linux-musl). Optional. - #[serde(default)] - pub target: String, - /// RUSTFLAGS environment variable - #[serde(default, deserialize_with = "deserialize_string_or_array")] - pub rustflags: Vec, - /// Ordered replacement rules applied to `rustflags` before export. - #[serde( - default, - alias = "replace-rustflags", - deserialize_with = "deserialize_string_or_array_no_split" - )] - pub replace_rustflags: Vec, - /// Additional cargo arguments (short name) - #[serde(default)] - pub cargs: Vec, - /// Binary installation directory relative to DESTDIR (default: /usr/bin) - #[serde(default = "default_bindir")] - pub bindir: String, - /// System binary installation directory for supported builders (default: /usr/bin). - #[serde(default)] - pub sbindir: String, - /// Library installation directory for supported builders. - /// - /// Defaults to `/usr/lib`, or `/usr/lib32` for the lib32 build variant. - #[serde(default)] - pub libdir: String, - /// Library helper executable installation directory for supported builders. - /// - /// Defaults to the effective `libdir`. - #[serde(default)] - pub libexecdir: String, - /// System configuration directory for supported builders (default: /etc). - #[serde(default)] - pub sysconfdir: String, - /// Variable state directory for supported builders (default: /var). - #[serde(default)] - pub localstatedir: String, - /// Shared variable state directory for supported builders (default: /var/lib). - #[serde(default)] - pub sharedstatedir: String, - /// Header installation directory for supported builders (default: /usr/include). - #[serde(default)] - pub includedir: String, - /// Data root installation directory for supported builders (default: /usr/share). - #[serde(default)] - pub datarootdir: String, - /// Architecture-independent data installation directory for supported builders. - /// - /// Defaults to the effective `datarootdir`. - #[serde(default)] - pub datadir: String, - /// Manual page installation directory for supported builders (default: /usr/share/man). - #[serde(default)] - pub mandir: String, - /// Info page installation directory for supported builders (default: /usr/share/info). - #[serde(default)] - pub infodir: String, - - /// Subdirectory within extracted source to use as the actual source root. - /// Useful for monorepos like llvm-project where you want to build just one component. - #[serde(default)] - pub source_subdir: String, - /// Build directory relative to source root (e.g. "build") - #[serde(default, skip_serializing_if = "Option::is_none")] - pub build_dir: Option, - /// Binary package type when using BuildType::Bin (e.g. "deb") - #[serde(default)] - pub binary_type: String, - /// Internal runtime marker used to adjust builder behavior for the lib32 variant. - #[serde(skip)] - pub lib32_variant: bool, - /// Internal runtime marker containing the absolute path to the native host helper build dir. - #[serde(skip)] - pub host_build_dir: Option, -} - -impl Default for BuildFlags { - fn default() -> Self { - BuildFlags { - cflags: Vec::new(), - replace_cflags: Vec::new(), - cflags_lib32: Vec::new(), - replace_cflags_lib32: Vec::new(), - cxxflags: Vec::new(), - replace_cxxflags: Vec::new(), - cxxflags_lib32: Vec::new(), - replace_cxxflags_lib32: Vec::new(), - ldflags: Vec::new(), - fuse_ld: String::new(), - replace_ldflags: Vec::new(), - ltoflags: Vec::new(), - rustltoflags: Vec::new(), - replace_ltoflags: Vec::new(), - keep: Vec::new(), - split_docs: false, - doc_dirs: Vec::new(), - use_lto: default_use_lto(), - no_flags: false, - no_strip: false, - no_delete_static: false, - no_compress_man: false, - skip_tests: false, - build_32: false, - lib32_only: false, - host_build: false, - configure: Vec::new(), - config_settings: Vec::new(), - configure_lib32: Vec::new(), - configure_file: String::new(), - tool_dir: String::new(), - cc: default_cc(), - cxx: default_cxx(), - ar: default_ar(), - ranlib: String::new(), - strip: String::new(), - ld: String::new(), - nm: String::new(), - objcopy: String::new(), - objdump: String::new(), - readelf: String::new(), - cpp: String::new(), - libc: String::new(), - rootfs: default_rootfs(), - post_configure: Vec::new(), - post_configure_lib32: Vec::new(), - post_compile: Vec::new(), - post_compile_lib32: Vec::new(), - post_install: Vec::new(), - post_install_lib32: Vec::new(), - makefile_commands: Vec::new(), - makefile_install_commands: Vec::new(), - prefix: default_prefix(), - chost: String::new(), - cbuild: String::new(), - carch: default_carch(), - makeflags: String::new(), - make_vars: Vec::new(), - make_exec: String::new(), - make_target: String::new(), - make_targets: Vec::new(), - make_dirs: Vec::new(), - make_test_vars: Vec::new(), - make_test_target: String::new(), - make_test_targets: Vec::new(), - make_test_dirs: Vec::new(), - make_install_vars: Vec::new(), - make_install_target: String::new(), - make_install_targets: Vec::new(), - make_install_dirs: Vec::new(), - passthrough_env: Vec::new(), - env_vars: Vec::new(), - profile: default_profile(), - target: String::new(), - rustflags: Vec::new(), - replace_rustflags: Vec::new(), - cargs: Vec::new(), - bindir: default_bindir(), - sbindir: String::new(), - libdir: String::new(), - libexecdir: String::new(), - sysconfdir: String::new(), - localstatedir: String::new(), - sharedstatedir: String::new(), - includedir: String::new(), - datarootdir: String::new(), - datadir: String::new(), - mandir: String::new(), - infodir: String::new(), - source_subdir: String::new(), - build_dir: None, - binary_type: String::new(), - lib32_variant: false, - host_build_dir: None, - } - } -} - -fn deserialize_string_or_array<'de, D>( - deserializer: D, -) -> std::result::Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrArray { - String(String), - Array(Vec), - } - - match Option::::deserialize(deserializer)? { - Some(StringOrArray::String(s)) => Ok(s.split_whitespace().map(String::from).collect()), - Some(StringOrArray::Array(a)) => Ok(a), - None => Ok(Vec::new()), - } -} - -fn deserialize_string_or_array_no_split<'de, D>( - deserializer: D, -) -> std::result::Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrArray { - String(String), - Array(Vec), - } - - match Option::::deserialize(deserializer)? { - Some(StringOrArray::String(s)) => Ok(vec![s]), - Some(StringOrArray::Array(a)) => Ok(a), - None => Ok(Vec::new()), - } -} - -fn deserialize_string_or_array_joined<'de, D>( - deserializer: D, -) -> std::result::Result -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrArray { - String(String), - Array(Vec), - } - - match Option::::deserialize(deserializer)? { - Some(StringOrArray::String(s)) => Ok(s), - Some(StringOrArray::Array(a)) => Ok(a - .iter() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect::>() - .join(" ")), - None => Ok(String::new()), - } -} - -fn deserialize_boolish<'de, D>(deserializer: D) -> std::result::Result -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum Boolish { - Bool(bool), - String(String), - } - - match Option::::deserialize(deserializer)? { - Some(Boolish::Bool(v)) => Ok(v), - Some(Boolish::String(s)) => match s.trim().to_ascii_lowercase().as_str() { - "true" | "1" | "yes" | "on" => Ok(true), - "false" | "0" | "no" | "off" => Ok(false), - other => Err(serde::de::Error::custom(format!( - "expected boolean string for lib32 flag, got '{}'", - other - ))), - }, - None => Ok(false), - } -} - -fn toml_value_as_boolish(value: &toml::Value) -> Option { - if let Some(b) = value.as_bool() { - return Some(b); - } - value - .as_str() - .and_then(|s| match s.trim().to_ascii_lowercase().as_str() { - "true" | "1" | "yes" | "on" => Some(true), - "false" | "0" | "no" | "off" => Some(false), - _ => None, - }) -} - -fn append_whitespace_separated(dst: &mut String, value: &str) { - let trimmed = value.trim(); - if trimmed.is_empty() { - return; - } - if dst.is_empty() { - dst.push_str(trimmed); - } else { - dst.push(' '); - dst.push_str(trimmed); - } -} - -fn default_cc() -> String { - // Prefer clang if available (supports -print-resource-dir and other useful flags) - if std::process::Command::new("which") - .arg("clang") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return "clang".to_string(); - } - "gcc".to_string() -} - -fn default_use_lto() -> bool { - true -} - -fn default_ar() -> String { - "ar".to_string() -} - -fn default_rootfs() -> String { - "/".to_string() -} - -fn default_profile() -> String { - "release".to_string() -} - -fn default_bindir() -> String { - "/usr/bin".to_string() -} - -fn default_prefix() -> String { - "/usr".to_string() -} - -fn default_carch() -> String { - std::env::consts::ARCH.to_string() -} - -fn default_cxx() -> String { - // Infer a sensible C++ compiler name from default_cc() - let cc = default_cc(); - if cc.contains("clang") { - "clang++".to_string() - } else { - "g++".to_string() - } -} - -/// Nested dependency override group for a specific output variant. -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -pub struct DependencyGroup { - /// Dependencies required for building packages. - #[serde(default)] - pub build: Vec, - /// Dependencies required at runtime. - #[serde(default)] - pub runtime: Vec, - /// Dependencies required to run package test suites. - #[serde(default)] - pub test: Vec, - /// Optional runtime integrations that enhance functionality when installed. - #[serde(default)] - pub optional: Vec, - /// Package groups associated with this package output. - #[serde(default)] - pub groups: Vec, -} - -impl DependencyGroup { - fn to_dependencies(&self) -> Dependencies { - Dependencies { - build: self.build.clone(), - runtime: self.runtime.clone(), - test: self.test.clone(), - optional: self.optional.clone(), - groups: self.groups.clone(), - lib32: None, - } - } -} - -/// Package dependencies -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -pub struct Dependencies { - /// Dependencies required for building packages. - #[serde(default)] - pub build: Vec, - /// Dependencies required at runtime. - #[serde(default)] - pub runtime: Vec, - /// Dependencies required to run package test suites. - #[serde(default)] - pub test: Vec, - /// Optional runtime integrations that enhance functionality when installed. - #[serde(default)] - pub optional: Vec, - /// Package groups associated with this package. - #[serde(default)] - pub groups: Vec, - /// Optional dependency overrides used only for the generated `lib32-*` companion package. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lib32: Option, -} - -impl Dependencies { - /// Return the top-level dependency set without any nested output-specific overrides. - pub fn primary_dependencies(&self) -> Dependencies { - Dependencies { - build: self.build.clone(), - runtime: self.runtime.clone(), - test: self.test.clone(), - optional: self.optional.clone(), - groups: self.groups.clone(), - lib32: None, - } - } - - /// Return the optional lib32-specific dependency override set. - pub fn lib32_dependencies(&self) -> Option { - self.lib32.as_ref().map(DependencyGroup::to_dependencies) - } -} diff --git a/src/package/spec/config.rs b/src/package/spec/config.rs new file mode 100644 index 0000000..d004509 --- /dev/null +++ b/src/package/spec/config.rs @@ -0,0 +1,1570 @@ +use super::loading::PackageSpec; +use super::model::*; +use anyhow::{Context, Result}; + +impl PackageSpec { + /// Apply system configuration overrides and appends + pub fn apply_config(&mut self, config: &crate::config::Config) { + // Apply build overrides from /etc/depot.d/build.toml + self.apply_toml_overrides(&config.build_overrides, "build"); + + // Apply appends from /etc/depot.d/build.toml (e.g. build.flags.cflags += ["-O3"]) + for (key, values) in &config.appends { + let key = normalize_append_key(key); + if let Some(subkey) = key.strip_prefix("build.flags.") { + self.apply_append(subkey, values); + } else if let Some(subkey) = key.strip_prefix("build.") { + self.apply_append(subkey, values); + } + } + } + + fn apply_toml_overrides(&mut self, overrides: &toml::Value, _prefix: &str) { + // Support both [build.flags] and top-level [build] fields + if let Some(table) = overrides.as_table() { + self.apply_flags_table(table); + } + if let Some(table) = overrides.get("flags").and_then(|f| f.as_table()) { + self.apply_flags_table(table); + } + } + + fn apply_default_string(target: &mut String, default: &str, value: &toml::Value) { + if let Some(s) = value.as_str() + && (target.trim().is_empty() || target == default) + { + *target = s.to_string(); + } + } + + fn apply_default_bool(target: &mut bool, default: bool, value: &toml::Value) { + if *target == default + && let Some(value) = toml_value_as_boolish(value) + { + *target = value; + } + } + + fn apply_flags_table(&mut self, table: &toml::map::Map) { + let default_flags = BuildFlags::default(); + for (k, v) in table { + // match case-insensitively for common keys (allow CXX/Cc etc.) + match k.to_lowercase().as_str() { + "cflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.cflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.cflags = vec![s.to_string()]; + } + } + "replace_cflags" | "replace-cflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_cflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cflags = vec![s.to_string()]; + } + } + "cflags-lib32" | "cflags_lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.cflags_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.cflags_lib32 = vec![s.to_string()]; + } + } + "replace_cflags-lib32" | "replace_cflags_lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_cflags_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cflags_lib32 = vec![s.to_string()]; + } + } + "cxxflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.cxxflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.cxxflags = vec![s.to_string()]; + } + } + "replace_cxxflags" | "replace-cxxflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_cxxflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cxxflags = vec![s.to_string()]; + } + } + "cxxflags-lib32" | "cxxflags_lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.cxxflags_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.cxxflags_lib32 = vec![s.to_string()]; + } + } + "replace_cxxflags-lib32" | "replace_cxxflags_lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_cxxflags_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cxxflags_lib32 = vec![s.to_string()]; + } + } + "ldflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.ldflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.ldflags = vec![s.to_string()]; + } + } + "fuse_ld" | "fuse-ld" => { + Self::apply_default_string( + &mut self.build.flags.fuse_ld, + &default_flags.fuse_ld, + v, + ); + } + "tool_dir" | "tool-dir" | "tools_dir" | "tools-dir" => { + Self::apply_default_string( + &mut self.build.flags.tool_dir, + &default_flags.tool_dir, + v, + ); + } + "replace_ldflags" | "replace-ldflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_ldflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_ldflags = vec![s.to_string()]; + } + } + "ltoflags" | "lto_flags" | "lto-flags" => { + if let Some(arr) = v.as_array() { + self.build.flags.ltoflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.ltoflags = vec![s.to_string()]; + } + } + "rustltoflags" | "rust_ltoflags" | "rust-ltoflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.rustltoflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.rustltoflags = vec![s.to_string()]; + } + } + "replace_ltoflags" | "replace_lto-flags" | "replace_lto_flags" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_ltoflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_ltoflags = vec![s.to_string()]; + } + } + "rustflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.rustflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.rustflags = vec![s.to_string()]; + } + } + "replace_rustflags" | "replace-rustflags" => { + if let Some(arr) = v.as_array() { + self.build.flags.replace_rustflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_rustflags = vec![s.to_string()]; + } + } + "keep" => { + if let Some(arr) = v.as_array() { + self.build.flags.keep = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.keep = vec![s.to_string()]; + } + } + "split_docs" | "split-docs" => { + if let Some(b) = toml_value_as_boolish(v) { + self.build.flags.split_docs = b; + } + } + "doc_dirs" | "doc-dirs" => { + if let Some(arr) = v.as_array() { + self.build.flags.doc_dirs = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.doc_dirs = vec![s.to_string()]; + } + } + "cc" => { + Self::apply_default_string(&mut self.build.flags.cc, &default_flags.cc, v); + } + "cxx" => { + Self::apply_default_string(&mut self.build.flags.cxx, &default_flags.cxx, v); + } + "ar" => { + Self::apply_default_string(&mut self.build.flags.ar, &default_flags.ar, v); + } + "ranlib" => { + Self::apply_default_string( + &mut self.build.flags.ranlib, + &default_flags.ranlib, + v, + ); + } + "strip" => { + Self::apply_default_string( + &mut self.build.flags.strip, + &default_flags.strip, + v, + ); + } + "ld" => { + Self::apply_default_string(&mut self.build.flags.ld, &default_flags.ld, v); + } + "nm" => { + Self::apply_default_string(&mut self.build.flags.nm, &default_flags.nm, v); + } + "objcopy" => { + Self::apply_default_string( + &mut self.build.flags.objcopy, + &default_flags.objcopy, + v, + ); + } + "objdump" => { + Self::apply_default_string( + &mut self.build.flags.objdump, + &default_flags.objdump, + v, + ); + } + "readelf" => { + Self::apply_default_string( + &mut self.build.flags.readelf, + &default_flags.readelf, + v, + ); + } + "cpp" => { + Self::apply_default_string(&mut self.build.flags.cpp, &default_flags.cpp, v); + } + "prefix" => { + Self::apply_default_string( + &mut self.build.flags.prefix, + &default_flags.prefix, + v, + ); + } + "bindir" => { + Self::apply_default_string( + &mut self.build.flags.bindir, + &default_flags.bindir, + v, + ); + } + "sbindir" => { + Self::apply_default_string( + &mut self.build.flags.sbindir, + &default_flags.sbindir, + v, + ); + } + "libdir" => { + Self::apply_default_string( + &mut self.build.flags.libdir, + &default_flags.libdir, + v, + ); + } + "libexecdir" => { + Self::apply_default_string( + &mut self.build.flags.libexecdir, + &default_flags.libexecdir, + v, + ); + } + "sysconfdir" => { + Self::apply_default_string( + &mut self.build.flags.sysconfdir, + &default_flags.sysconfdir, + v, + ); + } + "localstatedir" => { + Self::apply_default_string( + &mut self.build.flags.localstatedir, + &default_flags.localstatedir, + v, + ); + } + "sharedstatedir" => { + Self::apply_default_string( + &mut self.build.flags.sharedstatedir, + &default_flags.sharedstatedir, + v, + ); + } + "includedir" => { + Self::apply_default_string( + &mut self.build.flags.includedir, + &default_flags.includedir, + v, + ); + } + "datarootdir" => { + Self::apply_default_string( + &mut self.build.flags.datarootdir, + &default_flags.datarootdir, + v, + ); + } + "datadir" => { + Self::apply_default_string( + &mut self.build.flags.datadir, + &default_flags.datadir, + v, + ); + } + "mandir" => { + Self::apply_default_string( + &mut self.build.flags.mandir, + &default_flags.mandir, + v, + ); + } + "infodir" => { + Self::apply_default_string( + &mut self.build.flags.infodir, + &default_flags.infodir, + v, + ); + } + "chost" => { + Self::apply_default_string( + &mut self.build.flags.chost, + &default_flags.chost, + v, + ); + } + "cbuild" => { + Self::apply_default_string( + &mut self.build.flags.cbuild, + &default_flags.cbuild, + v, + ); + } + "carch" => { + Self::apply_default_string( + &mut self.build.flags.carch, + &default_flags.carch, + v, + ); + } + "makeflags" | "make_flags" | "make-flags" => { + if let Some(s) = v.as_str() { + self.build.flags.makeflags = s.to_string(); + } else if let Some(arr) = v.as_array() { + self.build.flags.makeflags = arr + .iter() + .filter_map(|x| x.as_str()) + .map(str::trim) + .filter(|x| !x.is_empty()) + .collect::>() + .join(" "); + } + } + "make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_vars = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_vars = + s.split_whitespace().map(String::from).collect(); + } + } + "make_exec" | "make-exec" => { + if let Some(s) = v.as_str() { + self.build.flags.make_exec = s.to_string(); + } + } + "make_target" | "make-target" | "make_build_target" | "make-build-target" => { + if let Some(s) = v.as_str() { + self.build.flags.make_target = s.to_string(); + } + } + "make_targets" | "make-targets" | "make_build_targets" | "make-build-targets" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_targets = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_targets = + s.split_whitespace().map(String::from).collect(); + } + } + "make_dirs" | "make-dirs" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_dirs = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_dirs = + s.split_whitespace().map(String::from).collect(); + } + } + "make_test_vars" | "make-test-vars" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_test_vars = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_test_vars = + s.split_whitespace().map(String::from).collect(); + } + } + "make_test_target" | "make-test-target" => { + if let Some(s) = v.as_str() { + self.build.flags.make_test_target = s.to_string(); + } + } + "make_test_targets" | "make-test-targets" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_test_targets = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_test_targets = + s.split_whitespace().map(String::from).collect(); + } + } + "make_test_dirs" | "make-test-dirs" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_test_dirs = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_test_dirs = + s.split_whitespace().map(String::from).collect(); + } + } + "make_install_vars" | "make-install-vars" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_install_vars = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_install_vars = + s.split_whitespace().map(String::from).collect(); + } + } + "make_install_target" | "make-install-target" => { + if let Some(s) = v.as_str() { + self.build.flags.make_install_target = s.to_string(); + } + } + "make_install_targets" | "make-install-targets" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_install_targets = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_install_targets = + s.split_whitespace().map(String::from).collect(); + } + } + "make_install_dirs" | "make-install-dirs" => { + if let Some(arr) = v.as_array() { + self.build.flags.make_install_dirs = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.make_install_dirs = + s.split_whitespace().map(String::from).collect(); + } + } + "passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env" + | "export-env" => { + if let Some(arr) = v.as_array() { + self.build.flags.passthrough_env = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.passthrough_env = + s.split_whitespace().map(String::from).collect(); + } + } + "env_vars" | "env-vars" => { + if let Some(arr) = v.as_array() { + self.build.flags.env_vars = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.env_vars = vec![s.to_string()]; + } + } + "no_flags" | "no-flags" => { + if let Some(b) = v.as_bool() { + self.build.flags.no_flags = b; + } + } + "use_lto" | "use-lto" => { + Self::apply_default_bool( + &mut self.build.flags.use_lto, + default_flags.use_lto, + v, + ); + } + "no_strip" | "no-strip" => { + if let Some(b) = v.as_bool() { + self.build.flags.no_strip = b; + } + } + "no_delete_static" | "no-delete-static" => { + if let Some(b) = v.as_bool() { + self.build.flags.no_delete_static = b; + } + } + "no_compress_man" + | "no-compress-man" + | "no_compress_manpages" + | "no-compress-manpages" => { + if let Some(b) = v.as_bool() { + self.build.flags.no_compress_man = b; + } + } + "skip_tests" | "skip-tests" => { + if let Some(b) = v.as_bool() { + self.build.flags.skip_tests = b; + } + } + "build_32" | "build-32" => { + if let Some(b) = toml_value_as_boolish(v) { + self.build.flags.build_32 = b; + } + } + "lib32_only" | "lib32-only" => { + if let Some(b) = toml_value_as_boolish(v) { + self.build.flags.lib32_only = b; + } + } + "host_build" | "host-build" => { + if let Some(b) = toml_value_as_boolish(v) { + self.build.flags.host_build = b; + } + } + "configure_lib32" | "configure-lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.configure_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.configure_lib32 = vec![s.to_string()]; + } + } + "config_setting" | "config_settings" | "config-setting" | "config-settings" => { + if let Some(arr) = v.as_array() { + self.build.flags.config_settings = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.config_settings = vec![s.to_string()]; + } + } + "configure_file" | "configure-file" => { + if let Some(s) = v.as_str() { + self.build.flags.configure_file = s.to_string(); + } + } + "post_configure" | "post-configure" => { + if let Some(arr) = v.as_array() { + self.build.flags.post_configure = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.post_configure = vec![s.to_string()]; + } + } + "post_configure_lib32" | "post_configure-lib32" | "post-configure-lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.post_configure_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.post_configure_lib32 = vec![s.to_string()]; + } + } + "post_compile" | "post-compile" => { + if let Some(arr) = v.as_array() { + self.build.flags.post_compile = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.post_compile = vec![s.to_string()]; + } + } + "post_compile_lib32" | "post_compile-lib32" | "post-compile-lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.post_compile_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.post_compile_lib32 = vec![s.to_string()]; + } + } + "post_install" | "post-install" => { + if let Some(arr) = v.as_array() { + self.build.flags.post_install = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.post_install = vec![s.to_string()]; + } + } + "post_install_lib32" | "post_install-lib32" | "post-install-lib32" => { + if let Some(arr) = v.as_array() { + self.build.flags.post_install_lib32 = arr + .iter() + .filter_map(|x| x.as_str()) + .map(String::from) + .collect(); + } else if let Some(s) = v.as_str() { + self.build.flags.post_install_lib32 = vec![s.to_string()]; + } + } + // Add more fields as needed + _ => {} + } + } + } + + pub(super) fn apply_append(&mut self, key: &str, values: &[toml::Value]) { + let key = normalize_append_key(key); + match key.as_str() { + "cflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .cflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.cflags.push(s.to_string()); + } + } + } + "replace_cflags" | "replace-cflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_cflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cflags.push(s.to_string()); + } + } + } + "cflags-lib32" | "cflags_lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .cflags_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.cflags_lib32.push(s.to_string()); + } + } + } + "replace_cflags-lib32" | "replace_cflags_lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_cflags_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cflags_lib32.push(s.to_string()); + } + } + } + "cxxflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .cxxflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.cxxflags.push(s.to_string()); + } + } + } + "replace_cxxflags" | "replace-cxxflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_cxxflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cxxflags.push(s.to_string()); + } + } + } + "cxxflags-lib32" | "cxxflags_lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .cxxflags_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.cxxflags_lib32.push(s.to_string()); + } + } + } + "replace_cxxflags-lib32" | "replace_cxxflags_lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_cxxflags_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_cxxflags_lib32.push(s.to_string()); + } + } + } + "ldflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .ldflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.ldflags.push(s.to_string()); + } + } + } + "replace_ldflags" | "replace-ldflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_ldflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_ldflags.push(s.to_string()); + } + } + } + "ltoflags" | "lto_flags" | "lto-flags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .ltoflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.ltoflags.push(s.to_string()); + } + } + } + "rustltoflags" | "rust_ltoflags" | "rust-ltoflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .rustltoflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.rustltoflags.push(s.to_string()); + } + } + } + "replace_ltoflags" | "replace_lto-flags" | "replace_lto_flags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_ltoflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_ltoflags.push(s.to_string()); + } + } + } + "keep" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .keep + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.keep.push(s.to_string()); + } + } + } + "doc_dirs" | "doc-dirs" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .doc_dirs + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.doc_dirs.push(s.to_string()); + } + } + } + "configure" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .configure + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.configure.push(s.to_string()); + } + } + } + key if let Some(arch) = configure_arch_append_key(key) => { + let args = self + .build + .flags + .configure_arch + .entry(arch.to_string()) + .or_default(); + append_string_values(args, values); + } + "configure_lib32" | "configure-lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .configure_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.configure_lib32.push(s.to_string()); + } + } + } + "config_setting" | "config_settings" | "config-setting" | "config-settings" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .config_settings + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.config_settings.push(s.to_string()); + } + } + } + "configure_file" | "configure-file" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.configure_file = s.to_string(); + } + } + "post_configure" | "post-configure" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .post_configure + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.post_configure.push(s.to_string()); + } + } + } + "post_configure_lib32" | "post_configure-lib32" | "post-configure-lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .post_configure_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.post_configure_lib32.push(s.to_string()); + } + } + } + "post_compile" | "post-compile" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .post_compile + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.post_compile.push(s.to_string()); + } + } + } + "post_compile_lib32" | "post_compile-lib32" | "post-compile-lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .post_compile_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.post_compile_lib32.push(s.to_string()); + } + } + } + "post_install" | "post-install" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .post_install + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.post_install.push(s.to_string()); + } + } + } + "post_install_lib32" | "post_install-lib32" | "post-install-lib32" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .post_install_lib32 + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.post_install_lib32.push(s.to_string()); + } + } + } + "cargs" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .cargs + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.cargs.push(s.to_string()); + } + } + } + "rustflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .rustflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.rustflags.push(s.to_string()); + } + } + } + "replace_rustflags" | "replace-rustflags" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .replace_rustflags + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.replace_rustflags.push(s.to_string()); + } + } + } + "cc" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.cc = s.to_string(); + } + } + "cxx" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.cxx = s.to_string(); + } + } + "ar" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.ar = s.to_string(); + } + } + "ranlib" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.ranlib = s.to_string(); + } + } + "strip" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.strip = s.to_string(); + } + } + "ld" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.ld = s.to_string(); + } + } + "nm" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.nm = s.to_string(); + } + } + "objcopy" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.objcopy = s.to_string(); + } + } + "objdump" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.objdump = s.to_string(); + } + } + "readelf" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.readelf = s.to_string(); + } + } + "cpp" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.cpp = s.to_string(); + } + } + "prefix" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.prefix = s.to_string(); + } + } + "bindir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.bindir = s.to_string(); + } + } + "sbindir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.sbindir = s.to_string(); + } + } + "libdir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.libdir = s.to_string(); + } + } + "libexecdir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.libexecdir = s.to_string(); + } + } + "sysconfdir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.sysconfdir = s.to_string(); + } + } + "localstatedir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.localstatedir = s.to_string(); + } + } + "sharedstatedir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.sharedstatedir = s.to_string(); + } + } + "includedir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.includedir = s.to_string(); + } + } + "datarootdir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.datarootdir = s.to_string(); + } + } + "datadir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.datadir = s.to_string(); + } + } + "mandir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.mandir = s.to_string(); + } + } + "infodir" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.infodir = s.to_string(); + } + } + "chost" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.chost = s.to_string(); + } + } + "cbuild" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.cbuild = s.to_string(); + } + } + "carch" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.carch = s.to_string(); + } + } + "makeflags" | "make_flags" | "make-flags" | "MAKEFLAGS" => { + for v in values { + if let Some(arr) = v.as_array() { + let joined = arr + .iter() + .filter_map(|x| x.as_str()) + .map(str::trim) + .filter(|x| !x.is_empty()) + .collect::>() + .join(" "); + append_whitespace_separated(&mut self.build.flags.makeflags, &joined); + } else if let Some(s) = v.as_str() { + append_whitespace_separated(&mut self.build.flags.makeflags, s); + } + } + } + "make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_vars + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_vars + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_exec" | "make-exec" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.make_exec = s.to_string(); + } + } + "make_target" | "make-target" | "make_build_target" | "make-build-target" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.make_target = s.to_string(); + } + } + "make_targets" | "make-targets" | "make_build_targets" | "make-build-targets" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_targets + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_targets + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_dirs" | "make-dirs" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_dirs + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_dirs + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_test_vars" | "make-test-vars" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_test_vars + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_test_vars + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_test_target" | "make-test-target" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.make_test_target = s.to_string(); + } + } + "make_test_targets" | "make-test-targets" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_test_targets + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_test_targets + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_test_dirs" | "make-test-dirs" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_test_dirs + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_test_dirs + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_install_vars" | "make-install-vars" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_install_vars + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_install_vars + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_install_target" | "make-install-target" => { + if let Some(s) = values.last().and_then(|v| v.as_str()) { + self.build.flags.make_install_target = s.to_string(); + } + } + "make_install_targets" | "make-install-targets" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_install_targets + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_install_targets + .extend(s.split_whitespace().map(String::from)); + } + } + } + "make_install_dirs" | "make-install-dirs" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .make_install_dirs + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .make_install_dirs + .extend(s.split_whitespace().map(String::from)); + } + } + } + "passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env" + | "export-env" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .passthrough_env + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build + .flags + .passthrough_env + .extend(s.split_whitespace().map(String::from)); + } + } + } + "env_vars" | "env-vars" => { + for v in values { + if let Some(arr) = v.as_array() { + self.build + .flags + .env_vars + .extend(arr.iter().filter_map(|x| x.as_str()).map(String::from)); + } else if let Some(s) = v.as_str() { + self.build.flags.env_vars.push(s.to_string()); + } + } + } + "no_flags" | "no-flags" => { + if let Some(b) = values.last().and_then(|v| v.as_bool()) { + self.build.flags.no_flags = b; + } + } + "use_lto" | "use-lto" => { + if let Some(b) = values.last().and_then(toml_value_as_boolish) { + self.build.flags.use_lto = b; + } + } + "no_strip" | "no-strip" => { + if let Some(b) = values.last().and_then(|v| v.as_bool()) { + self.build.flags.no_strip = b; + } + } + "no_delete_static" | "no-delete-static" => { + if let Some(b) = values.last().and_then(|v| v.as_bool()) { + self.build.flags.no_delete_static = b; + } + } + "no_compress_man" + | "no-compress-man" + | "no_compress_manpages" + | "no-compress-manpages" => { + if let Some(b) = values.last().and_then(|v| v.as_bool()) { + self.build.flags.no_compress_man = b; + } + } + "skip_tests" | "skip-tests" => { + if let Some(b) = values.last().and_then(toml_value_as_boolish) { + self.build.flags.skip_tests = b; + } + } + "build_32" | "build-32" => { + if let Some(b) = values.last().and_then(toml_value_as_boolish) { + self.build.flags.build_32 = b; + } + } + "lib32_only" | "lib32-only" => { + if let Some(b) = values.last().and_then(toml_value_as_boolish) { + self.build.flags.lib32_only = b; + } + } + "split_docs" | "split-docs" => { + if let Some(b) = values.last().and_then(toml_value_as_boolish) { + self.build.flags.split_docs = b; + } + } + _ => {} + } + } +} + +pub(super) fn preprocess_spec_toml_appends( + input: &str, +) -> Result<(String, std::collections::HashMap>)> { + let mut base_text = String::new(); + let mut appends = std::collections::HashMap::new(); + let mut current_table: Option = None; + let mut in_array_table = false; + + for line in input.lines() { + let trimmed = line.trim(); + + if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 { + current_table = Some(normalize_append_key(trimmed[2..trimmed.len() - 2].trim())); + in_array_table = true; + base_text.push_str(line); + base_text.push('\n'); + continue; + } + if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 { + current_table = Some(normalize_append_key(trimmed[1..trimmed.len() - 1].trim())); + in_array_table = false; + base_text.push_str(line); + base_text.push('\n'); + continue; + } + + if trimmed.is_empty() || trimmed.starts_with('#') { + base_text.push_str(line); + base_text.push('\n'); + continue; + } + + if let Some(plus_idx) = trimmed.find("+=") { + if in_array_table { + anyhow::bail!( + "'+=' is not supported inside array-of-table sections ({})", + current_table.as_deref().unwrap_or("") + ); + } + let key = normalize_append_key(trimmed[..plus_idx].trim()); + let val_str = trimmed[plus_idx + 2..].trim(); + let val: toml::Value = toml::from_str::(&format!("v = {}", val_str)) + .context("Failed to parse append value")? + .get("v") + .cloned() + .unwrap(); + + let full_key = if key.contains('.') { + key + } else if let Some(table) = current_table.as_deref() { + format!("{}.{}", table, key) + } else { + key + }; + + appends.entry(full_key).or_insert_with(Vec::new).push(val); + // Preserve line numbering for parser diagnostics. + base_text.push('\n'); + continue; + } + + base_text.push_str(line); + base_text.push('\n'); + } + + Ok((base_text, appends)) +} + +pub(super) fn normalize_append_key(raw: &str) -> String { + raw.split('.') + .map(str::trim) + .filter(|part| !part.is_empty()) + .map(|part| { + let stripped = if (part.starts_with('"') && part.ends_with('"')) + || (part.starts_with('\'') && part.ends_with('\'')) + { + &part[1..part.len() - 1] + } else { + part + }; + stripped.trim().to_ascii_lowercase() + }) + .collect::>() + .join(".") +} + +fn configure_arch_append_key(key: &str) -> Option<&str> { + key.strip_prefix("configure_") + .map(str::trim) + .filter(|arch| !arch.is_empty() && !matches!(*arch, "file" | "lib32")) +} + +fn append_string_values(target: &mut Vec, values: &[toml::Value]) { + for value in values { + if let Some(arr) = value.as_array() { + target.extend( + arr.iter() + .filter_map(|entry| entry.as_str()) + .map(String::from), + ); + } else if let Some(s) = value.as_str() { + target.push(s.to_string()); + } + } +} diff --git a/src/package/spec/loading.rs b/src/package/spec/loading.rs new file mode 100644 index 0000000..e71fcc3 --- /dev/null +++ b/src/package/spec/loading.rs @@ -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, + #[serde(default)] + pub alternatives: Alternatives, + /// Manual (local) sources to copy before fetching remote sources. + #[serde(default)] + pub manual_sources: Vec, + #[serde(default, deserialize_with = "deserialize_sources")] + pub source: Vec, + 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, + /// 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, + + /// 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 { + // 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>, + ) -> 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 { + 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 { + 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 { + 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: ---.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 + ) + } +} diff --git a/src/package/spec/mod.rs b/src/package/spec/mod.rs new file mode 100644 index 0000000..bc08313 --- /dev/null +++ b/src/package/spec/mod.rs @@ -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; diff --git a/src/package/spec/model.rs b/src/package/spec/model.rs new file mode 100644 index 0000000..88c33a1 --- /dev/null +++ b/src/package/spec/model.rs @@ -0,0 +1,1061 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::BTreeMap; + +/// Package metadata +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct PackageInfo { + pub name: String, + /// Stable package stream name used to associate renamed ABI-split packages. + #[serde(default, alias = "real-name", skip_serializing_if = "Option::is_none")] + pub real_name: Option, + pub version: String, + /// Maintenance revision of the package (defaults to 1) + #[serde(default = "default_revision")] + pub revision: u32, + pub description: String, + pub homepage: String, + /// When true, renamed updates retain versioned shared libraries from the old package. + #[serde(default, alias = "abi-breaking")] + pub abi_breaking: bool, + #[serde( + deserialize_with = "deserialize_licenses", + serialize_with = "serialize_licenses" + )] + pub license: Vec, +} + +impl PackageInfo { + /// Return the stable package stream name, defaulting to the package name. + pub fn effective_real_name(&self) -> &str { + self.real_name.as_deref().unwrap_or(&self.name) + } +} + +fn default_revision() -> u32 { + 1 +} + +fn deserialize_licenses<'de, D>(deserializer: D) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrArray { + String(String), + Array(Vec), + } + + match StringOrArray::deserialize(deserializer)? { + StringOrArray::String(s) => Ok(vec![s]), + StringOrArray::Array(v) => Ok(v), + } +} + +fn serialize_licenses(licenses: &[String], serializer: S) -> std::result::Result +where + S: Serializer, +{ + if licenses.len() == 1 { + serializer.serialize_str(&licenses[0]) + } else { + licenses.serialize(serializer) + } +} + +/// Nested alternatives override group used for output-specific variants such as `lib32-*`. +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct AlternativeGroup { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub provides: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub conflicts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub replaces: Vec, +} + +/// Package alternatives such as virtual provides, install conflicts, and replacements. +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct Alternatives { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub provides: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub conflicts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub replaces: Vec, + /// Optional alternatives override used only for the generated `lib32-*` companion package. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lib32: Option, +} + +impl Alternatives { + /// Return the optional lib32-specific alternatives override set. + pub fn lib32_alternatives(&self) -> Option { + self.lib32.as_ref().map(|group| Alternatives { + provides: group.provides.clone(), + conflicts: group.conflicts.clone(), + replaces: group.replaces.clone(), + lib32: None, + }) + } +} + +/// Source tarball information +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct Source { + pub url: String, + /// Checksum for the source (e.g. `sha256:...`, `sha512:...`, `sha1:...`, `md5:...`, `b2:...`, `b2sum:...`, or raw SHA256 hex). + /// Defaults to `skip` when omitted. + #[serde(default = "default_source_sha256")] + pub sha256: String, + /// Directory name after extraction (supports $name, $version) + pub extract_dir: String, + + /// Patch files or URLs to apply after extraction. + /// + /// Example: + /// patches = ["fix-build.patch", ""] + #[serde(default)] + pub patches: Vec, + + /// Commands to run after extraction (and after patches), executed in the source directory. + /// + /// Example: + /// post_extract = ["autoreconf -fi"] + #[serde(default)] + pub post_extract: Vec, + + /// Optional list of git commit hashes/revs to cherry-pick after checkout. + /// + /// This is only valid for git sources (`*.git` URL or `url#rev` git form). + /// Example: + /// cherry_pick = ["a1b2c3d4", "deadbeef"] + #[serde( + default, + alias = "cherry-pick", + deserialize_with = "deserialize_string_or_array" + )] + pub cherry_pick: Vec, +} + +/// Manual source copied before standard source fetching. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ManualSource { + /// Filename in the spec directory (local manual source mode). + #[serde(default)] + pub file: Option, + /// Multiple filenames in the spec directory (local manual source mode). + #[serde(default)] + pub files: Vec, + /// Remote URL to fetch or clone (remote manual source mode). + #[serde(default)] + pub url: Option, + /// Multiple remote URLs to fetch or clone (remote manual source mode). + #[serde(default)] + pub urls: Vec, + /// Checksum (optional, use "skip" to bypass verification). + #[serde(default)] + pub sha256: Option, + /// Destination path relative to build work directory. + /// Defaults to `file` for local mode, a derived filename for archive URLs, + /// or the repository directory name for git URLs. + #[serde(default)] + pub dest: Option, +} + +fn default_source_sha256() -> String { + "skip".to_string() +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum OneOrManySources { + One(Source), + Many(Vec), +} + +pub(super) fn deserialize_sources<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + // Try to deserialize; if the field is missing/null, return empty vec + let parsed = Option::::deserialize(deserializer)?; + match parsed { + Some(OneOrManySources::One(s)) => Ok(vec![s]), + Some(OneOrManySources::Many(v)) => Ok(v), + None => Ok(Vec::new()), + } +} + +/// Build configuration +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct Build { + #[serde(rename = "type")] + pub build_type: BuildType, + #[serde(default)] + pub flags: BuildFlags, +} + +/// Supported build systems +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub enum BuildType { + Autotools, + CMake, + Meson, + Perl, + Custom, + Python, + Rust, + Makefile, + Bin, + Meta, +} + +/// Build flags and toolchain configuration +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct BuildFlags { + /// Extra flags exported to `CFLAGS`. + #[serde(default, deserialize_with = "deserialize_string_or_array")] + pub cflags: Vec, + /// Ordered replacement rules applied to `cflags` before export. + /// + /// Each entry may use `old=>new`. Plain `old=new` is also accepted and + /// disambiguated against the current flag set when possible. + #[serde( + default, + alias = "replace-cflags", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_cflags: Vec, + /// Extra flags exported to `CFLAGS` only for the lib32 build variant. + #[serde( + default, + alias = "cflags-lib32", + alias = "cflags_lib32", + alias = "CFLAGS-lib32", + alias = "CFLAGS_lib32", + deserialize_with = "deserialize_string_or_array" + )] + pub cflags_lib32: Vec, + /// Ordered replacement rules applied to lib32-only `cflags`. + #[serde( + default, + alias = "replace-cflags-lib32", + alias = "replace_cflags-lib32", + alias = "replace_cflags_lib32", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_cflags_lib32: Vec, + /// Extra flags exported to `CXXFLAGS`. + #[serde(default, deserialize_with = "deserialize_string_or_array")] + pub cxxflags: Vec, + /// Ordered replacement rules applied to `cxxflags` before export. + #[serde( + default, + alias = "replace-cxxflags", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_cxxflags: Vec, + /// Extra flags exported to `CXXFLAGS` only for the lib32 build variant. + #[serde( + default, + alias = "cxxflags-lib32", + alias = "cxxflags_lib32", + alias = "CXXFLAGS-lib32", + alias = "CXXFLAGS_lib32", + deserialize_with = "deserialize_string_or_array" + )] + pub cxxflags_lib32: Vec, + /// Ordered replacement rules applied to lib32-only `cxxflags`. + #[serde( + default, + alias = "replace-cxxflags-lib32", + alias = "replace_cxxflags-lib32", + alias = "replace_cxxflags_lib32", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_cxxflags_lib32: Vec, + /// Extra flags exported to `LDFLAGS`. + #[serde(default, deserialize_with = "deserialize_string_or_array")] + pub ldflags: Vec, + /// Linker selected through compiler drivers with `-fuse-ld=`. + #[serde(default, alias = "fuse-ld")] + pub fuse_ld: String, + /// Ordered replacement rules applied to `ldflags` before export. + #[serde( + default, + alias = "replace-ldflags", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_ldflags: Vec, + /// Link-time optimization flags exported to `LTOFLAGS`. + /// + /// When `use_lto` is true (default), these flags are also appended to + /// `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`. + #[serde( + default, + alias = "lto-flags", + alias = "lto_flags", + alias = "LTOFLAGS", + deserialize_with = "deserialize_string_or_array" + )] + pub ltoflags: Vec, + /// Rust LTO flags exported to `RUSTLTOFLAGS`. + /// + /// When `use_lto` is true (default), these flags are also appended to + /// `RUSTFLAGS`. + #[serde( + default, + alias = "rust-ltoflags", + alias = "rust_ltoflags", + alias = "RUSTLTOFLAGS", + deserialize_with = "deserialize_string_or_array" + )] + pub rustltoflags: Vec, + /// Ordered replacement rules applied to `ltoflags` before export/injection. + #[serde( + default, + alias = "replace-ltoflags", + alias = "replace_lto-flags", + alias = "replace_lto_flags", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_ltoflags: Vec, + /// Keep existing files and install package-provided replacement as `.depotnew`. + #[serde(default, deserialize_with = "deserialize_string_or_array")] + pub keep: Vec, + /// Split documentation trees into a derived `-docs` output during staging. + #[serde( + default, + alias = "split-docs", + deserialize_with = "deserialize_boolish" + )] + pub split_docs: bool, + /// Additional documentation directories to move into `-docs`. + #[serde( + default, + alias = "doc-dirs", + alias = "doc_dirs", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub doc_dirs: Vec, + /// Disable automatic LTOFLAGS injection into CFLAGS/CXXFLAGS/LDFLAGS. + #[serde( + default = "default_use_lto", + alias = "use-lto", + deserialize_with = "deserialize_boolish" + )] + pub use_lto: bool, + /// Disable exporting CFLAGS/CXXFLAGS/LDFLAGS for this package build. + #[serde(default, alias = "no-flags")] + pub no_flags: bool, + /// Disable automatic stripping of ELF files during staging. + #[serde(default, alias = "no-strip")] + pub no_strip: bool, + /// Disable automatic deletion of static libraries (`*.a`) during staging. + #[serde( + default, + alias = "no-delete-static", + alias = "no_remove_static", + alias = "no-remove-static" + )] + pub no_delete_static: bool, + /// Disable automatic zstd compression of man pages during staging. + #[serde( + default, + alias = "no-compress-man", + alias = "no_compress_manpages", + alias = "no-compress-manpages" + )] + pub no_compress_man: bool, + /// Skip automatic build-system test execution (e.g. Autotools `make check`/`make test`). + /// + /// Automatic tests are also skipped for multilib (`build_32` / `lib32_only`) builds. + #[serde(default, alias = "skip-tests")] + pub skip_tests: bool, + /// Run an additional lib32 build pass and emit a `lib32-*` package. + #[serde( + default, + alias = "build-32", + alias = "build_32", + deserialize_with = "deserialize_boolish" + )] + pub build_32: bool, + /// Build/install only the generated `lib32-*` companion package output. + #[serde( + default, + alias = "lib32-only", + alias = "lib32_only", + deserialize_with = "deserialize_boolish" + )] + pub lib32_only: bool, + /// Perform an additional native host-side helper build when the active target arch differs. + #[serde( + default, + alias = "host-build", + alias = "host_build", + deserialize_with = "deserialize_boolish" + )] + pub host_build: bool, + #[serde(default)] + pub configure: Vec, + /// Configure arguments appended only when the effective target architecture matches. + /// + /// Package specs populate this with append keys such as `configure_x86_64 += ["--enable-sse2"]`. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub configure_arch: BTreeMap>, + /// PEP 517 config settings for Python builds (each entry is `KEY=VALUE` or `KEY`). + #[serde( + default, + alias = "config-setting", + alias = "config-settings", + alias = "config_setting", + alias = "config_settings", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub config_settings: Vec, + /// Configure arguments used only for the lib32 build variant (replaces `configure` when set). + #[serde(default, alias = "configure-lib32", alias = "configure_lib32")] + pub configure_lib32: Vec, + /// Autotools configure script path, relative to source root or absolute. + #[serde(default, alias = "configure-file")] + pub configure_file: String, + /// Directory containing the configured compiler, linker, and binutils tools. + #[serde(default, alias = "tool-dir", alias = "tools_dir", alias = "tools-dir")] + pub tool_dir: String, + /// C compiler + #[serde(default = "default_cc")] + pub cc: String, + /// C++ compiler + #[serde(default = "default_cxx")] + pub cxx: String, + /// Archiver + #[serde(default = "default_ar")] + pub ar: String, + /// Archive indexer exported as `RANLIB` when configured. + #[serde(default)] + pub ranlib: String, + /// Strip executable exported as `STRIP` when configured. + #[serde(default)] + pub strip: String, + /// Linker executable or linker flavor override for supported builders. + #[serde(default)] + pub ld: String, + /// Symbol table dumper exported as `NM` when configured. + #[serde(default)] + pub nm: String, + /// Object copy tool exported as `OBJCOPY` when configured. + #[serde(default)] + pub objcopy: String, + /// Object dump tool exported as `OBJDUMP` when configured. + #[serde(default)] + pub objdump: String, + /// ELF reader exported as `READELF` when configured. + #[serde(default)] + pub readelf: String, + /// C preprocessor executable exported as `CPP` when configured. + #[serde(default, alias = "CPP")] + pub cpp: String, + /// Dynamic loader path + #[serde(default)] + pub libc: String, + /// Root filesystem for installation (per-package override) + #[serde(default = "default_rootfs")] + #[allow(dead_code)] + pub rootfs: String, + /// Commands to run after configure/setup step, before compile/build step. + #[serde(default, alias = "post-configure")] + pub post_configure: Vec, + /// Commands to run after configure/setup for the lib32 build variant. + #[serde( + default, + alias = "post-configure-lib32", + alias = "post_configure-lib32", + alias = "post_configure_lib32" + )] + pub post_configure_lib32: Vec, + /// Commands to run after compile (after make, before make install). + #[serde(default, alias = "post-compile")] + pub post_compile: Vec, + /// Commands to run after compile for the lib32 build variant. + #[serde( + default, + alias = "post-compile-lib32", + alias = "post_compile-lib32", + alias = "post_compile_lib32" + )] + pub post_compile_lib32: Vec, + /// Commands to run after install (after make install) + #[serde(default, alias = "post-install")] + pub post_install: Vec, + /// Commands to run after the lib32 install step (replaces `post_install` when set). + #[serde( + default, + alias = "post-install-lib32", + alias = "post_install-lib32", + alias = "post_install_lib32" + )] + pub post_install_lib32: Vec, + + /// Specific commands for 'makefile' build type + #[serde(default)] + pub makefile_commands: Vec, + #[serde(default)] + pub makefile_install_commands: Vec, + + /// Installation prefix (default: /usr) + #[serde(default = "default_prefix")] + pub prefix: String, + + /// Target architecture triple (CHOST equivalent) + #[serde(default)] + pub chost: String, + + /// Build architecture triple (CBUILD equivalent) + #[serde(default)] + pub cbuild: String, + + /// CPU architecture short name (CARCH equivalent), e.g. "x86_64", "aarch64" + #[serde(default = "default_carch")] + pub carch: String, + /// MAKEFLAGS environment variable passed to build commands. + #[serde( + default, + alias = "make-flags", + alias = "make_flags", + alias = "MAKEFLAGS", + deserialize_with = "deserialize_string_or_array_joined" + )] + pub makeflags: String, + /// Variable overrides passed directly to `make` (compile step), e.g. ["V=1", "CC=clang"]. + #[serde( + default, + alias = "make-vars", + alias = "make_build_vars", + alias = "make-build-vars", + deserialize_with = "deserialize_string_or_array" + )] + pub make_vars: Vec, + /// Make-like executable for build/test/install phases (default: `make`), e.g. `ninja`. + #[serde(default, alias = "make-exec")] + pub make_exec: String, + /// Target for the compile/build phase (e.g. `all`, `bootstrap`). + #[serde( + default, + alias = "make-target", + alias = "make_build_target", + alias = "make-build-target" + )] + pub make_target: String, + /// Targets for the compile/build phase (e.g. `["all", "bootstrap"]`). + #[serde( + default, + alias = "make-targets", + alias = "make_build_targets", + alias = "make-build-targets", + deserialize_with = "deserialize_string_or_array" + )] + pub make_targets: Vec, + /// Subdirectories (relative to build directory) where `make` should run. + #[serde( + default, + alias = "make-dirs", + deserialize_with = "deserialize_string_or_array" + )] + pub make_dirs: Vec, + /// Variable overrides passed directly to `make check` / `make test`. + #[serde( + default, + alias = "make-test-vars", + deserialize_with = "deserialize_string_or_array" + )] + pub make_test_vars: Vec, + /// Target for the test phase, passed to the make-like executable. + #[serde(default, alias = "make-test-target")] + pub make_test_target: String, + /// Targets for the test phase, passed to the make-like executable. + #[serde( + default, + alias = "make-test-targets", + deserialize_with = "deserialize_string_or_array" + )] + pub make_test_targets: Vec, + /// Subdirectories (relative to build directory) where test targets should run. + #[serde( + default, + alias = "make-test-dirs", + deserialize_with = "deserialize_string_or_array" + )] + pub make_test_dirs: Vec, + /// Variable overrides passed directly to `make install`. + #[serde( + default, + alias = "make-install-vars", + deserialize_with = "deserialize_string_or_array" + )] + pub make_install_vars: Vec, + /// Target for the install phase (default: `install`). + #[serde(default, alias = "make-install-target")] + pub make_install_target: String, + /// Targets for the install phase. + #[serde( + default, + alias = "make-install-targets", + deserialize_with = "deserialize_string_or_array" + )] + pub make_install_targets: Vec, + /// Subdirectories (relative to build directory) where `make install` should run. + #[serde( + default, + alias = "make-install-dirs", + deserialize_with = "deserialize_string_or_array" + )] + pub make_install_dirs: Vec, + /// Additional host environment variable names to export unchanged to build commands. + /// Example: ["RUSTFLAGS", "CARGO_HOME"]. + #[serde( + default, + alias = "passthrough-env", + alias = "pass_env", + alias = "pass-env", + alias = "export_env", + alias = "export-env", + deserialize_with = "deserialize_string_or_array" + )] + pub passthrough_env: Vec, + /// Explicit environment variable assignments exported to build commands. + /// Each entry must be `KEY=VALUE`. + #[serde( + default, + alias = "env-vars", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub env_vars: Vec, + + // Rust-specific fields + /// Rust build profile: "debug" or "release" (default: release) + #[serde(default = "default_profile")] + pub profile: String, + /// Rust target triple (e.g., x86_64-unknown-linux-musl). Optional. + #[serde(default)] + pub target: String, + /// RUSTFLAGS environment variable + #[serde(default, deserialize_with = "deserialize_string_or_array")] + pub rustflags: Vec, + /// Ordered replacement rules applied to `rustflags` before export. + #[serde( + default, + alias = "replace-rustflags", + deserialize_with = "deserialize_string_or_array_no_split" + )] + pub replace_rustflags: Vec, + /// Additional cargo arguments (short name) + #[serde(default)] + pub cargs: Vec, + /// Binary installation directory relative to DESTDIR (default: /usr/bin) + #[serde(default = "default_bindir")] + pub bindir: String, + /// System binary installation directory for supported builders (default: /usr/bin). + #[serde(default)] + pub sbindir: String, + /// Library installation directory for supported builders. + /// + /// Defaults to `/usr/lib`, or `/usr/lib32` for the lib32 build variant. + #[serde(default)] + pub libdir: String, + /// Library helper executable installation directory for supported builders. + /// + /// Defaults to the effective `libdir`. + #[serde(default)] + pub libexecdir: String, + /// System configuration directory for supported builders (default: /etc). + #[serde(default)] + pub sysconfdir: String, + /// Variable state directory for supported builders (default: /var). + #[serde(default)] + pub localstatedir: String, + /// Shared variable state directory for supported builders (default: /var/lib). + #[serde(default)] + pub sharedstatedir: String, + /// Header installation directory for supported builders (default: /usr/include). + #[serde(default)] + pub includedir: String, + /// Data root installation directory for supported builders (default: /usr/share). + #[serde(default)] + pub datarootdir: String, + /// Architecture-independent data installation directory for supported builders. + /// + /// Defaults to the effective `datarootdir`. + #[serde(default)] + pub datadir: String, + /// Manual page installation directory for supported builders (default: /usr/share/man). + #[serde(default)] + pub mandir: String, + /// Info page installation directory for supported builders (default: /usr/share/info). + #[serde(default)] + pub infodir: String, + + /// Subdirectory within extracted source to use as the actual source root. + /// Useful for monorepos like llvm-project where you want to build just one component. + #[serde(default)] + pub source_subdir: String, + /// Build directory relative to source root (e.g. "build") + #[serde(default, skip_serializing_if = "Option::is_none")] + pub build_dir: Option, + /// Binary package type when using BuildType::Bin (e.g. "deb") + #[serde(default)] + pub binary_type: String, + /// Internal runtime marker used to adjust builder behavior for the lib32 variant. + #[serde(skip)] + pub lib32_variant: bool, + /// Internal runtime marker containing the absolute path to the native host helper build dir. + #[serde(skip)] + pub host_build_dir: Option, +} + +impl Default for BuildFlags { + fn default() -> Self { + BuildFlags { + cflags: Vec::new(), + replace_cflags: Vec::new(), + cflags_lib32: Vec::new(), + replace_cflags_lib32: Vec::new(), + cxxflags: Vec::new(), + replace_cxxflags: Vec::new(), + cxxflags_lib32: Vec::new(), + replace_cxxflags_lib32: Vec::new(), + ldflags: Vec::new(), + fuse_ld: String::new(), + replace_ldflags: Vec::new(), + ltoflags: Vec::new(), + rustltoflags: Vec::new(), + replace_ltoflags: Vec::new(), + keep: Vec::new(), + split_docs: false, + doc_dirs: Vec::new(), + use_lto: default_use_lto(), + no_flags: false, + no_strip: false, + no_delete_static: false, + no_compress_man: false, + skip_tests: false, + build_32: false, + lib32_only: false, + host_build: false, + configure: Vec::new(), + configure_arch: BTreeMap::new(), + config_settings: Vec::new(), + configure_lib32: Vec::new(), + configure_file: String::new(), + tool_dir: String::new(), + cc: default_cc(), + cxx: default_cxx(), + ar: default_ar(), + ranlib: String::new(), + strip: String::new(), + ld: String::new(), + nm: String::new(), + objcopy: String::new(), + objdump: String::new(), + readelf: String::new(), + cpp: String::new(), + libc: String::new(), + rootfs: default_rootfs(), + post_configure: Vec::new(), + post_configure_lib32: Vec::new(), + post_compile: Vec::new(), + post_compile_lib32: Vec::new(), + post_install: Vec::new(), + post_install_lib32: Vec::new(), + makefile_commands: Vec::new(), + makefile_install_commands: Vec::new(), + prefix: default_prefix(), + chost: String::new(), + cbuild: String::new(), + carch: default_carch(), + makeflags: String::new(), + make_vars: Vec::new(), + make_exec: String::new(), + make_target: String::new(), + make_targets: Vec::new(), + make_dirs: Vec::new(), + make_test_vars: Vec::new(), + make_test_target: String::new(), + make_test_targets: Vec::new(), + make_test_dirs: Vec::new(), + make_install_vars: Vec::new(), + make_install_target: String::new(), + make_install_targets: Vec::new(), + make_install_dirs: Vec::new(), + passthrough_env: Vec::new(), + env_vars: Vec::new(), + profile: default_profile(), + target: String::new(), + rustflags: Vec::new(), + replace_rustflags: Vec::new(), + cargs: Vec::new(), + bindir: default_bindir(), + sbindir: String::new(), + libdir: String::new(), + libexecdir: String::new(), + sysconfdir: String::new(), + localstatedir: String::new(), + sharedstatedir: String::new(), + includedir: String::new(), + datarootdir: String::new(), + datadir: String::new(), + mandir: String::new(), + infodir: String::new(), + source_subdir: String::new(), + build_dir: None, + binary_type: String::new(), + lib32_variant: false, + host_build_dir: None, + } + } +} + +fn deserialize_string_or_array<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrArray { + String(String), + Array(Vec), + } + + match Option::::deserialize(deserializer)? { + Some(StringOrArray::String(s)) => Ok(s.split_whitespace().map(String::from).collect()), + Some(StringOrArray::Array(a)) => Ok(a), + None => Ok(Vec::new()), + } +} + +fn deserialize_string_or_array_no_split<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrArray { + String(String), + Array(Vec), + } + + match Option::::deserialize(deserializer)? { + Some(StringOrArray::String(s)) => Ok(vec![s]), + Some(StringOrArray::Array(a)) => Ok(a), + None => Ok(Vec::new()), + } +} + +fn deserialize_string_or_array_joined<'de, D>( + deserializer: D, +) -> std::result::Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrArray { + String(String), + Array(Vec), + } + + match Option::::deserialize(deserializer)? { + Some(StringOrArray::String(s)) => Ok(s), + Some(StringOrArray::Array(a)) => Ok(a + .iter() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect::>() + .join(" ")), + None => Ok(String::new()), + } +} + +fn deserialize_boolish<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum Boolish { + Bool(bool), + String(String), + } + + match Option::::deserialize(deserializer)? { + Some(Boolish::Bool(v)) => Ok(v), + Some(Boolish::String(s)) => match s.trim().to_ascii_lowercase().as_str() { + "true" | "1" | "yes" | "on" => Ok(true), + "false" | "0" | "no" | "off" => Ok(false), + other => Err(serde::de::Error::custom(format!( + "expected boolean string for lib32 flag, got '{}'", + other + ))), + }, + None => Ok(false), + } +} + +pub(super) fn toml_value_as_boolish(value: &toml::Value) -> Option { + if let Some(b) = value.as_bool() { + return Some(b); + } + value + .as_str() + .and_then(|s| match s.trim().to_ascii_lowercase().as_str() { + "true" | "1" | "yes" | "on" => Some(true), + "false" | "0" | "no" | "off" => Some(false), + _ => None, + }) +} + +pub(super) fn append_whitespace_separated(dst: &mut String, value: &str) { + let trimmed = value.trim(); + if trimmed.is_empty() { + return; + } + if dst.is_empty() { + dst.push_str(trimmed); + } else { + dst.push(' '); + dst.push_str(trimmed); + } +} + +fn default_cc() -> String { + // Prefer clang if available (supports -print-resource-dir and other useful flags) + if std::process::Command::new("which") + .arg("clang") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { + return "clang".to_string(); + } + "gcc".to_string() +} + +fn default_use_lto() -> bool { + true +} + +fn default_ar() -> String { + "ar".to_string() +} + +fn default_rootfs() -> String { + "/".to_string() +} + +fn default_profile() -> String { + "release".to_string() +} + +fn default_bindir() -> String { + "/usr/bin".to_string() +} + +fn default_prefix() -> String { + "/usr".to_string() +} + +fn default_carch() -> String { + std::env::consts::ARCH.to_string() +} + +fn default_cxx() -> String { + // Infer a sensible C++ compiler name from default_cc() + let cc = default_cc(); + if cc.contains("clang") { + "clang++".to_string() + } else { + "g++".to_string() + } +} + +/// Nested dependency override group for a specific output variant. +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct DependencyGroup { + /// Dependencies required for building packages. + #[serde(default)] + pub build: Vec, + /// Dependencies required at runtime. + #[serde(default)] + pub runtime: Vec, + /// Dependencies required to run package test suites. + #[serde(default)] + pub test: Vec, + /// Optional runtime integrations that enhance functionality when installed. + #[serde(default)] + pub optional: Vec, + /// Package groups associated with this package output. + #[serde(default)] + pub groups: Vec, +} + +impl DependencyGroup { + fn to_dependencies(&self) -> Dependencies { + Dependencies { + build: self.build.clone(), + runtime: self.runtime.clone(), + test: self.test.clone(), + optional: self.optional.clone(), + groups: self.groups.clone(), + lib32: None, + } + } +} + +/// Package dependencies +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct Dependencies { + /// Dependencies required for building packages. + #[serde(default)] + pub build: Vec, + /// Dependencies required at runtime. + #[serde(default)] + pub runtime: Vec, + /// Dependencies required to run package test suites. + #[serde(default)] + pub test: Vec, + /// Optional runtime integrations that enhance functionality when installed. + #[serde(default)] + pub optional: Vec, + /// Package groups associated with this package. + #[serde(default)] + pub groups: Vec, + /// Optional dependency overrides used only for the generated `lib32-*` companion package. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lib32: Option, +} + +impl Dependencies { + /// Return the top-level dependency set without any nested output-specific overrides. + pub fn primary_dependencies(&self) -> Dependencies { + Dependencies { + build: self.build.clone(), + runtime: self.runtime.clone(), + test: self.test.clone(), + optional: self.optional.clone(), + groups: self.groups.clone(), + lib32: None, + } + } + + /// Return the optional lib32-specific dependency override set. + pub fn lib32_dependencies(&self) -> Option { + self.lib32.as_ref().map(DependencyGroup::to_dependencies) + } +} diff --git a/src/package/spec/tests.rs b/src/package/spec/tests.rs new file mode 100644 index 0000000..bc30189 --- /dev/null +++ b/src/package/spec/tests.rs @@ -0,0 +1,2383 @@ +use super::*; +use std::path::{Path, PathBuf}; + +#[test] +fn parse_single_source_table() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo-$version.tar.gz" +sha256 = "skip" +extract_dir = "foo-$version" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.package.name, "foo"); + assert_eq!(spec.sources().len(), 1); + assert_eq!( + spec.expand_vars(&spec.sources()[0].url), + "https://example.com/foo-1.0.tar.gz" + ); + assert!(spec.sources()[0].patches.is_empty()); + assert!(spec.sources()[0].post_extract.is_empty()); + assert!(spec.sources()[0].cherry_pick.is_empty()); + assert_eq!(spec.spec_dir, tmp.path()); +} + +#[test] +fn parse_source_array() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[[source]] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[[source]] +url = "https://example.com/bar.tar.gz" +sha256 = "skip" +extract_dir = "bar" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.sources().len(), 2); + assert_eq!(spec.sources()[0].extract_dir, "foo"); + assert_eq!(spec.sources()[1].extract_dir, "bar"); +} + +#[test] +fn parse_source_without_sha256_defaults_to_skip() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +extract_dir = "foo" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.sources()[0].sha256, "skip"); +} + +#[test] +fn parse_git_source_with_cherry_pick() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.git#main" +sha256 = "skip" +extract_dir = "foo" +cherry_pick = ["deadbeef", "cafebabe"] + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.sources()[0].cherry_pick, + vec!["deadbeef".to_string(), "cafebabe".to_string()] + ); +} + +#[test] +fn parse_package_dependencies_overrides() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "llvm" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/llvm.tar.gz" +sha256 = "skip" +extract_dir = "llvm" + +[build] +type = "custom" + +[dependencies] +runtime = ["base"] +groups = ["toolchain"] + +[package_dependencies.clang] +runtime = ["llvm-libs", "llvm-libgcc"] +groups = ["compiler"] + +[package_dependencies.llvm-libs] +runtime = ["llvm-libgcc", "zstd"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.dependencies_for_output("llvm").runtime, + vec!["base".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("llvm").groups, + vec!["toolchain".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("clang").runtime, + vec!["llvm-libs".to_string(), "llvm-libgcc".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("clang").groups, + vec!["compiler".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("llvm-libs").runtime, + vec!["llvm-libgcc".to_string(), "zstd".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("llvm-libs").groups, + Vec::::new() + ); +} + +#[test] +fn parse_lib32_dependencies_override() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "llvm" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/llvm.tar.gz" +sha256 = "skip" +extract_dir = "llvm" + +[build] +type = "custom" + +[dependencies] +runtime = ["base"] +groups = ["toolchain"] + +[dependencies.lib32] +build = ["gcc-multilib"] +runtime = ["lib32-zlib"] +test = ["bats"] +groups = ["lib32-toolchain"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.dependencies_for_output("llvm").runtime, + vec!["base".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("llvm").groups, + vec!["toolchain".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("lib32-llvm").build, + vec!["gcc-multilib".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("lib32-llvm").runtime, + vec!["lib32-zlib".to_string(), "llvm".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("lib32-llvm").test, + vec!["bats".to_string()] + ); + assert_eq!( + spec.dependencies_for_output("lib32-llvm").groups, + vec!["lib32-toolchain".to_string()] + ); +} + +#[test] +fn parse_package_alternatives_overrides() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "llvm" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/llvm.tar.gz" +sha256 = "skip" +extract_dir = "llvm" + +[build] +type = "custom" + +[alternatives] +provides = ["toolchain"] +conflicts = ["gcc"] + +[package_alternatives.clang] +provides = ["cc", "c++", "gcc"] +conflicts = ["clang-legacy"] + +[package_alternatives.llvm] +provides = ["binutils"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.alternatives_for_output("llvm").provides, + vec!["binutils".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("llvm").conflicts, + Vec::::new() + ); + assert_eq!( + spec.alternatives_for_output("clang").provides, + vec!["cc".to_string(), "c++".to_string(), "gcc".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("clang").conflicts, + vec!["clang-legacy".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("other").provides, + vec!["toolchain".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("other").conflicts, + vec!["gcc".to_string()] + ); +} + +#[test] +fn parse_lib32_alternatives_override() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "llvm" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/llvm.tar.gz" +sha256 = "skip" +extract_dir = "llvm" + +[build] +type = "custom" + +[alternatives] +provides = ["toolchain"] +replaces = ["clang"] + +[alternatives.lib32] +provides = ["lib32-toolchain"] +conflicts = ["lib32-gcc"] +replaces = ["lib32-clang"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.alternatives_for_output("llvm").replaces, + vec!["clang".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("lib32-llvm").provides, + vec!["lib32-toolchain".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("lib32-llvm").conflicts, + vec!["lib32-gcc".to_string()] + ); + assert_eq!( + spec.alternatives_for_output("lib32-llvm").replaces, + vec!["lib32-clang".to_string()] + ); +} + +#[test] +fn lib32_output_does_not_fallback_to_primary_alternatives() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "llvm" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +build_32 = true + +[alternatives] +provides = ["toolchain"] +conflicts = ["gcc"] +replaces = ["clang"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + let lib32 = spec.alternatives_for_output("lib32-llvm"); + + assert!(lib32.provides.is_empty()); + assert!(lib32.conflicts.is_empty()); + assert!(lib32.replaces.is_empty()); +} + +#[test] +fn parse_python_build_type() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "python" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(matches!(spec.build.build_type, BuildType::Python)); +} + +#[test] +fn parse_perl_build_type() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "perl" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(matches!(spec.build.build_type, BuildType::Perl)); +} + +#[test] +fn parse_python_config_settings_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "python" + +[build.flags] +config-setting = ["editable_mode=compat", "setup-args=--plat-name=x86_64"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.config_settings, + vec![ + "editable_mode=compat".to_string(), + "setup-args=--plat-name=x86_64".to_string() + ] + ); +} + +#[test] +fn parse_multiple_licenses() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = ["MIT", "Apache-2.0"] + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.package.license, + vec!["MIT".to_string(), "Apache-2.0".to_string()] + ); +} + +#[test] +fn parse_rejects_empty_sources() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + // `source = []` is not accepted (must have at least one entry) + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +source = [] + +[build] +type = "custom" +"#, + ) + .unwrap(); + + assert!(PackageSpec::from_file(&path).is_err()); +} + +#[test] +fn parse_allows_metapackage_without_sources() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo-meta" +version = "1.0" +description = "metapackage" +homepage = "https://example.com" +license = "MIT" + +[build] +type = "meta" + +[dependencies] +runtime = ["foo", "bar"] +groups = ["base"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.source.is_empty()); + assert!(spec.manual_sources.is_empty()); + assert!(spec.is_metapackage()); + assert_eq!(spec.dependencies.runtime, vec!["foo", "bar"]); + assert_eq!(spec.dependencies.groups, vec!["base"]); +} + +#[test] +fn parse_manual_source_with_url() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[[manual_sources]] +url = "https://example.com/manual.patch" +sha256 = "skip" +dest = "patches/manual.patch" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.manual_sources.len(), 1); + assert_eq!( + spec.manual_sources[0].url.as_deref(), + Some("https://example.com/manual.patch") + ); + assert_eq!(spec.manual_sources[0].file, None); +} + +#[test] +fn parse_manual_source_rejects_missing_file_and_url() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[[manual_sources]] +sha256 = "skip" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let err = PackageSpec::from_file(&path).expect_err("spec should be rejected"); + assert!( + err.to_string() + .contains("must specify one of 'file', 'files', 'url', or 'urls'") + ); +} + +#[test] +fn parse_manual_source_rejects_file_and_url_together() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[[manual_sources]] +file = "manual.patch" +url = "https://example.com/manual.patch" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let err = PackageSpec::from_file(&path).expect_err("spec should be rejected"); + assert!( + err.to_string() + .contains("cannot mix local ('file'/'files') and remote ('url'/'urls') entries") + ); +} + +#[test] +fn parse_manual_source_with_files_array() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[[manual_sources]] +files = ["other", "system-auth"] + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.manual_sources.len(), 1); + assert_eq!(spec.manual_sources[0].files, vec!["other", "system-auth"]); + assert!(spec.manual_sources[0].urls.is_empty()); +} + +#[test] +fn test_apply_config() { + let mut spec = mk_spec("foo", "1.0"); + let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); + + // Mock some overrides and appends + config.build_overrides = toml::from_str( + r#" +[flags] +cc = "my-cc" +cxx = "my-cxx" +ar = "my-ar" +ranlib = "my-ranlib" +strip = "my-strip" +ld = "ld.lld" +nm = "my-nm" +objcopy = "my-objcopy" +objdump = "my-objdump" +readelf = "my-readelf" +CPP = "clang-cpp" +tool_dir = "/opt/toolchain/bin" +cflags = ["-O2"] +replace_cflags = ["-O2=>-O3"] +cxxflags = ["-O2", "-pipe"] +replace_cxxflags = ["-pipe=>-fPIC"] +passthrough_env = ["RUSTFLAGS"] +env_vars = ["SETUPTOOLS_SCM_PRETEND_VERSION=$version"] +bindir = "/opt/bin" +sbindir = "/opt/sbin" +libdir = "/opt/lib64" +sysconfdir = "/opt/etc" +datarootdir = "/opt/share-root" +makeflags = "-j8" +make_vars = ["V=1"] +make_dirs = ["lib"] +make_test_dirs = ["tests"] +make_install_dirs = ["lib"] +ltoflags = ["-flto=auto"] +RUSTLTOFLAGS = ["-Clinker-plugin-lto"] +replace_ltoflags = ["auto=>thin"] +rustflags = ["-C", "debuginfo=2"] +replace_rustflags = ["debuginfo=2=>opt-level=2"] +use_lto = true +no_flags = true +no_strip = true +no_delete_static = true +no_compress_man = true +skip_tests = true +keep = ["etc/locale.gen"] +configure_file = "configure.gnu" +config-setting = ["editable_mode=compat"] +post_configure = ["echo configured"] +"#, + ) + .unwrap(); + config.appends.insert( + "build.flags.cflags".to_string(), + vec![toml::Value::String("-g".to_string())], + ); + config.appends.insert( + "build.flags.replace_cflags".to_string(), + vec![toml::Value::String( + "-D_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".to_string(), + )], + ); + config.appends.insert( + "build.flags.cxxflags".to_string(), + vec![toml::Value::String("-stdlib=libc++".to_string())], + ); + config.appends.insert( + "build.flags.replace_cxxflags".to_string(), + vec![toml::Value::String( + "-stdlib=libc++=>-stdlib=libstdc++".to_string(), + )], + ); + config.appends.insert( + "build.flags.rustflags".to_string(), + vec![toml::Value::Array(vec![ + toml::Value::String("-C".to_string()), + toml::Value::String("opt-level=3".to_string()), + ])], + ); + config.appends.insert( + "build.flags.replace_rustflags".to_string(), + vec![toml::Value::String("opt-level=3=>opt-level=z".to_string())], + ); + config.appends.insert( + "build.flags.keep".to_string(), + vec![toml::Value::Array(vec![toml::Value::String( + "etc/locale.gen".to_string(), + )])], + ); + config.appends.insert( + "build.flags.ltoflags".to_string(), + vec![toml::Value::Array(vec![toml::Value::String( + "-fno-fat-lto-objects".to_string(), + )])], + ); + config.appends.insert( + "build.flags.replace_ltoflags".to_string(), + vec![toml::Value::String( + "-fno-fat-lto-objects=>-flto-jobs=8".to_string(), + )], + ); + config.appends.insert( + "build.flags.use_lto".to_string(), + vec![toml::Value::Boolean(false)], + ); + config.appends.insert( + "build.flags.no_strip".to_string(), + vec![toml::Value::Boolean(false)], + ); + config.appends.insert( + "build.flags.no_compress_man".to_string(), + vec![toml::Value::Boolean(false)], + ); + config.appends.insert( + "build.flags.no_delete_static".to_string(), + vec![toml::Value::Boolean(false)], + ); + config.appends.insert( + "build.flags.passthrough_env".to_string(), + vec![toml::Value::String("CARGO_HOME".to_string())], + ); + config.appends.insert( + "build.flags.env_vars".to_string(), + vec![toml::Value::String( + "SOURCE_DATE_EPOCH=1700000000".to_string(), + )], + ); + config.appends.insert( + "build.flags.make_test_vars".to_string(), + vec![toml::Value::String("TESTS=smoke".to_string())], + ); + config.appends.insert( + "build.flags.makeflags".to_string(), + vec![toml::Value::String("--output-sync=target".to_string())], + ); + config.appends.insert( + "build.flags.make_dirs".to_string(), + vec![toml::Value::String("libelf".to_string())], + ); + config.appends.insert( + "build.flags.make_test_dirs".to_string(), + vec![toml::Value::String("fuzz".to_string())], + ); + config.appends.insert( + "build.flags.make_install_dirs".to_string(), + vec![toml::Value::String("tools".to_string())], + ); + config.appends.insert( + "build.flags.make_install_vars".to_string(), + vec![toml::Value::String("DESTDIR=/tmp/pkg".to_string())], + ); + config.appends.insert( + "build.flags.configure_file".to_string(), + vec![toml::Value::String("build-aux/configure".to_string())], + ); + config.appends.insert( + "build.flags.libexecdir".to_string(), + vec![toml::Value::String("/opt/libexec".to_string())], + ); + config.appends.insert( + "build.flags.datadir".to_string(), + vec![toml::Value::String("/opt/share-data".to_string())], + ); + config.appends.insert( + "build.flags.config-setting".to_string(), + vec![toml::Value::String( + "setup-args=--plat-name=x86_64".to_string(), + )], + ); + config.appends.insert( + "build.flags.post_configure".to_string(), + vec![toml::Value::String("touch configured.stamp".to_string())], + ); + config.appends.insert( + "build.flags.configure_x86_64".to_string(), + vec![toml::Value::String("--enable-x86-tuning".to_string())], + ); + + spec.apply_config(&config); + + assert_eq!(spec.build.flags.cc, "my-cc"); + assert_eq!(spec.build.flags.cxx, "my-cxx"); + assert_eq!(spec.build.flags.ar, "my-ar"); + assert_eq!(spec.build.flags.ranlib, "my-ranlib"); + assert_eq!(spec.build.flags.strip, "my-strip"); + assert_eq!(spec.build.flags.ld, "ld.lld"); + assert_eq!(spec.build.flags.nm, "my-nm"); + assert_eq!(spec.build.flags.objcopy, "my-objcopy"); + assert_eq!(spec.build.flags.objdump, "my-objdump"); + assert_eq!(spec.build.flags.readelf, "my-readelf"); + assert_eq!(spec.build.flags.cpp, "clang-cpp"); + assert_eq!(spec.build.flags.tool_dir, "/opt/toolchain/bin"); + assert!(spec.build.flags.cflags.contains(&"-O2".to_string())); + assert!(spec.build.flags.cflags.contains(&"-g".to_string())); + assert!( + spec.build + .flags + .replace_cflags + .contains(&"-O2=>-O3".to_string()) + ); + assert!( + spec.build + .flags + .replace_cflags + .contains(&"-D_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".to_string()) + ); + assert!(spec.build.flags.cxxflags.contains(&"-O2".to_string())); + assert!(spec.build.flags.cxxflags.contains(&"-pipe".to_string())); + assert!( + spec.build + .flags + .cxxflags + .contains(&"-stdlib=libc++".to_string()) + ); + assert!( + spec.build + .flags + .replace_cxxflags + .contains(&"-pipe=>-fPIC".to_string()) + ); + assert!( + spec.build + .flags + .replace_cxxflags + .contains(&"-stdlib=libc++=>-stdlib=libstdc++".to_string()) + ); + assert!(spec.build.flags.rustflags.contains(&"-C".to_string())); + assert!( + spec.build + .flags + .rustflags + .contains(&"debuginfo=2".to_string()) + ); + assert!( + spec.build + .flags + .rustflags + .contains(&"opt-level=3".to_string()) + ); + assert!( + spec.build + .flags + .replace_rustflags + .contains(&"debuginfo=2=>opt-level=2".to_string()) + ); + assert!( + spec.build + .flags + .replace_rustflags + .contains(&"opt-level=3=>opt-level=z".to_string()) + ); + assert!( + spec.build + .flags + .ltoflags + .contains(&"-flto=auto".to_string()) + ); + assert!( + spec.build + .flags + .rustltoflags + .contains(&"-Clinker-plugin-lto".to_string()) + ); + assert!( + spec.build + .flags + .ltoflags + .contains(&"-fno-fat-lto-objects".to_string()) + ); + assert!( + spec.build + .flags + .replace_ltoflags + .contains(&"auto=>thin".to_string()) + ); + assert!( + spec.build + .flags + .replace_ltoflags + .contains(&"-fno-fat-lto-objects=>-flto-jobs=8".to_string()) + ); + assert!(!spec.build.flags.use_lto); + assert!(spec.build.flags.no_flags); + assert!(!spec.build.flags.no_strip); + assert!(!spec.build.flags.no_delete_static); + assert!(!spec.build.flags.no_compress_man); + assert!( + spec.build + .flags + .keep + .contains(&"etc/locale.gen".to_string()) + ); + assert!( + spec.build + .flags + .passthrough_env + .contains(&"RUSTFLAGS".to_string()) + ); + assert!( + spec.build + .flags + .passthrough_env + .contains(&"CARGO_HOME".to_string()) + ); + assert!( + spec.build + .flags + .env_vars + .contains(&"SETUPTOOLS_SCM_PRETEND_VERSION=$version".to_string()) + ); + assert!( + spec.build + .flags + .env_vars + .contains(&"SOURCE_DATE_EPOCH=1700000000".to_string()) + ); + assert_eq!(spec.build.flags.bindir, "/opt/bin"); + assert_eq!(spec.build.flags.sbindir, "/opt/sbin"); + assert_eq!(spec.build.flags.libdir, "/opt/lib64"); + assert_eq!(spec.build.flags.libexecdir, "/opt/libexec"); + assert_eq!(spec.build.flags.sysconfdir, "/opt/etc"); + assert_eq!(spec.build.flags.datarootdir, "/opt/share-root"); + assert_eq!(spec.build.flags.datadir, "/opt/share-data"); + assert_eq!( + spec.build.flags.configure_arch.get("x86_64"), + Some(&vec!["--enable-x86-tuning".to_string()]) + ); + assert_eq!(spec.build.flags.makeflags, "-j8 --output-sync=target"); + assert!(spec.build.flags.make_vars.contains(&"V=1".to_string())); + assert!(spec.build.flags.make_dirs.contains(&"lib".to_string())); + assert!(spec.build.flags.make_dirs.contains(&"libelf".to_string())); + assert!(spec.build.flags.skip_tests); + assert!( + spec.build + .flags + .make_test_vars + .contains(&"TESTS=smoke".to_string()) + ); + assert!( + spec.build + .flags + .make_test_dirs + .contains(&"tests".to_string()) + ); + assert!( + spec.build + .flags + .make_test_dirs + .contains(&"fuzz".to_string()) + ); + assert!( + spec.build + .flags + .make_install_vars + .contains(&"DESTDIR=/tmp/pkg".to_string()) + ); + assert!( + spec.build + .flags + .make_install_dirs + .contains(&"lib".to_string()) + ); + assert!( + spec.build + .flags + .make_install_dirs + .contains(&"tools".to_string()) + ); + assert_eq!(spec.build.flags.configure_file, "build-aux/configure"); + assert!( + spec.build + .flags + .config_settings + .contains(&"editable_mode=compat".to_string()) + ); + assert!( + spec.build + .flags + .config_settings + .contains(&"setup-args=--plat-name=x86_64".to_string()) + ); + assert!( + spec.build + .flags + .post_configure + .contains(&"echo configured".to_string()) + ); + assert!( + spec.build + .flags + .post_configure + .contains(&"touch configured.stamp".to_string()) + ); +} + +#[test] +fn test_apply_config_preserves_package_scalar_tool_and_layout_overrides() { + let mut spec = mk_spec("foo", "1.0"); + spec.build.flags.ld = "ld.lld".to_string(); + spec.build.flags.libdir = "/package/lib".to_string(); + spec.build.flags.sysconfdir = "/package/etc".to_string(); + let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); + config.build_overrides = toml::from_str( + r#" +ld = "/config/bin/ld" +fuse_ld = "/config/bin/ld.lld" +ranlib = "/config/bin/ranlib" +libdir = "/config/lib" +sysconfdir = "/config/etc" +"#, + ) + .unwrap(); + + spec.apply_config(&config); + + assert_eq!(spec.build.flags.ld, "ld.lld"); + assert_eq!(spec.build.flags.libdir, "/package/lib"); + assert_eq!(spec.build.flags.sysconfdir, "/package/etc"); + assert_eq!(spec.build.flags.ranlib, "/config/bin/ranlib"); + assert_eq!(spec.build.flags.fuse_ld, "/config/bin/ld.lld"); +} + +#[test] +fn test_apply_config_preserves_package_lto_disable() { + let mut spec = mk_spec("glibc", "2.43"); + spec.build.flags.use_lto = false; + let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); + config.build_overrides = toml::from_str( + r#" +[flags] +ltoflags = ["-flto=thin"] +use_lto = true +"#, + ) + .unwrap(); + + spec.apply_config(&config); + + assert!(!spec.build.flags.use_lto); + assert_eq!(spec.build.flags.ltoflags, vec!["-flto=thin".to_string()]); +} + +#[test] +fn parse_no_flags_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +no_flags = true +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.no_flags); +} + +#[test] +fn parse_tool_commands_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +cc = "/tools/bin/cc" +cxx = "/tools/bin/c++" +ar = "/tools/bin/ar" +ranlib = "/tools/bin/ranlib" +strip = "/tools/bin/strip" +ld = "/tools/bin/ld" +fuse_ld = "/usr/bin/ld.lld" +nm = "/tools/bin/nm" +objcopy = "/tools/bin/objcopy" +objdump = "/tools/bin/objdump" +readelf = "/tools/bin/readelf" +cpp = "/tools/bin/cpp" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.cc, "/tools/bin/cc"); + assert_eq!(spec.build.flags.cxx, "/tools/bin/c++"); + assert_eq!(spec.build.flags.ar, "/tools/bin/ar"); + assert_eq!(spec.build.flags.ranlib, "/tools/bin/ranlib"); + assert_eq!(spec.build.flags.strip, "/tools/bin/strip"); + assert_eq!(spec.build.flags.ld, "/tools/bin/ld"); + assert_eq!(spec.build.flags.fuse_ld, "/usr/bin/ld.lld"); + assert_eq!(spec.build.flags.nm, "/tools/bin/nm"); + assert_eq!(spec.build.flags.objcopy, "/tools/bin/objcopy"); + assert_eq!(spec.build.flags.objdump, "/tools/bin/objdump"); + assert_eq!(spec.build.flags.readelf, "/tools/bin/readelf"); + assert_eq!(spec.build.flags.cpp, "/tools/bin/cpp"); +} + +#[test] +fn parse_ltoflags_and_use_lto_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +ltoflags = ["-flto=auto", "-fuse-linker-plugin"] +use_lto = false +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.ltoflags, + vec!["-flto=auto".to_string(), "-fuse-linker-plugin".to_string()] + ); + assert!(!spec.build.flags.use_lto); +} + +#[test] +fn parse_ltoflags_and_use_lto_aliases_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +LTOFLAGS = "-flto=auto" +"use-lto" = false +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.ltoflags, vec!["-flto=auto".to_string()]); + assert!(!spec.build.flags.use_lto); +} + +#[test] +fn parse_no_strip_no_delete_static_and_no_compress_man_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +no_strip = true +"no-delete-static" = true +no-compress-man = true +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.no_strip); + assert!(spec.build.flags.no_delete_static); + assert!(spec.build.flags.no_compress_man); +} + +#[test] +fn parse_no_flags_alias_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +"no-flags" = true +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.no_flags); +} + +#[test] +fn parse_skip_tests_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[build.flags] +skip_tests = true +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.skip_tests); +} + +#[test] +fn parse_skip_tests_alias_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[build.flags] +"skip-tests" = true +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.skip_tests); +} + +#[test] +fn reject_unknown_nested_keys_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[dependencies] +runtime = ["glibc"] +skip_tests = true +"#, + ) + .unwrap(); + + let err = PackageSpec::from_file(&path).expect_err("expected unknown nested key to fail"); + assert!( + err.to_string() + .contains("unknown key: dependencies.skip_tests") + ); +} + +#[test] +fn parse_configure_file_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[build.flags] +configure_file = "build-aux/configure" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.configure_file, "build-aux/configure"); +} + +#[test] +fn parse_install_dirs_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "cmake" + +[build.flags] +bindir = "/custom/bin" +sbindir = "/custom/sbin" +libdir = "/custom/lib64" +libexecdir = "/custom/libexec" +sysconfdir = "/custom/etc" +localstatedir = "/custom/var" +sharedstatedir = "/custom/var/lib" +includedir = "/custom/include" +datarootdir = "/custom/share-root" +datadir = "/custom/share" +mandir = "/custom/share/man" +infodir = "/custom/share/info" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.bindir, "/custom/bin"); + assert_eq!(spec.build.flags.sbindir, "/custom/sbin"); + assert_eq!(spec.build.flags.libdir, "/custom/lib64"); + assert_eq!(spec.build.flags.libexecdir, "/custom/libexec"); + assert_eq!(spec.build.flags.sysconfdir, "/custom/etc"); + assert_eq!(spec.build.flags.localstatedir, "/custom/var"); + assert_eq!(spec.build.flags.sharedstatedir, "/custom/var/lib"); + assert_eq!(spec.build.flags.includedir, "/custom/include"); + assert_eq!(spec.build.flags.datarootdir, "/custom/share-root"); + assert_eq!(spec.build.flags.datadir, "/custom/share"); + assert_eq!(spec.build.flags.mandir, "/custom/share/man"); + assert_eq!(spec.build.flags.infodir, "/custom/share/info"); +} + +#[test] +fn parse_lib32_build_flags_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[build.flags] +"build-32" = "true" +"lib32-only" = "yes" +"CFLAGS-lib32" = ["-mstackrealign"] +"CXXFLAGS-lib32" = ["-fno-rtti"] +"configure-lib32" = ["--disable-static"] +"post_configure-lib32" = ["echo configured lib32"] +"post_compile-lib32" = ["echo compiled lib32"] +"post_install-lib32" = ["echo lib32"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.build_32); + assert!(spec.build.flags.lib32_only); + assert!(spec.builds_lib32_output()); + assert!(spec.builds_only_lib32_output()); + assert_eq!(spec.build.flags.cflags_lib32, vec!["-mstackrealign"]); + assert_eq!(spec.build.flags.cxxflags_lib32, vec!["-fno-rtti"]); + assert_eq!(spec.build.flags.configure_lib32, vec!["--disable-static"]); + assert_eq!( + spec.build.flags.post_configure_lib32, + vec!["echo configured lib32"] + ); + assert_eq!( + spec.build.flags.post_compile_lib32, + vec!["echo compiled lib32"] + ); + assert_eq!(spec.build.flags.post_install_lib32, vec!["echo lib32"]); +} + +#[test] +fn multilib_builds_skip_automatic_tests() { + let mut spec = mk_spec("foo", "1.0"); + assert!(!spec.should_skip_automatic_tests()); + + spec.build.flags.build_32 = true; + assert!(spec.should_skip_automatic_tests()); + + spec.build.flags.build_32 = false; + spec.build.flags.skip_tests = true; + assert!(spec.should_skip_automatic_tests()); +} + +#[test] +fn parse_post_configure_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "cmake" + +[build.flags] +post_configure = ["cmake -L . > cmake-options.txt"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.post_configure, + vec!["cmake -L . > cmake-options.txt".to_string()] + ); +} + +#[test] +fn parse_keep_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +keep = ["etc/locale.gen", "etc/resolv.conf"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.keep, + vec!["etc/locale.gen".to_string(), "etc/resolv.conf".to_string()] + ); +} + +#[test] +fn parse_split_docs_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +split_docs = true +doc_dirs = ["/opt/docs", "usr/share/devhelp"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert!(spec.build.flags.split_docs); + assert_eq!( + spec.build.flags.doc_dirs, + vec!["/opt/docs".to_string(), "usr/share/devhelp".to_string()] + ); +} + +#[test] +fn parse_build_flags_appends_from_spec_file() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +replace_cflags = ["-O2=>-O3"] +replace_rustflags = ["debuginfo=2=>opt-level=2"] +cxxflags = ["-O2"] +cxxflags += [ "-Wno-gnu-statement-expression-from-macro-expansion" ] +ldflags += "-Wl,--as-needed" +replace_cflags += [ "_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2" ] +replace_rustflags += "opt-level=3=>opt-level=z" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.cxxflags, + vec![ + "-O2".to_string(), + "-Wno-gnu-statement-expression-from-macro-expansion".to_string() + ] + ); + assert_eq!( + spec.build.flags.ldflags, + vec!["-Wl,--as-needed".to_string()] + ); + assert_eq!( + spec.build.flags.replace_cflags, + vec![ + "-O2=>-O3".to_string(), + "_FORTIFY_SOURCE=3=_FORTIFY_SOURCE=2".to_string() + ] + ); + assert_eq!( + spec.build.flags.replace_rustflags, + vec![ + "debuginfo=2=>opt-level=2".to_string(), + "opt-level=3=>opt-level=z".to_string() + ] + ); +} + +#[test] +fn parse_configure_arch_appends_from_spec_file() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +configure = ["--base"] +configure_x86_64 += ["--enable-sse2"] +configure_aarch64 += "--enable-neon" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.configure, vec!["--base".to_string()]); + assert_eq!( + spec.build.flags.configure_arch.get("x86_64"), + Some(&vec!["--enable-sse2".to_string()]) + ); + assert_eq!( + spec.build.flags.configure_arch.get("aarch64"), + Some(&vec!["--enable-neon".to_string()]) + ); +} + +#[test] +fn parse_build_flags_appends_accepts_quoted_and_uppercase_keys() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +"cflags" += ["-fPIC"] +CXXFLAGS += ["-stdlib=libc++"] +"LDFLAGS" += "-Wl,--as-needed" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.cflags, vec!["-fPIC".to_string()]); + assert_eq!( + spec.build.flags.cxxflags, + vec!["-stdlib=libc++".to_string()] + ); + assert_eq!( + spec.build.flags.ldflags, + vec!["-Wl,--as-needed".to_string()] + ); +} + +#[test] +fn apply_config_reads_build_flag_appends_from_rootfs_build_toml() { + let tmp = tempfile::tempdir().unwrap(); + let config_path = tmp.path().join("etc/depot.d/build.toml"); + std::fs::create_dir_all(config_path.parent().unwrap()).unwrap(); + std::fs::write( + &config_path, + r#" +[flags] +cflags += ["-g"] +CXXFLAGS += ["-stdlib=libc++"] +LDFLAGS += "-Wl,--as-needed" +"#, + ) + .unwrap(); + + let config = crate::config::Config::for_rootfs(tmp.path()); + assert_eq!( + config.appends.get("build.flags.cflags").unwrap()[0] + .as_array() + .unwrap()[0] + .as_str(), + Some("-g") + ); + assert_eq!( + config.appends.get("build.flags.cxxflags").unwrap()[0] + .as_array() + .unwrap()[0] + .as_str(), + Some("-stdlib=libc++") + ); + assert_eq!( + config.appends.get("build.flags.ldflags").unwrap()[0].as_str(), + Some("-Wl,--as-needed") + ); + let mut spec = mk_spec("foo", "1.0"); + spec.apply_config(&config); + + assert!(spec.build.flags.cflags.contains(&"-g".to_string())); + assert!( + spec.build + .flags + .cxxflags + .contains(&"-stdlib=libc++".to_string()) + ); + assert!( + spec.build + .flags + .ldflags + .contains(&"-Wl,--as-needed".to_string()) + ); +} + +#[test] +fn parse_passthrough_env_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "custom" + +[build.flags] +passthrough_env = ["RUSTFLAGS", "CARGO_HOME"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.passthrough_env, + vec!["RUSTFLAGS".to_string(), "CARGO_HOME".to_string()] + ); +} + +#[test] +fn parse_env_vars_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "python" + +[build.flags] +env_vars = ["SETUPTOOLS_SCM_PRETEND_VERSION=$version", "PYO3_CONFIG_FILE=$specdir/pyo3.toml"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.env_vars, + vec![ + "SETUPTOOLS_SCM_PRETEND_VERSION=$version".to_string(), + "PYO3_CONFIG_FILE=$specdir/pyo3.toml".to_string() + ] + ); +} + +#[test] +fn parse_test_dependencies_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + + [dependencies] + build = ["make"] + test = ["python", "bats"] + optional = ["gtk-doc"] + "#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.dependencies.test, + vec!["python".to_string(), "bats".to_string()] + ); + assert_eq!(spec.dependencies.optional, vec!["gtk-doc".to_string()]); +} + +#[test] +fn parse_make_var_overrides_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[build.flags] +make_vars = ["V=1", "CC=clang"] +make_exec = "ninja" +make_target = "bootstrap" +make_targets = ["stage1", "stage2"] +make_dirs = ["lib", "libelf"] +make_test_vars = ["TESTS=unit"] +make_test_target = "test" +make_test_targets = ["test-unit", "test-integration"] +make_test_dirs = ["tests"] +make_install_vars = ["STRIPPROG=true"] +make_install_target = "install/strip" +make_install_targets = ["install-runtime", "install-devel"] +make_install_dirs = ["lib", "apps"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!( + spec.build.flags.make_vars, + vec!["V=1".to_string(), "CC=clang".to_string()] + ); + assert_eq!(spec.build.flags.make_exec, "ninja"); + assert_eq!(spec.build.flags.make_target, "bootstrap"); + assert_eq!( + spec.build.flags.make_targets, + vec!["stage1".to_string(), "stage2".to_string()] + ); + assert_eq!( + spec.build.flags.make_dirs, + vec!["lib".to_string(), "libelf".to_string()] + ); + assert_eq!( + spec.build.flags.make_test_vars, + vec!["TESTS=unit".to_string()] + ); + assert_eq!(spec.build.flags.make_test_target, "test".to_string()); + assert_eq!( + spec.build.flags.make_test_targets, + vec!["test-unit".to_string(), "test-integration".to_string()] + ); + assert_eq!(spec.build.flags.make_test_dirs, vec!["tests".to_string()]); + assert_eq!( + spec.build.flags.make_install_vars, + vec!["STRIPPROG=true".to_string()] + ); + assert_eq!( + spec.build.flags.make_install_target, + "install/strip".to_string() + ); + assert_eq!( + spec.build.flags.make_install_targets, + vec!["install-runtime".to_string(), "install-devel".to_string()] + ); + assert_eq!( + spec.build.flags.make_install_dirs, + vec!["lib".to_string(), "apps".to_string()] + ); +} + +#[test] +fn parse_makeflags_from_spec() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[source] +url = "https://example.com/foo.tar.gz" +sha256 = "skip" +extract_dir = "foo" + +[build] +type = "autotools" + +[build.flags] +MAKEFLAGS = ["-j12", "--output-sync=target"] +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + assert_eq!(spec.build.flags.makeflags, "-j12 --output-sync=target"); +} + +#[test] +fn test_chost_cbuild_overrides() { + let mut spec = mk_spec("foo", "1.0"); + let config = crate::config::Config { + cache_dir: "/tmp".into(), + build_dir: "/tmp".into(), + db_dir: "/tmp".into(), + build_overrides: toml::from_str( + r#" +chost = "x86_64-sfg-linux-gnu" +cbuild = "x86_64-pc-linux-gnu" +"#, + ) + .unwrap(), + package_overrides: toml::Value::Table(toml::map::Map::new()), + appends: std::collections::HashMap::new(), + repo_settings: crate::config::RepoSettings::default(), + source_repos: std::collections::BTreeMap::new(), + binary_repos: std::collections::BTreeMap::new(), + mirrors: std::collections::HashMap::new(), + repo_clone_dir: PathBuf::from("/tmp"), + package_cache_dir: PathBuf::from("/tmp"), + install_test_deps: false, + }; + + spec.apply_config(&config); + assert_eq!(spec.build.flags.chost, "x86_64-sfg-linux-gnu"); + assert_eq!(spec.build.flags.cbuild, "x86_64-pc-linux-gnu"); +} + +#[test] +fn test_default_and_override_carch() { + let mut spec = mk_spec("foo", "1.0"); + // Default should be host arch + assert_eq!(spec.build.flags.carch, std::env::consts::ARCH.to_string()); + + // Override via config + let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent")); + config.build_overrides = toml::from_str( + r#"[flags] +carch = "armv7" +"#, + ) + .unwrap(); + spec.apply_config(&config); + assert_eq!(spec.build.flags.carch, "armv7"); +} + +#[test] +fn test_package_filename() { + let mut spec = mk_spec("foo", "1.0"); + spec.package.revision = 2; + assert_eq!( + spec.package_filename("x86_64"), + "foo-1.0-2-x86_64.depot.pkg.tar.zst" + ); +} + +#[test] +fn parse_packages_array() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("pkg.toml"); + + std::fs::write( + &path, + r#" +[package] +name = "foo" +version = "1.0" +description = "d" +homepage = "h" +license = "MIT" + +[[packages]] +name = "foo-dev" +version = "1.0" +description = "development files" +homepage = "h" +license = "MIT" + +[[source]] +url = "https://example.com/foo-1.0.tar.gz" +sha256 = "skip" +extract_dir = "foo-1.0" + +[build] +type = "custom" +"#, + ) + .unwrap(); + + let spec = PackageSpec::from_file(&path).unwrap(); + let outputs = spec.outputs(); + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].name, "foo"); + assert_eq!(outputs[1].name, "foo-dev"); +} + +#[test] +fn docs_output_uses_runtime_dependency_on_parent_package() { + let mut spec = mk_spec("foo", "1.0"); + spec.build.flags.split_docs = true; + let docs_name = PackageSpec::docs_package_name("foo"); + + let deps = spec.dependencies_for_output(&docs_name); + assert_eq!(deps.runtime, vec!["foo".to_string()]); + + let alternatives = spec.alternatives_for_output(&docs_name); + assert!(alternatives.provides.is_empty()); + assert!(alternatives.conflicts.is_empty()); +} + +#[test] +fn docs_package_for_output_derives_name_and_description() { + let mut spec = mk_spec("foo", "1.0"); + spec.build.flags.split_docs = true; + + let docs = spec.docs_package_for_output(&spec.package); + assert_eq!(docs.name, "foo-docs"); + assert_eq!(docs.description, "Documentation for foo"); + assert_eq!(docs.version, "1.0"); +} + +fn mk_spec(name: &str, version: &str) -> PackageSpec { + PackageSpec { + package: PackageInfo { + name: name.into(), + real_name: None, + version: version.into(), + revision: 1, + description: "d".into(), + homepage: "h".into(), + abi_breaking: false, + license: vec!["MIT".into()], + }, + packages: Vec::new(), + alternatives: Alternatives::default(), + manual_sources: Vec::new(), + source: vec![Source { + url: "h".into(), + sha256: "s".into(), + extract_dir: "e".into(), + patches: Vec::new(), + post_extract: Vec::new(), + cherry_pick: Vec::new(), + }], + build: Build { + build_type: BuildType::Custom, + flags: BuildFlags::default(), + }, + dependencies: Dependencies::default(), + package_alternatives: Default::default(), + package_dependencies: Default::default(), + spec_dir: PathBuf::from("."), + } +} diff --git a/src/planner.rs b/src/planner.rs index 6e5a7ce..7b4c344 100644 --- a/src/planner.rs +++ b/src/planner.rs @@ -144,6 +144,7 @@ struct NodeData { #[derive(Debug, Clone)] struct LocalSpecHit { spec_name: String, + real_name: Option, path: PathBuf, provides: Vec, replaces: Vec, @@ -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(); diff --git a/src/source/extractor.rs b/src/source/extractor.rs index 917e80e..ec9b264 100755 --- a/src/source/extractor.rs +++ b/src/source/extractor.rs @@ -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( diff --git a/src/source/mod.rs b/src/source/mod.rs index 01bb493..1e2960a 100755 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -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()); diff --git a/src/staging/mod.rs b/src/staging/mod.rs index 7eff484..453e13a 100755 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -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 { + (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 { fn auto_strip_elf_files(destdir: &Path, strip_command: &str) -> Result { let mut stripped = 0usize; + let mut hardlink_keys_by_path: HashMap = HashMap::new(); + let mut stripped_hardlinks: HashMap = 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 { ); } 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 { - 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::, _>>() - .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 { - 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 { - 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 = HashMap::new(); let result: Result<()> = (|| { // First, create all directories from destdir (for packages with only directories) @@ -1817,9 +1686,29 @@ pub fn install_atomic( .with_context(|| format!("Failed to create symlink: {}", install_rel_path))?; tx.replay_relocated_dir_if_present(&install_rel_path)?; } else { - fs::copy(src_path, &dest_path) - .with_context(|| format!("Failed to install: {}", install_rel_path))?; - apply_unix_mode(&dest_path, &metadata)?; + 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 diff --git a/src/system_state.rs b/src/system_state.rs deleted file mode 100644 index 3655cea..0000000 --- a/src/system_state.rs +++ /dev/null @@ -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, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) target: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) arch: Option, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub(crate) layers: BTreeMap>, -} - -pub(crate) fn state_path(config: &Config) -> PathBuf { - config.db_dir.join(STATE_FILENAME) -} - -pub(crate) fn load(config: &Config) -> Result { - 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 { - 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 { - 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 { - 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::>(); - 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 { - 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 { - let layer = normalize_token("layer", &layer)?; - let remove = packages - .iter() - .map(|package| normalize_token("package", package)) - .collect::>>()?; - 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 { - 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 { - 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::(); - 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::, _>>() - .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 { - 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::() - .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()); - } -} diff --git a/src/ui.rs b/src/ui.rs index f0ae8e0..0c26dee 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -50,16 +50,6 @@ pub fn success(message: impl AsRef) { 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) { eprintln!( "{} {}",