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!( "{} {}",