Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-build-thing/src/container_road.rs

//! **The container road, whole** — enter the pinned `[build.toolchain]` image once,
//! build a declared cargo row in it, and run the row's `[[build.wasm]]` stages
//! (wasm-bindgen, the component manifest) in the SAME image.
//!
//! THE LAW: *a build thing builds in a container or a KVM, never on the host.*
//! [`build_variant_in_container`](crate::cargo_container::build_variant_in_container)
//! already compiled in the image, but a wasm row is not done when cargo exits: its
//! bindgen and manifest stages ran through the one [`WasmStageRunner`] edda had,
//! [`XtaskWasmStageRunner`](crate::wasm::XtaskWasmStageRunner) — the HOST's
//! `wasm-bindgen` and HOST cargo. So a module the image compiled was wrapped by a
//! host tool.
//!
//! Lifted verbatim-in-substance from facett (`xtask/src/door.rs` + its stages module, deleted
//! there once the road lived here — facett 6127d496 —
//! measured on oden and dwarf 2026-09-29) when a second and third repo wanted it:
//! `nornir_pages::demo_wasm` (nornir, dwarves) builds its demo row through
//! [`build_row`], and facett's xtask uses the same three calls. One road, no copy.
//!
//! ## What the host contributes
//!
//! A container engine socket and a workdir — nothing else. [`Road::enter`] asks the
//! engine for the PINNED reference first and mints only when it is absent. A mint
//! writes the recipe's own tag (`<image>:recipe-<hash>`), never `<image>:live`: a
//! floating tag every session on a shared box sees, and moving it un-named the pin
//! other repos build in (2026-10-05).
//!
//! ## The bindgen CLI is built IN the image, offline, once per version
//!
//! The toolchain image carries no `wasm-bindgen`, and the run is `--network none`.
//! So the stage `cargo install --offline --locked wasm-bindgen-cli --version
//! <Cargo.lock's>` into the bound target dir and reuses it. The registry is bound
//! READ-ONLY and `cargo install` must extract sources it has never built (MEASURED
//! oden 2026-09-29: `Read-only file system` on `/cargo/registry/src/…`), so the install
//! runs with a scratch `CARGO_HOME` whose `index` and `cache` are symlinks onto the
//! read-only registry and whose `src` is writable; the scratch is removed afterwards.

use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};

use crate::cargo_container::{
    self as cc, CargoContainerRunner, CargoRunRequest, CargoToolchain, DraupnirCargoRunner, HostPaths,
};
use crate::variant::{BuildVariant, BuildVariantSet};
use crate::wasm::{WasmStageRun, WasmStageRunner};

/// **The build environment, resolved and entered ONCE** — the pinned toolchain,
/// the host paths it binds, and the engine.
pub struct Road {
    /// The validated, pinned toolchain (after the `CARGO_BUILD_JOBS` cap).
    pub tc: CargoToolchain,
    /// The host target dir and registry the image binds.
    pub paths: HostPaths,
    runner: DraupnirCargoRunner,
}

/// **The declared toolchain, validated, with the one run-time cap a caller may
/// apply**: `CARGO_BUILD_JOBS` below the declared `cpus` lowers the container's CPU
/// bound (announced), never raises it. It changes scheduling, not the artifact.
///
/// Refuses BY NAME: no `[build.toolchain]`, and every `CargoToolchain::validate`
/// arm. There is no host fallback.
pub fn toolchain(set: &BuildVariantSet) -> Result<CargoToolchain> {
    let mut tc = cc::toolchain_with_override(set.toolchain.clone())?.ok_or_else(|| {
        anyhow!(
            "the declaration has no `[build.toolchain]`, so there is no image to build in — \
             and the container road does NOT fall back to host cargo"
        )
    })?;
    tc.validate()?;
    if let Some(jobs) = std::env::var("CARGO_BUILD_JOBS").ok().and_then(|j| j.trim().parse::<u32>().ok()) {
        if jobs > 0 && jobs < tc.cpus {
            eprintln!("[cargo-container] CARGO_BUILD_JOBS={jobs}: container bound {} → {jobs} cpus", tc.cpus);
            tc.cpus = jobs;
        }
    }
    Ok(tc)
}

impl Road {
    /// **Enter the road**: validate the toolchain, ask the engine for the PINNED
    /// reference; present → build in it and mint nothing; absent → mint from the
    /// Containerfile embedded in this crate (reproducibly, under the recipe tag)
    /// and require the pin is what the engine now holds. A pin the recipe does not
    /// mint is refused by name.
    pub fn enter(set: &BuildVariantSet, root: &Path) -> Result<Road> {
        let tc = toolchain(set)?;
        let paths = HostPaths::from_env(root);
        std::fs::create_dir_all(&paths.target_dir)
            .with_context(|| format!("create {}", paths.target_dir.display()))?;
        let runner = DraupnirCargoRunner::new();
        let pinned = tc.image_ref();
        let t0 = std::time::Instant::now();
        // Step 1 never writes a floating tag: present → nothing minted; absent →
        // minted under the recipe's own content-addressed tag, and the pin must
        // resolve afterwards (`cargo_container::ensure_declared_toolchain`).
        let ctx = paths.target_dir.join(format!("{}-context", tc.image));
        let minted = runner.ensure_declared_toolchain(&ctx, &tc).map_err(|e| {
            anyhow!(
                "step 1, the declared toolchain image {pinned}: {e:#}\n  The build needs a \
                 container engine socket (rootless podman: $XDG_RUNTIME_DIR/podman/podman.sock) \
                 — the ONE thing the host is asked for. Nothing pulls {pinned}; every box mints \
                 it from the committed recipe."
            )
        })?;
        eprintln!(
            "[cargo-container] {} → building in {pinned} (rust {}, {} MiB, {} cpus, network {}, \
             siblings: {})",
            match &minted {
                cc::ToolchainMint::Minted { tag } => {
                    format!("{tag} minted in {:.1} s", t0.elapsed().as_secs_f64())
                }
                cc::ToolchainMint::AlreadyPresent => {
                    "the pin is already in the engine, nothing minted".to_string()
                }
            },
            tc.rust_version,
            tc.mem_limit_mb,
            tc.cpus,
            if tc.network { "on" } else { "none" },
            tc.siblings.join(", ")
        );
        Ok(Road { tc, paths, runner })
    }

    /// The engine the road runs on.
    pub fn runner(&self) -> &DraupnirCargoRunner {
        &self.runner
    }
}

/// The refusal for a row cargo cannot build (container, package, iso, …), naming
/// it — `None` for a cargo kind.
#[must_use]
pub fn refusal(v: &BuildVariant) -> Option<String> {
    (!v.kind.is_cargo()).then(|| {
        format!(
            "refused: '{}' is kind '{}', not a cargo build — the container road builds cargo rows only",
            v.name,
            v.kind.as_str()
        )
    })
}

/// **Build ONE cargo row IN THE CONTAINER**: the forge's argv
/// (`build_variant_in_container`), then the row's `[[build.wasm]]` stages when it
/// declares a cell — in the same image ([`ContainerStages`]). Returns the files
/// produced (the bundle's, when there is a cell). A row cargo cannot build
/// (container, package, iso, …) is refused BY NAME.
pub fn build_row(set: &BuildVariantSet, v: &BuildVariant, root: &Path, road: &Road) -> Result<Vec<PathBuf>> {
    if let Some(r) = refusal(v) {
        return Err(anyhow!(r));
    }
    eprintln!("── {} · {} · in {}", v.name, v.command_line(), road.tc.image_ref());
    let t0 = std::time::Instant::now();
    let out = cc::build_variant_in_container(
        set,
        &v.name,
        root,
        &road.paths.target_dir,
        road.paths.registry_bind(),
        &road.tc,
        &road.runner,
    )?;
    if !out.success {
        return Err(anyhow!(
            "variant '{}': cargo did not succeed in {} ({})",
            v.name,
            road.tc.image_ref(),
            v.command_line()
        ));
    }
    eprintln!(
        "   built in {:.1} s · provenance digest {} · toolchain {}",
        t0.elapsed().as_secs_f64(),
        out.attestation.digest_hex(),
        out.attestation.record.toolchain.rustc
    );
    match set.wasm(&v.name) {
        None => Ok(out.artifacts),
        Some(cell) => {
            // The raw module is where the CONTAINER wrote it — under the bound target dir.
            let raw = crate::wasm::cargo_wasm_path(v, &road.paths.target_dir);
            let stages = ContainerStages { tc: &road.tc, paths: &road.paths, runner: &road.runner, row: v };
            let t1 = std::time::Instant::now();
            let produced = crate::wasm::run_stages(cell, v, &raw, root, &stages)?;
            eprintln!("   stages in {:.1} s · {}", t1.elapsed().as_secs_f64(), stages.describe());
            Ok(produced)
        }
    }
}

/// **A declared wasm row, built and staged** — [`Road::enter`] + [`build_row`], and
/// the dist dir its `[[build.wasm]]` cell writes. Refused by name when the row does
/// not exist or declares no cell (no bindgen, no bundle).
pub fn build_wasm_row(set: &BuildVariantSet, variant: &str, root: &Path) -> Result<(Vec<PathBuf>, PathBuf)> {
    let row = set.require(variant)?;
    let cell = set
        .wasm(&row.name)
        .ok_or_else(|| anyhow!("'{}' declares no [[build.wasm]] cell — no bindgen, no bundle", row.name))?;
    let dist = cell.out_dir(root);
    let road = Road::enter(set, root)?;
    let produced = build_row(set, row, root, &road)?;
    Ok((produced, dist))
}

/// The row's feature selection as cargo flags — the same two fields `cargo_args()`
/// renders, in the same spelling, so the manifest example is compiled with the
/// features of the module it describes.
#[must_use]
pub fn row_feature_args(row: &BuildVariant) -> Vec<String> {
    let mut a = Vec::new();
    if row.no_default_features {
        a.push("--no-default-features".to_string());
    }
    if !row.features.is_empty() {
        a.push("--features".to_string());
        a.push(row.features.join(","));
    }
    if row.all_features {
        a.push("--all-features".to_string());
    }
    a
}

/// **The `[[build.wasm]]` stages, in the pinned image** — the second impl of
/// [`WasmStageRunner`], over the container runner. Owns no row knowledge: stage,
/// input, out dir and flags arrive typed from the cell; the wasm-bindgen version is
/// `Cargo.lock`'s.
pub struct ContainerStages<'a> {
    /// The pinned toolchain.
    pub tc: &'a CargoToolchain,
    /// The bound host paths.
    pub paths: &'a HostPaths,
    /// The engine.
    pub runner: &'a dyn CargoContainerRunner,
    /// The row whose stages these are — its features reach the manifest example
    /// (edda's `WasmStageRun::ComponentManifest` carries none; MEASURED oden
    /// 2026-09-29: a feature-gated group got the default build's manifest).
    pub row: &'a BuildVariant,
}

impl ContainerStages<'_> {
    /// The container path a host path under the repo or the target dir lands at.
    fn inside(&self, repo_root: &Path, p: &Path) -> Result<String> {
        if let Ok(rel) = p.strip_prefix(&self.paths.target_dir) {
            return Ok(format!("{}/{}", cc::TARGETDIR, rel.display()));
        }
        if let Ok(rel) = p.strip_prefix(repo_root) {
            return Ok(format!("{}/{}", self.tc.container_repo_dir(repo_root), rel.display()));
        }
        Err(anyhow!(
            "{} is neither under the repo nor under the bound target dir {} — the container \
             cannot see it",
            p.display(),
            self.paths.target_dir.display()
        ))
    }

    /// The image's own toolchain, the bound target dir, the read-only registry, and
    /// the cpu bound as cargo's job count.
    fn prologue(&self) -> String {
        let mut s = format!(
            "export CARGO_HOME={}\nexport CARGO_TARGET_DIR={}\nexport CARGO_TERM_COLOR=never\n\
             export CARGO_BUILD_JOBS={}\n",
            cc::CARGO_HOME,
            cc::TARGETDIR,
            self.tc.cpus
        );
        if !self.tc.rust_version.is_empty() {
            s.push_str(&format!("export RUSTUP_TOOLCHAIN={}\n", q(&self.tc.rust_version)));
        }
        s
    }

    fn run_script(&self, label: &str, script: String, repo_root: &Path) -> Result<()> {
        let req = CargoRunRequest {
            name: format!("{}-wasm-stage-{label}", self.row.name),
            image: self.tc.image_ref(),
            script,
            binds: self.tc.binds(repo_root, &self.paths.target_dir, self.paths.registry_bind()),
            timeout: std::time::Duration::from_secs(self.tc.timeout_secs),
            network: self.tc.network,
            mem_limit_mb: self.tc.mem_limit_mb,
            cpus: self.tc.cpus,
        };
        let out = self.runner.run(&req)?;
        if out.exit_code != Some(0) {
            let all: Vec<&str> = out.stderr.iter().chain(out.stdout.iter()).map(String::as_str).collect();
            let tail = &all[all.len().saturating_sub(30)..];
            return Err(anyhow!(
                "wasm stage `{label}` exited {:?} in {}:\n{}",
                out.exit_code,
                self.tc.image_ref(),
                tail.join("\n")
            ));
        }
        Ok(())
    }
}

/// POSIX single-quoting: every value reaching the script is typed, and quoted anyway.
fn q(s: &str) -> String {
    format!("'{}'", s.replace('\'', r"'\''"))
}

impl WasmStageRunner for ContainerStages<'_> {
    fn describe(&self) -> String {
        format!(
            "wasm-bindgen (Cargo.lock's version, built offline in the image) + the manifest \
             example, IN {}",
            self.tc.image_ref()
        )
    }

    fn run(&self, run: &WasmStageRun, repo_root: &Path) -> Result<()> {
        match run {
            WasmStageRun::Bindgen { input, out_dir, target, typescript } => {
                let version = nornir_xtask_support::locked_wasm_bindgen_version(repo_root)
                    .map_err(|e| anyhow!("the wasm-bindgen version Cargo.lock pins: {e}"))?;
                let tool = format!("{}/wasm-bindgen-cli-{version}", cc::TARGETDIR);
                let scratch = format!("{tool}-home");
                let registry = format!("{}/registry", cc::CARGO_HOME);
                let mut bindgen = format!(
                    "{}/bin/wasm-bindgen {} --out-dir {} --target {}",
                    q(&tool),
                    q(&self.inside(repo_root, input)?),
                    q(&self.inside(repo_root, out_dir)?),
                    q(target)
                );
                if !typescript {
                    bindgen.push_str(" --no-typescript");
                }
                let script = format!(
                    "{prologue}\
                     if [ ! -x {tool}/bin/wasm-bindgen ]; then\n\
                     rm -rf {scratch}; mkdir -p {scratch}/registry/src\n\
                     ln -s {registry}/index {scratch}/registry/index\n\
                     ln -s {registry}/cache {scratch}/registry/cache\n\
                     CARGO_HOME={scratch} CARGO_TARGET_DIR={scratch}/target cargo install --offline \
                     --locked wasm-bindgen-cli --version {v} --root {tool}\n\
                     rm -rf {scratch}\n\
                     fi\n\
                     {tool}/bin/wasm-bindgen --version\n\
                     {bindgen}\n",
                    prologue = self.prologue(),
                    tool = q(&tool),
                    scratch = q(&scratch),
                    registry = q(&registry),
                    v = q(&version),
                );
                self.run_script("bindgen", script, repo_root)
            }
            WasmStageRun::ComponentManifest { package, example, out_file } => {
                let out = self.inside(repo_root, out_file)?;
                let feats: String = row_feature_args(self.row).iter().map(|a| format!(" {}", q(a))).collect();
                let script = format!(
                    "{prologue}export RUSTFLAGS={flags}\ncd {work}\n\
                     cargo run --locked {offline}--quiet --example {example} -p {package}{feats} > {out}.part\n\
                     mv {out}.part {out}\n",
                    prologue = self.prologue(),
                    flags = q(&self.tc.container_rustflags()),
                    work = q(&self.tc.container_repo_dir(repo_root)),
                    offline = if self.tc.network { "" } else { "--offline " },
                    example = q(example),
                    package = q(package),
                    out = q(&out),
                );
                self.run_script("component-manifest", script, repo_root)?;
                // An empty capture is a loader that enumerates no components while
                // every check passes.
                match std::fs::metadata(out_file) {
                    Ok(m) if m.len() > 0 => Ok(()),
                    _ => Err(anyhow!(
                        "the `{example}` example of `{package}` exited 0 in {} and {} is empty or \
                         absent — there is no manifest to ship",
                        self.tc.image_ref(),
                        out_file.display()
                    )),
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn feature_args_render_the_rows_two_fields() {
        let mut v = BuildVariant { name: "r".into(), ..Default::default() };
        assert!(row_feature_args(&v).is_empty());
        v.no_default_features = true;
        v.features = vec!["a".into(), "b".into()];
        assert_eq!(row_feature_args(&v), ["--no-default-features", "--features", "a,b"]);
    }

    #[test]
    fn a_non_cargo_row_is_refused_by_name_before_any_engine_is_asked() {
        use crate::variant::ArtifactKind;
        let v = BuildVariant { name: "img".into(), kind: ArtifactKind::Container, ..Default::default() };
        let set = BuildVariantSet::default();
        // No engine, no toolchain: the refusal must come first.
        assert!(refusal(&v).unwrap().contains("'img' is kind 'container'"));
        assert!(refusal(&BuildVariant { name: "x".into(), ..Default::default() }).is_none());
        assert!(build_wasm_row(&set, "absent", Path::new(".")).is_err());
    }}