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