From 9a544c60b0001e4fc6fdc83773c0169e2963728f Mon Sep 17 00:00:00 2001
From: SFG545
Date: Tue, 9 Jun 2026 06:14:15 -0500
Subject: [PATCH] Simplify Depot staging and preserve filesystem topology
Remove the old LBI bootstrap/system-state path and make Depot stage and
install packages against standard FHS paths instead of rewriting payloads into
the custom /system layout. This drops the bootstrap command surface,
system-state storage, /system path normalization, and bootstrap chroot helper
while moving rootfs state defaults back under /var and aliases under /usr/bin.
Preserve hardlink topology throughout the package lifecycle. Add shared fs_copy
helpers for symlink/hardlink-aware tree copies, use them for source preparation,
binary packages, Rust installs, and lib32 staging, and keep hardlinks intact
during stripping, atomic installation, and package archive creation.
Replace the external fakeroot wrapper with an internal user-namespace based
install command wrapper. Install phases now run as UID/GID 0 inside a private
namespace while mapping ownership back to the invoking user on the host.
Split the package spec parser into loading, model, config, and tests modules
without changing the public PackageSpec surface. Keep strict unknown-key
handling, manual source validation, config/appends support, lib32 overrides,
and generated output helpers in the new layout.
Tighten build and dependency behavior:
- emit explicit shared-library selectors when static builds are disabled
- apply configure_arch entries for the effective target/lib32 architecture
- add CMake sysroot defaults for non-live DEPOT_ROOTFS builds
- resolve Rust source_subdir before build hooks and binary installation
- satisfy dependencies and local sibling plans through package real_name aliases
- accept bare git:// source URLs as HEAD checkouts
Update tests around FHS path expectations, namespace fakeroot installs,
hardlink-preserving copies/archives/installs/stripping, real_name dependency
resolution, configure_arch expansion, CMake sysroot defaults, Rust source
subdirs, and git:// URL parsing.
Bump suppaftp to 8.0.4 and lzma-rust2 to 0.16.4.
Bump Depot to 0.50.0
---
Cargo.lock | 12 +-
Cargo.toml | 6 +-
meson.build | 2 +-
src/bootstrap.rs | 6811 ---------------------------------
src/builder/autotools.rs | 2 +-
src/builder/bin.rs | 26 +-
src/builder/cmake.rs | 89 +-
src/builder/custom.rs | 53 +-
src/builder/makefile.rs | 76 +-
src/builder/meson.rs | 14 +-
src/builder/mod.rs | 287 +-
src/builder/perl.rs | 10 -
src/builder/rust.rs | 123 +-
src/cli.rs | 105 -
src/commands.rs | 16 +-
src/commands/misc.rs | 111 -
src/commands/misc/internal.rs | 28 +-
src/commands/set.rs | 17 +-
src/config.rs | 40 +-
src/db/mod.rs | 103 +-
src/deps.rs | 29 +-
src/fakeroot.rs | 152 +-
src/fs_copy.rs | 165 +
src/interrupts.rs | 6 +-
src/main.rs | 3 +-
src/package/packager.rs | 61 +-
src/package/spec.rs | 5274 -------------------------
src/package/spec/config.rs | 1570 ++++++++
src/package/spec/loading.rs | 378 ++
src/package/spec/mod.rs | 11 +
src/package/spec/model.rs | 1061 +++++
src/package/spec/tests.rs | 2383 ++++++++++++
src/planner.rs | 58 +-
src/source/extractor.rs | 20 +-
src/source/mod.rs | 33 +-
src/staging/mod.rs | 567 +--
src/system_state.rs | 505 ---
src/ui.rs | 10 -
38 files changed, 6561 insertions(+), 13656 deletions(-)
delete mode 100644 src/bootstrap.rs
create mode 100644 src/fs_copy.rs
delete mode 100755 src/package/spec.rs
create mode 100644 src/package/spec/config.rs
create mode 100644 src/package/spec/loading.rs
create mode 100644 src/package/spec/mod.rs
create mode 100644 src/package/spec/model.rs
create mode 100644 src/package/spec/tests.rs
delete mode 100644 src/system_state.rs
diff --git a/Cargo.lock b/Cargo.lock
index a3d3857..82f9fb0 100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -542,7 +542,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]]
name = "depot"
-version = "0.36.0"
+version = "0.50.0"
dependencies = [
"anyhow",
"ar",
@@ -1516,11 +1516,11 @@ dependencies = [
[[package]]
name = "lzma-rust2"
-version = "0.16.2"
+version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
+checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02"
dependencies = [
- "sha2 0.10.9",
+ "sha2 0.11.0",
]
[[package]]
@@ -2410,9 +2410,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "suppaftp"
-version = "8.0.3"
+version = "8.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4275c142b5be3af2eeadd70dd368caf3b65546c8af1035839372dd7a1436127d"
+checksum = "4cf00e4d8418c477a8cb3c13ae5396a68d31658e760c74280bdbd34926e3b94b"
dependencies = [
"chrono",
"futures-lite",
diff --git a/Cargo.toml b/Cargo.toml
index 7258d8d..8c2526e 100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "depot"
-version = "0.36.0"
+version = "0.50.0"
edition = "2024"
[lints.rust]
@@ -43,7 +43,7 @@ zip = "8.5.0"
zstd = { version = "0.13.3", features = ["zstdmt"] }
inquire = "0.9.4"
md5 = "0.8.0"
-suppaftp = "8.0.2"
+suppaftp = "8.0.4"
minisign = "0.9.1"
petgraph = "0.8.3"
fd-lock = "4.0.4"
@@ -56,7 +56,7 @@ time = { version = "0.3.47", features = ["formatting", "parsing"] }
b2sum-rust = "0.3.0"
serde_ignored = "0.1.14"
lz4_flex = "0.13.1"
-lzma-rust2 = "0.16.2"
+lzma-rust2 = "0.16.4"
signal-hook = "0.4.4"
sha1 = "0.11.0"
pdf-extract = "0.10.0"
diff --git a/meson.build b/meson.build
index 0e0ecf9..de140fd 100644
--- a/meson.build
+++ b/meson.build
@@ -1,6 +1,6 @@
project(
'depot',
- version: '0.36.0',
+ version: '0.50.0',
meson_version: '>=0.60.0',
default_options: ['buildtype=release'],
)
diff --git a/src/bootstrap.rs b/src/bootstrap.rs
deleted file mode 100644
index 7746a01..0000000
--- a/src/bootstrap.rs
+++ /dev/null
@@ -1,6811 +0,0 @@
-use crate::cli::BootstrapArgs;
-use crate::{config, source, system_state, ui};
-use anyhow::{Context, Result};
-use std::collections::{BTreeMap, BTreeSet};
-use std::ffi::{OsStr, OsString};
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::{Command, Stdio};
-use std::time::{Duration, SystemTime, UNIX_EPOCH};
-use sys_mount::{Mount, MountFlags, Unmount, UnmountFlags};
-use url::Url;
-
-const TEMP_LAYER: &str = "temp";
-const BASE_LAYER: &str = "base";
-const DEVEL_LAYER: &str = "devel";
-const FILESYSTEM_PACKAGE: &str = "filesystem";
-const BOOTSTRAP_DIR: &str = "bootstrap/lbi";
-const DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS: &str = "DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS";
-const CHAPTER7_RETIRED_PACKAGES: &[&str] = &["llvm-clang-pass1"];
-const BOOTSTRAP_CHROOT_SHIM_DIR: &str = "/tmp/depot-bootstrap-tools";
-const BOOTSTRAP_CHROOT_PATH: &str = "/system/tools/bin:/system/binaries:/system/systembinaries:/tmp/depot-bootstrap-tools:/bin:/usr/bin:/sbin:/usr/sbin";
-const BOOK_FETCH_CACHE_BUST_PARAM: &str = "depot_refresh";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct BookPackage {
- chapter: u8,
- section: String,
- title: String,
- name: String,
- version: String,
- layer: String,
- page_url: String,
- recipe_id: String,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum BookOperationKind {
- ResetTargetTreeOwnership,
- CreateVirtualFilesystemLinkTargets,
- CopyBuildProfile,
- EnterChroot,
- CreateEssentialSystemFiles,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct BookOperation {
- section: String,
- title: String,
- kind: BookOperationKind,
- recipe_id: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum BookStep {
- Package(BookPackage),
- Operation(BookOperation),
-}
-
-#[derive(Debug, Clone)]
-struct GeneratedRecipe {
- package: BookPackage,
- spec_path: PathBuf,
- progress_path: PathBuf,
-}
-
-#[derive(Debug, Clone)]
-struct PageRecipe {
- input_files: Vec,
- source_urls: Vec,
- extract_dir: Option,
- commands: Vec,
- dependencies: Vec,
- license: String,
- description: String,
-}
-
-#[derive(Debug, Clone)]
-struct ManifestEntry {
- url: String,
- output_name: String,
-}
-
-#[derive(Debug, Clone, Default)]
-struct SourceManifest {
- entries: Vec,
- blake2b_512: BTreeMap,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum BootstrapBuildMode {
- Host,
- Cross,
- Chroot,
-}
-
-#[derive(Debug, Clone)]
-struct BootstrapInstallInvocation {
- args: Vec,
- env: Vec<(OsString, OsString)>,
-}
-
-pub(crate) fn run(args: BootstrapArgs) -> Result<()> {
- let config = config::Config::for_rootfs(&args.sysroot);
- let (target, arch) = bootstrap_target_arch(&args, &config)?;
- ensure_lbi_layout_for_fresh_bootstrap(&args.sysroot, &config, &target, &arch)?;
- let pdf = load_book_pdf(&args)?;
- let text =
- pdf_extract::extract_text_from_mem(&pdf).context("Failed to extract text from book PDF")?;
- let steps = parse_book_steps(&text, &args.book_url)?;
- let packages = packages_from_steps(&steps);
- if packages.is_empty() {
- anyhow::bail!("No package sections were found in the Linux by Intent book");
- }
-
- let recipes = generate_recipes(&args, &config, &packages, &target, &arch)?;
- let recipes_by_id = recipes
- .iter()
- .map(|recipe| (recipe.package.recipe_id.as_str(), recipe))
- .collect::>();
- let progress_root = bootstrap_progress_root(&config);
-
- for (idx, step) in steps.iter().enumerate() {
- if step_requires_root(step)
- && !step_is_done(step, &recipes_by_id, &progress_root, &args.sysroot, &config)
- && !crate::fakeroot::is_root()
- {
- ensure_root_for_bootstrap()?;
- return Ok(());
- }
-
- match step {
- BookStep::Package(package) => {
- let recipe = recipes_by_id
- .get(package.recipe_id.as_str())
- .copied()
- .with_context(|| format!("Missing generated recipe for {}", package.title))?;
- let mode = build_mode_for_package(package);
- ui::merge_package(&package.layer, &package.name);
- install_recipe(&args.sysroot, &config, recipe, mode, &target)?;
- system_state::set_stage(&config, stage_for_step(step))?;
- }
- BookStep::Operation(operation) => {
- run_operation_step(&args.sysroot, &config, operation, &target, &arch)?;
- system_state::set_stage(&config, stage_for_step(step))?;
- }
- }
-
- if step_completes_chapter(step, steps.get(idx + 1), 7) {
- retire_packages_after_chapter7(&args.sysroot, &config)?;
- system_state::set_stage(&config, "bootstrap-chapter7-cleanup".to_string())?;
- }
- }
-
- let layers = packages_by_layer(&packages);
- for layer in [TEMP_LAYER, BASE_LAYER, DEVEL_LAYER] {
- let packages = bootstrap_layer_packages_for_state(&layers, layer);
- system_state::set_layer_packages(&config, layer.to_string(), &packages)?;
- }
-
- system_state::set_stage(&config, "bootstrap-complete".to_string())?;
- ui::success(format!(
- "Bootstrapped {} Linux by Intent package(s) through chapter 9",
- packages.len()
- ));
- Ok(())
-}
-
-fn bootstrap_target_arch(
- args: &BootstrapArgs,
- config: &config::Config,
-) -> Result<(String, String)> {
- let state = system_state::load(config)?;
- let target = args
- .target
- .clone()
- .or(state.target)
- .unwrap_or_else(|| "x86_64-lbi-linux-musl".to_string());
- let arch = args
- .arch
- .clone()
- .or(state.arch)
- .unwrap_or_else(|| crate::cross::target_arch_from_triple(&target).to_string());
- Ok((target, arch))
-}
-
-fn ensure_lbi_layout_for_fresh_bootstrap(
- sysroot: &Path,
- config: &config::Config,
- target: &str,
- arch: &str,
-) -> Result<()> {
- let state = system_state::load(config)?;
- system_state::ensure_lbi_layout_paths(sysroot)?;
- if state.stage.is_some()
- || !state.layers.is_empty()
- || sysroot.join("etc/depot.d/build.toml").exists()
- {
- ui::info("Resuming existing bootstrap state");
- return Ok(());
- }
-
- ui::info("Initializing Linux by Intent sysroot layout");
- system_state::init_lbi_layout(sysroot, config, target, Some(arch), false)?;
- Ok(())
-}
-
-fn install_recipe(
- sysroot: &Path,
- config: &config::Config,
- recipe: &GeneratedRecipe,
- mode: BootstrapBuildMode,
- target: &str,
-) -> Result<()> {
- if bootstrap_package_is_complete(sysroot, config, recipe)? {
- ui::info(format!(
- "Skipping already completed package {} ({})",
- recipe.package.name, recipe.package.section
- ));
- return Ok(());
- }
-
- prepare_bootstrap_package_install(sysroot, config, &recipe.package)?;
- let exe = std::env::current_exe().context("Failed to locate depot executable")?;
- let invocation = bootstrap_install_invocation(sysroot, recipe, mode, target, &exe)?;
- let mut cmd = Command::new(&exe);
- cmd.args(&invocation.args);
- for (key, value) in &invocation.env {
- cmd.env(key, value);
- }
- let status = cmd
- .status()
- .with_context(|| format!("Failed to run depot install for {}", recipe.package.name))?;
- if !status.success() {
- anyhow::bail!(
- "depot install failed for {} with status {status}",
- recipe.package.name
- );
- }
- if let Some(parent) = recipe.progress_path.parent() {
- fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create {}", parent.display()))?;
- }
- let mut progress = format!(
- "section = \"{}\"\npackage = \"{}\"\nlayer = \"{}\"\n",
- recipe.package.section, recipe.package.name, recipe.package.layer
- );
- if let Some(revision) = bootstrap_recipe_revision(&recipe.package.name) {
- progress.push_str(&format!("recipe_revision = {revision}\n"));
- }
- fs::write(&recipe.progress_path, progress)
- .with_context(|| format!("Failed to write {}", recipe.progress_path.display()))?;
- Ok(())
-}
-
-fn bootstrap_package_is_complete(
- sysroot: &Path,
- config: &config::Config,
- recipe: &GeneratedRecipe,
-) -> Result {
- if package_is_retired(config, &recipe.package.name) {
- return Ok(true);
- }
-
- if !recipe.progress_path.exists() {
- return Ok(false);
- }
-
- if let Some(revision) = bootstrap_recipe_revision(&recipe.package.name) {
- let progress = fs::read_to_string(&recipe.progress_path)
- .with_context(|| format!("Failed to read {}", recipe.progress_path.display()))?;
- let expected = format!("recipe_revision = {revision}");
- if !progress.lines().any(|line| line.trim() == expected) {
- ui::info(format!(
- "Reinstalling {} because its bootstrap recipe changed",
- recipe.package.name
- ));
- return Ok(false);
- }
- }
-
- let db_path = config.installed_db_path(sysroot);
- let replaced = crate::db::get_all_replaces(&db_path).with_context(|| {
- format!(
- "Failed to inspect replacement metadata for {}",
- recipe.package.name
- )
- })?;
- if replaced.contains(&recipe.package.name) {
- return Ok(true);
- }
-
- let files =
- crate::db::get_package_files(&db_path, &recipe.package.name).with_context(|| {
- format!(
- "Failed to inspect installed files for {}",
- recipe.package.name
- )
- })?;
- if files.is_empty() {
- ui::info(format!(
- "Reinstalling {} because bootstrap progress exists but no installed files were recorded",
- recipe.package.name
- ));
- return Ok(false);
- }
-
- for rel_path in files {
- if crate::staging::is_purged_payload_path(&rel_path) {
- continue;
- }
- let disk_path = sysroot.join(&rel_path);
- if disk_path.symlink_metadata().is_err() {
- ui::info(format!(
- "Reinstalling {} because {} is missing from the sysroot",
- recipe.package.name, rel_path
- ));
- return Ok(false);
- }
- }
-
- for rel_path in bootstrap_required_payload_paths(&recipe.package.name) {
- let disk_path = sysroot.join(rel_path);
- if !disk_path.exists() {
- ui::info(format!(
- "Reinstalling {} because required bootstrap payload {} is missing from the sysroot",
- recipe.package.name, rel_path
- ));
- return Ok(false);
- }
- }
-
- Ok(true)
-}
-
-fn bootstrap_required_payload_paths(package: &str) -> &'static [&'static str] {
- match package {
- "bmake" => &["system/binaries/bmake", "system/share/mk/sys.mk"],
- "byacc" => &["system/binaries/yacc"],
- "llvm-clang-pass1" => &[
- "system/tools/bin/llvm-config",
- "system/tools/bin/llvm-tblgen",
- "system/tools/bin/clang-tblgen",
- ],
- "ubase" => &["system/binaries/id"],
- _ => &[],
- }
-}
-
-fn bootstrap_recipe_revision(package: &str) -> Option {
- match package {
- "bmake" => Some(2),
- "byacc" => Some(1),
- "llvm" => Some(3),
- "ubase" => Some(1),
- _ => None,
- }
-}
-
-const BSDDIFFUTILS_SBASE_HANDOFF_PATHS: &[&str] = &[
- "system/binaries/cmp",
- "system/documentation/man-pages/man1/cmp.1",
-];
-
-const BSDGREP_SBASE_HANDOFF_PATHS: &[&str] = &[
- "system/binaries/grep",
- "system/documentation/man-pages/man1/grep.1",
-];
-
-fn prepare_bootstrap_package_install(
- sysroot: &Path,
- config: &config::Config,
- package: &BookPackage,
-) -> Result<()> {
- let handoff_paths = match package.name.as_str() {
- "bsddiffutils" => BSDDIFFUTILS_SBASE_HANDOFF_PATHS,
- "bsdgrep" => BSDGREP_SBASE_HANDOFF_PATHS,
- _ => return Ok(()),
- };
-
- if handoff_paths.is_empty() {
- return Ok(());
- }
-
- let db_path = config.installed_db_path(sysroot);
- if !db_path.exists() {
- return Ok(());
- }
-
- let mut conn = rusqlite::Connection::open(&db_path)
- .with_context(|| format!("Failed to open package database {}", db_path.display()))?;
- let tx = conn.transaction()?;
-
- for rel_path in handoff_paths {
- let owner = match tx.query_row(
- "SELECT p.name FROM files f JOIN packages p ON f.package_id = p.id WHERE f.path = ?1",
- rusqlite::params![rel_path],
- |row| row.get::<_, String>(0),
- ) {
- Ok(owner) => owner,
- Err(rusqlite::Error::QueryReturnedNoRows) => continue,
- Err(err) => return Err(err).context("Failed to query package file ownership"),
- };
-
- if owner != "sbase" {
- continue;
- }
-
- tx.execute(
- "DELETE FROM files WHERE path = ?1 AND package_id = (SELECT id FROM packages WHERE name = 'sbase')",
- rusqlite::params![rel_path],
- )
- .with_context(|| format!("Failed to clear sbase ownership for {rel_path}"))?;
-
- let disk_path = sysroot.join(rel_path);
- if disk_path.exists() {
- fs::remove_file(&disk_path)
- .with_context(|| format!("Failed to remove {}", disk_path.display()))?;
- }
- ui::info(format!(
- "Handed off {} from sbase to {}",
- rel_path, package.name
- ));
- }
-
- tx.commit()
- .with_context(|| format!("Failed to update package database {}", db_path.display()))?;
- Ok(())
-}
-
-fn bootstrap_install_invocation(
- sysroot: &Path,
- recipe: &GeneratedRecipe,
- mode: BootstrapBuildMode,
- target: &str,
- depot_exe: &Path,
-) -> Result {
- let mut args = vec![
- OsString::from("install"),
- OsString::from("-r"),
- sysroot.as_os_str().to_os_string(),
- OsString::from("--yes"),
- OsString::from("--no-deps"),
- ];
- if mode == BootstrapBuildMode::Cross {
- args.push(OsString::from("--cross-prefix"));
- args.push(OsString::from(target));
- }
- args.push(recipe.spec_path.as_os_str().to_os_string());
-
- let mut env = Vec::new();
- if matches!(mode, BootstrapBuildMode::Cross | BootstrapBuildMode::Chroot) {
- env.push((
- OsString::from("PATH"),
- bootstrap_tool_path(
- sysroot,
- std::env::var_os("PATH").as_deref(),
- mode == BootstrapBuildMode::Chroot,
- )?,
- ));
- }
- env.push((
- OsString::from("DEPOT_LBI_SYSROOT"),
- bootstrap_sysroot_env_path(sysroot)?.into_os_string(),
- ));
- env.push((
- OsString::from("LWI_MAKE_FLAGS"),
- OsString::from(bootstrap_parallel_makeflags()),
- ));
- env.push((
- OsString::from("LWI_MAKE_JOBS"),
- OsString::from(bootstrap_parallel_make_jobs()),
- ));
- env.push((
- OsString::from(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS),
- OsString::from("1"),
- ));
- if mode == BootstrapBuildMode::Chroot {
- env.extend([
- (OsString::from("DEPOT_LBI_CHROOT"), OsString::from("1")),
- (
- OsString::from("DEPOT_LBI_CHROOT_ROOT"),
- sysroot.as_os_str().to_os_string(),
- ),
- (
- OsString::from("DEPOT_LBI_DEPOT_EXE"),
- depot_exe.as_os_str().to_os_string(),
- ),
- ]);
- }
-
- Ok(BootstrapInstallInvocation { args, env })
-}
-
-fn bootstrap_sysroot_env_path(sysroot: &Path) -> Result {
- if sysroot.is_absolute() {
- Ok(sysroot.to_path_buf())
- } else {
- Ok(std::env::current_dir()
- .context("Failed to resolve current directory for bootstrap sysroot")?
- .join(sysroot))
- }
-}
-
-fn bootstrap_tool_path(
- sysroot: &Path,
- inherited_path: Option<&OsStr>,
- include_target_paths: bool,
-) -> Result {
- let mut paths = vec![
- sysroot.join("system/tools/bin"),
- sysroot.join("system/tools/sbin"),
- ];
- if include_target_paths {
- paths.extend([
- sysroot.join("system/binaries"),
- sysroot.join("system/systembinaries"),
- ]);
- }
- if let Some(inherited) = inherited_path {
- paths.extend(std::env::split_paths(inherited));
- }
- std::env::join_paths(paths).context("Failed to construct bootstrap PATH")
-}
-
-fn bootstrap_parallel_makeflags() -> String {
- if let Ok(value) = std::env::var("LWI_MAKE_FLAGS") {
- let trimmed = value.trim();
- if !trimmed.is_empty() {
- return trimmed.to_string();
- }
- }
-
- format!("-j{}", bootstrap_parallel_make_jobs())
-}
-
-fn bootstrap_parallel_make_jobs() -> String {
- if let Ok(value) = std::env::var("LWI_MAKE_JOBS") {
- let trimmed = value.trim();
- if !trimmed.is_empty() {
- return trimmed.to_string();
- }
- }
-
- std::thread::available_parallelism()
- .map(|parallelism| parallelism.get())
- .unwrap_or(1)
- .to_string()
-}
-
-fn bootstrap_progress_root(config: &config::Config) -> PathBuf {
- config.db_dir.join(BOOTSTRAP_DIR).join("progress")
-}
-
-fn retired_package_progress_path(config: &config::Config, package: &str) -> PathBuf {
- bootstrap_progress_root(config).join(format!("retired-{package}.done"))
-}
-
-fn package_is_retired(config: &config::Config, package: &str) -> bool {
- retired_package_progress_path(config, package).exists()
-}
-
-fn step_is_done(
- step: &BookStep,
- recipes: &BTreeMap<&str, &GeneratedRecipe>,
- progress_root: &Path,
- sysroot: &Path,
- config: &config::Config,
-) -> bool {
- match step {
- BookStep::Package(package) => {
- recipes
- .get(package.recipe_id.as_str())
- .is_some_and(|recipe| {
- bootstrap_package_is_complete(sysroot, config, recipe).unwrap_or(false)
- })
- }
- BookStep::Operation(operation) => {
- operation_is_complete(progress_root, operation, sysroot, config).unwrap_or(false)
- }
- }
-}
-
-fn step_chapter(step: &BookStep) -> Option {
- section_chapter(step.section())
-}
-
-fn step_completes_chapter(step: &BookStep, next: Option<&BookStep>, chapter: u8) -> bool {
- step_chapter(step) == Some(chapter) && next.and_then(step_chapter) != Some(chapter)
-}
-
-fn step_requires_root(step: &BookStep) -> bool {
- match step {
- BookStep::Package(package) => build_mode_for_package(package) == BootstrapBuildMode::Chroot,
- BookStep::Operation(_) => true,
- }
-}
-
-fn build_mode_for_package(package: &BookPackage) -> BootstrapBuildMode {
- if use_cross_toolchain_by_default(package) {
- return BootstrapBuildMode::Cross;
- }
-
- match package.chapter {
- 5 => BootstrapBuildMode::Host,
- 6 => BootstrapBuildMode::Cross,
- 7..=9 => BootstrapBuildMode::Chroot,
- _ => BootstrapBuildMode::Host,
- }
-}
-
-fn stage_for_step(step: &BookStep) -> String {
- match step {
- BookStep::Package(package) => format!("bootstrap-{}", package.recipe_id),
- BookStep::Operation(operation) => format!("bootstrap-{}", operation.recipe_id),
- }
-}
-
-fn operation_progress_path(progress_root: &Path, operation: &BookOperation) -> PathBuf {
- progress_root.join(format!("{}.done", operation.recipe_id))
-}
-
-fn operation_is_complete(
- progress_root: &Path,
- operation: &BookOperation,
- sysroot: &Path,
- config: &config::Config,
-) -> Result {
- if !operation_progress_path(progress_root, operation).exists() {
- return Ok(false);
- }
-
- match operation.kind {
- BookOperationKind::CreateEssentialSystemFiles => {
- filesystem_package_is_registered(sysroot, config)
- }
- _ => Ok(true),
- }
-}
-
-fn retire_packages_after_chapter7(sysroot: &Path, config: &config::Config) -> Result<()> {
- let db_path = config.installed_db_path(sysroot);
- let installed = crate::db::get_installed_packages(&db_path).with_context(|| {
- format!(
- "Failed to inspect installed packages in {}",
- db_path.display()
- )
- })?;
- let needs_removal = CHAPTER7_RETIRED_PACKAGES
- .iter()
- .any(|package| installed.contains(*package));
-
- if needs_removal && !crate::fakeroot::is_root() {
- ensure_root_for_bootstrap()?;
- }
-
- for package in CHAPTER7_RETIRED_PACKAGES {
- if installed.contains(*package) {
- ui::info(format!(
- "Retiring bootstrap-only package {package} after chapter 7"
- ));
- crate::commands::remove_installed_package_with_hooks(package, sysroot, config)
- .with_context(|| format!("Failed to retire bootstrap-only package {package}"))?;
- }
-
- let progress_path = retired_package_progress_path(config, package);
- if let Some(parent) = progress_path.parent() {
- fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create {}", parent.display()))?;
- }
- fs::write(
- &progress_path,
- "reason = \"chapter7-complete\"\nretired = true\n",
- )
- .with_context(|| format!("Failed to write {}", progress_path.display()))?;
- }
-
- Ok(())
-}
-
-fn run_operation_step(
- sysroot: &Path,
- config: &config::Config,
- operation: &BookOperation,
- target: &str,
- arch: &str,
-) -> Result<()> {
- let progress_path = operation_progress_path(&bootstrap_progress_root(config), operation);
- if operation_is_complete(&bootstrap_progress_root(config), operation, sysroot, config)? {
- ui::info(format!(
- "Skipping already completed bootstrap step {} ({})",
- operation.title, operation.section
- ));
- return Ok(());
- }
-
- ui::info(format!("Running bootstrap step: {}", operation.title));
- match operation.kind {
- BookOperationKind::ResetTargetTreeOwnership => reset_target_tree_ownership(sysroot)?,
- BookOperationKind::CreateVirtualFilesystemLinkTargets => {
- create_virtual_filesystem_link_targets(sysroot)?
- }
- BookOperationKind::CopyBuildProfile => copy_build_profile(sysroot, target, arch)?,
- BookOperationKind::EnterChroot => mark_chroot_entry(sysroot)?,
- BookOperationKind::CreateEssentialSystemFiles => {
- create_essential_system_files(sysroot)?;
- register_filesystem_package(sysroot, config)?;
- }
- }
-
- if let Some(parent) = progress_path.parent() {
- fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create {}", parent.display()))?;
- }
- fs::write(
- &progress_path,
- format!(
- "section = \"{}\"\noperation = \"{}\"\n",
- operation.section,
- operation_slug(operation.kind)
- ),
- )
- .with_context(|| format!("Failed to write {}", progress_path.display()))?;
- Ok(())
-}
-
-fn reset_target_tree_ownership(sysroot: &Path) -> Result<()> {
- let status = Command::new("chown")
- .arg("-R")
- .arg("0:0")
- .arg(sysroot)
- .status()
- .with_context(|| format!("Failed to run chown for {}", sysroot.display()))?;
- if !status.success() {
- anyhow::bail!(
- "Failed to reset target tree ownership for {}: chown exited with {}",
- sysroot.display(),
- status
- );
- }
- Ok(())
-}
-
-fn create_virtual_filesystem_link_targets(sysroot: &Path) -> Result<()> {
- system_state::ensure_lbi_layout_paths(sysroot)?;
- for rel in [
- "system/devices/pts",
- "system/devices/shm",
- "system/temporary",
- ] {
- let path = sysroot.join(rel);
- fs::create_dir_all(&path)
- .with_context(|| format!("Failed to create {}", path.display()))?;
- }
- Ok(())
-}
-
-fn copy_build_profile(sysroot: &Path, target: &str, arch: &str) -> Result<()> {
- let profile_dir = sysroot.join("etc/profile.d");
- fs::create_dir_all(&profile_dir)
- .with_context(|| format!("Failed to create {}", profile_dir.display()))?;
- let profile_path = profile_dir.join("lbi-build.sh");
- let content = format!(
- r#"# Generated by `depot bootstrap`.
-export LBI_TARGET="{target}"
-export LBI_ARCH="{arch}"
-export CHOST="$LBI_TARGET"
-export CARCH="$LBI_ARCH"
-export LBI_ROOT="/"
-export LBI_SOURCES="/sources"
-export PATH="/system/tools/bin:/system/binaries:/system/systembinaries:/bin:/usr/bin:/sbin:/usr/sbin"
-
-lbi_configure() {{
- ./configure \
- --target="$LBI_TARGET" \
- --host="$LBI_TARGET" \
- --prefix=/system \
- --bindir=/system/binaries \
- --sbindir=/system/systembinaries \
- --libdir=/system/libraries \
- --includedir=/system/headers \
- --sysconfdir=/system/configuration \
- --localstatedir=/system/variable \
- --datarootdir=/system/share \
- --mandir=/system/documentation/man-pages \
- --infodir=/system/documentation/info \
- "$@"
-}}
-"#
- );
- fs::write(&profile_path, content)
- .with_context(|| format!("Failed to write {}", profile_path.display()))?;
- Ok(())
-}
-
-fn mark_chroot_entry(sysroot: &Path) -> Result<()> {
- let marker = sysroot.join("var/lib/depot/bootstrap-chroot-ready");
- if let Some(parent) = marker.parent() {
- fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create {}", parent.display()))?;
- }
- fs::write(&marker, "ready\n").with_context(|| format!("Failed to write {}", marker.display()))
-}
-
-fn create_essential_system_files(sysroot: &Path) -> Result<()> {
- write_if_missing(
- &sysroot.join("etc/passwd"),
- "root:x:0:0:root:/system/charlie:/bin/oksh\n\
-bin:x:1:1:bin:/dev/null:/system/binaries/false\n\
-daemon:x:6:6:Daemon User:/dev/null:/system/binaries/false\n\
-messagebus:x:18:18:D-Bus Message Daemon User:/run/dbus:/system/binaries/false\n\
-uuidd:x:80:80:UUID Generation Daemon User:/dev/null:/system/binaries/false\n\
-nobody:x:65534:65534:Unprivileged User:/dev/null:/system/binaries/false\n",
- )?;
- write_if_missing(
- &sysroot.join("etc/group"),
- "root:x:0:\n\
-bin:x:1:daemon\n\
-sys:x:2:\n\
-kmem:x:3:\n\
-tape:x:4:\n\
-tty:x:5:\n\
-daemon:x:6:\n\
-floppy:x:7:\n\
-disk:x:8:\n\
-lp:x:9:\n\
-dialout:x:10:\n\
-audio:x:11:\n\
-video:x:12:\n\
-utmp:x:13:\n\
-clock:x:14:\n\
-cdrom:x:15:\n\
-adm:x:16:\n\
-messagebus:x:18:\n\
-input:x:24:\n\
-mail:x:34:\n\
-kvm:x:61:\n\
-uuidd:x:80:\n\
-wheel:x:97:\n\
-users:x:999:\n\
-nogroup:x:65534:\n",
- )?;
- write_if_missing(
- &sysroot.join("etc/hosts"),
- "127.0.0.1 localhost\n::1 localhost\n",
- )?;
- write_if_missing(
- &sysroot.join("etc/fstab"),
- "# file system mount point type options dump pass\n",
- )?;
- write_if_missing(
- &sysroot.join("etc/shells"),
- "/bin/sh\n/system/binaries/sh\n",
- )?;
- Ok(())
-}
-
-fn filesystem_package_is_registered(sysroot: &Path, config: &config::Config) -> Result {
- let installed = crate::db::get_installed_packages(&config.installed_db_path(sysroot))?;
- Ok(installed.contains(FILESYSTEM_PACKAGE))
-}
-
-fn register_filesystem_package(sysroot: &Path, config: &config::Config) -> Result<()> {
- ui::info("Registering system layout as package filesystem");
- system_state::ensure_lbi_layout_paths(sysroot)?;
- create_essential_system_files(sysroot)?;
- let staged = tempfile::tempdir().context("Failed to create filesystem package staging dir")?;
- system_state::ensure_lbi_layout_paths(staged.path())?;
- create_essential_system_files(staged.path())?;
- crate::db::register_package(
- &config.installed_db_path(sysroot),
- &filesystem_package_spec(),
- staged.path(),
- )
- .context("Failed to register filesystem package")
-}
-
-fn filesystem_package_spec() -> crate::package::PackageSpec {
- crate::package::PackageSpec {
- package: crate::package::PackageInfo {
- name: FILESYSTEM_PACKAGE.to_string(),
- real_name: None,
- version: "1.0".to_string(),
- revision: 1,
- description: "Linux by Intent system layout".to_string(),
- homepage: "https://www.vertexlinux.net/lbi/".to_string(),
- abi_breaking: false,
- license: vec!["MIT".to_string()],
- },
- packages: Vec::new(),
- alternatives: crate::package::Alternatives::default(),
- manual_sources: Vec::new(),
- source: Vec::new(),
- build: crate::package::Build {
- build_type: crate::package::BuildType::Meta,
- flags: crate::package::BuildFlags::default(),
- },
- dependencies: crate::package::Dependencies {
- groups: vec![BASE_LAYER.to_string()],
- ..crate::package::Dependencies::default()
- },
- package_alternatives: BTreeMap::new(),
- package_dependencies: BTreeMap::new(),
- spec_dir: PathBuf::new(),
- }
-}
-
-fn write_if_missing(path: &Path, content: &str) -> Result<()> {
- if path.exists() {
- return Ok(());
- }
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create {}", parent.display()))?;
- }
- fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum RootTransition {
- AlreadyRoot,
- Reexec(PathBuf),
-}
-
-fn ensure_root_for_bootstrap() -> Result<()> {
- match root_transition(
- crate::fakeroot::is_root(),
- std::env::var_os("PATH").as_deref(),
- )? {
- RootTransition::AlreadyRoot => Ok(()),
- RootTransition::Reexec(helper) => {
- let exe = std::env::current_exe().context("Failed to locate depot executable")?;
- ui::info(format!(
- "Re-executing bootstrap through {} for root-owned and chroot steps",
- helper.display()
- ));
- let status = Command::new(&helper)
- .arg(exe)
- .args(std::env::args_os().skip(1))
- .status()
- .with_context(|| {
- format!(
- "Failed to re-execute depot bootstrap via {}",
- helper.display()
- )
- })?;
- if status.success() {
- std::process::exit(0);
- }
- anyhow::bail!(
- "depot bootstrap via {} failed with status {}",
- helper.display(),
- status
- );
- }
- }
-}
-
-fn root_transition(is_root: bool, path: Option<&OsStr>) -> Result {
- if is_root {
- return Ok(RootTransition::AlreadyRoot);
- }
- let Some(helper) = privilege_helper(path) else {
- anyhow::bail!(
- "Bootstrap needs root for ownership and chroot steps, but neither sudo nor doas was found in PATH"
- );
- };
- Ok(RootTransition::Reexec(helper))
-}
-
-fn privilege_helper(path: Option<&OsStr>) -> Option {
- for name in ["sudo", "doas"] {
- if let Some(found) = find_executable_in_path(name, path) {
- return Some(found);
- }
- }
- None
-}
-
-fn find_executable_in_path(name: &str, path: Option<&OsStr>) -> Option {
- let path = path?;
- for dir in std::env::split_paths(path) {
- let candidate = dir.join(name);
- if !candidate.is_file() {
- continue;
- }
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let Ok(metadata) = fs::metadata(&candidate) else {
- continue;
- };
- if metadata.permissions().mode() & 0o111 == 0 {
- continue;
- }
- }
- return Some(candidate);
- }
- None
-}
-
-#[derive(Default)]
-struct BootstrapChrootMountGuard {
- mounted: Vec,
- created_files: Vec,
-}
-
-impl BootstrapChrootMountGuard {
- fn mount_path(
- &mut self,
- source: &Path,
- target: &Path,
- fstype: Option<&str>,
- flags: MountFlags,
- data: Option<&str>,
- ) -> Result<()> {
- let mut builder = Mount::builder().flags(flags);
- if let Some(fstype) = fstype {
- builder = builder.fstype(fstype);
- }
- if let Some(data) = data {
- builder = builder.data(data);
- }
- let mount = builder
- .mount(source, target)
- .with_context(|| format!("Failed to mount {}", target.display()))?;
- self.mounted.push(mount);
- Ok(())
- }
-
- fn prepare_file_mount_target(&mut self, target: &Path) -> Result<()> {
- if let Some(parent) = target.parent() {
- fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create {}", parent.display()))?;
- }
-
- match target.symlink_metadata() {
- Ok(metadata) if metadata.file_type().is_dir() => {
- anyhow::bail!("Mount target is a directory: {}", target.display());
- }
- Ok(_) => Ok(()),
- Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
- fs::File::create(target)
- .with_context(|| format!("Failed to create {}", target.display()))?;
- self.created_files.push(target.to_path_buf());
- Ok(())
- }
- Err(err) => Err(err).with_context(|| format!("Failed to inspect {}", target.display())),
- }
- }
-
- fn mount_host_resolver_config(&mut self, rootfs: &Path) -> Result {
- let source = Path::new("/etc/resolv.conf");
- if !source.is_file() {
- return Ok(false);
- }
-
- let target = rootfs.join("etc/resolv.conf");
- self.prepare_file_mount_target(&target)?;
- self.mount_path(source, &target, None, MountFlags::BIND, None)?;
- Ok(true)
- }
-}
-
-impl Drop for BootstrapChrootMountGuard {
- fn drop(&mut self) {
- for mount in self.mounted.iter().rev() {
- if mount.unmount(UnmountFlags::empty()).is_ok() {
- continue;
- }
- let _ = mount.unmount(UnmountFlags::DETACH);
- }
- for path in self.created_files.iter().rev() {
- let _ = fs::remove_file(path);
- }
- }
-}
-
-pub(crate) fn run_bootstrap_chroot(
- rootfs: &Path,
- sources: &Path,
- destdir: &Path,
- workdir: &str,
- script: &str,
-) -> Result<()> {
- if !crate::fakeroot::is_root() {
- anyhow::bail!("internal bootstrap-chroot requires root");
- }
- if !sources.is_dir() {
- anyhow::bail!(
- "Bootstrap source workspace is not a directory: {}",
- sources.display()
- );
- }
- fs::create_dir_all(destdir)
- .with_context(|| format!("Failed to create {}", destdir.display()))?;
-
- let _mounts = mount_bootstrap_chroot(rootfs, sources, destdir)?;
- install_bootstrap_chroot_tool_shims(rootfs)?;
- let command = format!(
- "cd {} && exec /bin/sh {}",
- shell_quote(workdir),
- shell_quote(script)
- );
- let mut cmd = Command::new("chroot");
- cmd.arg(rootfs)
- .arg("/bin/sh")
- .arg("-lc")
- .arg(command)
- .env("DEPOT_LBI_INSIDE_CHROOT", "1")
- .env("DESTDIR", "/destdir")
- .env("DEPOT_PRIMARY_DESTDIR", "/destdir")
- .env("DEPOT_LBI_SYSROOT", "/")
- .env("DEPOT_STARBUILD_WORKDIR", "/sources")
- .env("LBI_ROOT", "/destdir")
- .env("LBI_SYSROOT", "/")
- .env("LBI_SOURCES", "/sources")
- .stdin(Stdio::inherit())
- .stdout(Stdio::inherit())
- .stderr(Stdio::inherit());
- apply_bootstrap_chroot_tool_env(&mut cmd);
- let status = crate::interrupts::command_status(&mut cmd)
- .with_context(|| format!("Failed to execute bootstrap build in {}", rootfs.display()))?;
- if !status.success() {
- anyhow::bail!("Bootstrap chroot build failed with status {}", status);
- }
- Ok(())
-}
-
-fn bootstrap_chroot_tool_env() -> &'static [(&'static str, &'static str)] {
- &[
- ("CC", "cc"),
- ("CXX", "c++"),
- ("AR", "ar"),
- ("RANLIB", "ranlib"),
- ("NM", "nm"),
- ("STRIP", "strip"),
- ("PATH", BOOTSTRAP_CHROOT_PATH),
- ]
-}
-
-fn apply_bootstrap_chroot_tool_env(cmd: &mut Command) {
- for (key, value) in bootstrap_chroot_tool_env() {
- cmd.env(key, value);
- }
- cmd.env_remove("CROSS_COMPILE");
-}
-
-fn install_bootstrap_chroot_tool_shims(rootfs: &Path) -> Result<()> {
- let shim_dir = rootfs.join(BOOTSTRAP_CHROOT_SHIM_DIR.trim_start_matches('/'));
- fs::create_dir_all(&shim_dir)
- .with_context(|| format!("Failed to create {}", shim_dir.display()))?;
-
- let id_path = shim_dir.join("id");
- fs::write(
- &id_path,
- r#"#!/bin/sh
-case "${1:-}" in
- "" )
- printf '%s\n' 'uid=0(root) gid=0(root) groups=0(root)'
- ;;
- -u )
- printf '%s\n' 0
- ;;
- -g )
- printf '%s\n' 0
- ;;
- -G )
- printf '%s\n' 0
- ;;
- -un )
- printf '%s\n' root
- ;;
- -gn )
- printf '%s\n' root
- ;;
- * )
- printf '%s\n' "id: unsupported bootstrap option: $1" >&2
- exit 1
- ;;
-esac
-"#,
- )
- .with_context(|| format!("Failed to write {}", id_path.display()))?;
- make_executable(&id_path)?;
- Ok(())
-}
-
-#[cfg(unix)]
-fn make_executable(path: &Path) -> Result<()> {
- use std::os::unix::fs::PermissionsExt;
-
- let mut perms = fs::metadata(path)
- .with_context(|| format!("Failed to stat {}", path.display()))?
- .permissions();
- perms.set_mode(0o755);
- fs::set_permissions(path, perms).with_context(|| format!("Failed to chmod {}", path.display()))
-}
-
-#[cfg(not(unix))]
-fn make_executable(_path: &Path) -> Result<()> {
- Ok(())
-}
-
-fn mount_bootstrap_chroot(
- rootfs: &Path,
- sources: &Path,
- destdir: &Path,
-) -> Result {
- for rel in [
- "proc", "dev", "dev/pts", "sys", "run", "tmp", "sources", "destdir",
- ] {
- let path = rootfs.join(rel);
- fs::create_dir_all(&path)
- .with_context(|| format!("Failed to create {}", path.display()))?;
- }
- set_world_writable_sticky(&rootfs.join("tmp"))?;
-
- let mut guard = BootstrapChrootMountGuard::default();
- guard.mount_path(
- Path::new("proc"),
- &rootfs.join("proc"),
- Some("proc"),
- MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::NOSUID,
- None,
- )?;
- guard.mount_path(
- Path::new("/dev"),
- &rootfs.join("dev"),
- None,
- MountFlags::BIND,
- None,
- )?;
- guard.mount_path(
- Path::new("sysfs"),
- &rootfs.join("sys"),
- Some("sysfs"),
- MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::NOSUID,
- None,
- )?;
- if let Err(_err) = guard.mount_path(
- Path::new("devpts"),
- &rootfs.join("dev/pts"),
- Some("devpts"),
- MountFlags::NOSUID | MountFlags::NOEXEC,
- Some("gid=5,mode=620"),
- ) {
- guard.mount_path(
- Path::new("devpts"),
- &rootfs.join("dev/pts"),
- Some("devpts"),
- MountFlags::NOSUID | MountFlags::NOEXEC,
- None,
- )?;
- }
- guard.mount_path(
- Path::new("/run"),
- &rootfs.join("run"),
- None,
- MountFlags::BIND,
- None,
- )?;
- guard.mount_path(
- sources,
- &rootfs.join("sources"),
- None,
- MountFlags::BIND,
- None,
- )?;
- guard.mount_path(
- destdir,
- &rootfs.join("destdir"),
- None,
- MountFlags::BIND,
- None,
- )?;
- if guard.mount_host_resolver_config(rootfs)? {
- ui::info("Mounted host resolver configuration for bootstrap chroot");
- }
- Ok(guard)
-}
-
-#[cfg(unix)]
-fn set_world_writable_sticky(path: &Path) -> Result<()> {
- use std::os::unix::fs::PermissionsExt;
-
- let mut perms = fs::metadata(path)
- .with_context(|| format!("Failed to stat {}", path.display()))?
- .permissions();
- perms.set_mode(0o1777);
- fs::set_permissions(path, perms)
- .with_context(|| format!("Failed to set permissions on {}", path.display()))
-}
-
-#[cfg(not(unix))]
-fn set_world_writable_sticky(_path: &Path) -> Result<()> {
- Ok(())
-}
-
-fn shell_quote(input: &str) -> String {
- if input.is_empty() {
- return "''".to_string();
- }
- format!("'{}'", input.replace('\'', "'\\''"))
-}
-
-fn load_book_pdf(args: &BootstrapArgs) -> Result> {
- if let Some(path) = &args.book_pdf {
- return fs::read(path).with_context(|| format!("Failed to read {}", path.display()));
- }
-
- ui::info(format!("Fetching Linux by Intent book: {}", args.book_url));
- let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
- let client = source::build_blocking_client(&ua, Some(Duration::from_secs(60)))?;
- let fetch_url = fresh_book_fetch_url(&args.book_url)?;
- let mut response = client
- .get(fetch_url.as_str())
- .send()
- .with_context(|| format!("Failed to fetch {}", args.book_url))?;
- let status = response.status();
- if !status.is_success() {
- anyhow::bail!("HTTP error fetching {}: {}", args.book_url, status);
- }
- let mut body = Vec::new();
- response
- .copy_to(&mut body)
- .with_context(|| format!("Failed to read {}", args.book_url))?;
- Ok(body)
-}
-
-fn generate_recipes(
- args: &BootstrapArgs,
- config: &config::Config,
- packages: &[BookPackage],
- target: &str,
- arch: &str,
-) -> Result> {
- let root = config.db_dir.join(BOOTSTRAP_DIR);
- let recipe_root = root.join("recipes");
- let page_root = root.join("pages");
- let progress_root = root.join("progress");
- fs::create_dir_all(&recipe_root)
- .with_context(|| format!("Failed to create {}", recipe_root.display()))?;
- fs::create_dir_all(&page_root)
- .with_context(|| format!("Failed to create {}", page_root.display()))?;
- fs::create_dir_all(&progress_root)
- .with_context(|| format!("Failed to create {}", progress_root.display()))?;
-
- let manifest = load_source_manifest(args, &root)?;
- let mut generated = Vec::new();
- for package in packages {
- let (actual_page_url, html) = fetch_package_page(package)?;
- let mut package = package.clone();
- package.page_url = actual_page_url;
-
- let page_path = page_root.join(format!("{}.html", package.recipe_id));
- fs::write(&page_path, &html)
- .with_context(|| format!("Failed to write {}", page_path.display()))?;
-
- let page_recipe = parse_page_recipe(&html, &package)?;
- let recipe_dir = recipe_root.join(&package.layer).join(&package.recipe_id);
- fs::create_dir_all(&recipe_dir)
- .with_context(|| format!("Failed to create {}", recipe_dir.display()))?;
- let spec_path = recipe_dir.join(format!("{}.toml", package.name));
- let build_path = recipe_dir.join("build.sh");
- write_generated_recipe(
- &spec_path,
- &build_path,
- &package,
- &page_recipe,
- &manifest,
- target,
- arch,
- )?;
- generated.push(GeneratedRecipe {
- package: package.clone(),
- spec_path,
- progress_path: progress_root.join(format!("{}.done", package.recipe_id)),
- });
- }
- Ok(generated)
-}
-
-fn load_source_manifest(args: &BootstrapArgs, root: &Path) -> Result {
- let manifest_url = book_base_url(&args.book_url)?.join("scripts/sources.manifest")?;
- let b2sums_url = book_base_url(&args.book_url)?.join("scripts/sources.b2sums")?;
- let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
- let client = source::build_blocking_client(&ua, Some(Duration::from_secs(60)))?;
- let manifest_fetch_url = fresh_book_fetch_url(manifest_url.as_str())?;
- let b2sums_fetch_url = fresh_book_fetch_url(b2sums_url.as_str())?;
- ui::info(format!(
- "Fetching Linux by Intent source manifest: {manifest_url}"
- ));
- let body = client
- .get(manifest_fetch_url.as_str())
- .send()
- .with_context(|| format!("Failed to fetch {manifest_url}"))?
- .error_for_status()
- .with_context(|| format!("HTTP error fetching {manifest_url}"))?
- .text()
- .with_context(|| format!("Failed to read {manifest_url}"))?;
- let path = root.join("sources.manifest");
- fs::write(&path, &body).with_context(|| format!("Failed to write {}", path.display()))?;
- ui::info(format!(
- "Fetching Linux by Intent BLAKE2 source manifest: {b2sums_url}"
- ));
- let b2sums = client
- .get(b2sums_fetch_url.as_str())
- .send()
- .with_context(|| format!("Failed to fetch {b2sums_url}"))?
- .error_for_status()
- .with_context(|| format!("HTTP error fetching {b2sums_url}"))?
- .text()
- .with_context(|| format!("Failed to read {b2sums_url}"))?;
- let b2sums_path = root.join("sources.b2sums");
- fs::write(&b2sums_path, &b2sums)
- .with_context(|| format!("Failed to write {}", b2sums_path.display()))?;
- Ok(SourceManifest {
- entries: parse_source_manifest(&body),
- blake2b_512: parse_source_b2sums(&b2sums)?,
- })
-}
-
-fn fetch_page(url: &str) -> Result {
- let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
- let client = source::build_blocking_client(&ua, Some(Duration::from_secs(60)))?;
- let fetch_url = fresh_book_fetch_url(url)?;
- client
- .get(fetch_url.as_str())
- .send()
- .with_context(|| format!("Failed to fetch {url}"))?
- .error_for_status()
- .with_context(|| format!("HTTP error fetching {url}"))?
- .text()
- .with_context(|| format!("Failed to read {url}"))
-}
-
-fn fresh_book_fetch_url(url: &str) -> Result {
- let mut parsed = Url::parse(url).with_context(|| format!("Invalid book fetch URL: {url}"))?;
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .context("System clock is before UNIX epoch")?
- .as_secs()
- .to_string();
- parsed
- .query_pairs_mut()
- .append_pair(BOOK_FETCH_CACHE_BUST_PARAM, &stamp);
- Ok(parsed.to_string())
-}
-
-fn fetch_package_page(package: &BookPackage) -> Result<(String, String)> {
- match fetch_page(&package.page_url) {
- Ok(html) => Ok((package.page_url.clone(), html)),
- Err(primary_err) => {
- let Some(fallback_url) = fallback_package_page_url(package)? else {
- return Err(primary_err);
- };
-
- ui::info(format!(
- "Primary page fetch failed for {}; trying fallback {}",
- package.page_url, fallback_url
- ));
-
- fetch_page(&fallback_url)
- .map(|html| (fallback_url, html))
- .with_context(|| {
- format!(
- "Failed to fetch package page for {}. Primary URL was {}; fallback URL was tried after primary error: {primary_err}",
- package.name,
- package.page_url
- )
- })
- }
- }
-}
-
-fn fallback_package_page_url(package: &BookPackage) -> Result
".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:
- "#;
-
- 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:
- "#;
- 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:
- "#;
-
- 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:
- "#;
-
- 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