nordisk/edda
crates/nornir-build-thing/src/cargo_container.rs
//! **The cargo kind builds in a container** — the leg it did not have.
//!
//! # The defect this module closes
//!
//! [`build_variant`](crate::provenance::build_variant) — "the one cargo-driving
//! loop in this crate" — is `Command::new("cargo")` in the host's environment
//! (`provenance.rs:473`). Everything the compile needs therefore comes from
//! whichever box the forge happens to run on: the rustc version, the installed
//! target set, the C compiler, `~/.cargo/config.toml`'s linker choice, the
//! registry cache. MEASURED 2026-08-25 on oden:
//!
//! ```text
//! $ dwarves buildthing gunnar --variant gunnar-server
//! error[E0463]: can't find crate for `core`
//! FAILED in 1.8 s
//! ```
//!
//! …because every gunnar variant targets `x86_64-unknown-linux-musl` and oden's
//! rustup had no musl std. Reproduced on t14s by naming a toolchain that lacks
//! the target (`RUSTUP_TOOLCHAIN=1.96.0 cargo build --target
//! x86_64-unknown-linux-musl` → `error[E0463]: can't find crate for std`): the
//! build's verdict was decided by the box, not by the declaration.
//!
//! The law was already written, in the sibling kind's image
//! (`containerfiles/kernel-toolchain.Containerfile:3-6`):
//!
//! > The host contributes a container engine socket and a workdir. NOTHING
//! > else… A build that only works on one box is a bug, not a build.
//!
//! The kernel kind obeys it ([`KernelContainerRunner`](crate::kernel::KernelContainerRunner)).
//! The cargo kind — the kind that builds nearly everything in the constellation
//! — had no image and no seam. This module is that seam, deliberately shaped
//! like the kernel one so there is one pattern and not two.
//!
//! # Declaration
//!
//! One table per repo, in `.nornir/build-variants.toml`. It is repo-level and
//! not per-variant on purpose: "which box can build this" is a property of the
//! repo's whole delivery, and a per-variant spelling would let one variant
//! quietly keep the host habit.
//!
//! ```toml
//! [build.toolchain]
//! # the image the cargo variants compile in
//! image = "nordisk-cargo-toolchain"
//! # PIN IT BY DIGEST — the MANIFEST digest (`podman image inspect --format
//! # '{{.Digest}}'`), not the image id. A named tag is a convenience, not a pin.
//! image_sha256 = "0d8caf1a…"
//! rust_version = "1.97.1" # what the image was built with
//! targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
//! # the bounds — a build container is a GUEST, never a host process:
//! mem_limit_mb = 8192 # draupnir --memory; 0 is REFUSED
//! cpus = 8 # draupnir --cpus
//! timeout_secs = 3600 # overrun is a loud refusal, not a wait
//! network = false # self-contained image ⇒ --network none
//! # the SIBLING REPOS this repo's `path = "../../.."` deps reach into. gunnar
//! # has ten such deps; a container holding only this repo cannot see one of
//! # them. An escaping dep whose sibling is missing here is refused BY NAME,
//! # before the container starts, with the manifest line and the fix spelled:
//! siblings = ["edda", "znippy", "draupnir", "tunnr", "znippy-zoomies"]
//! ```
//!
//! # `--network none` and where the crates come from
//!
//! A self-contained image plus `network = false` means the compile cannot reach
//! crates.io, so the run adds `--offline --locked`. The dependency sources are
//! bind-mounted from a registry the HOST populated (`cargo fetch --locked`,
//! host-side, the same division of labour the kernel kind uses for its tarball:
//! fetch on the host in Rust, compile in the container). That is not the host
//! leaking back in — every one of those sources is checksum-pinned by
//! `Cargo.lock`, exactly as the kernel tarball is pinned by `tarball_sha256`.
//! `--locked` is what makes the pin load-bearing rather than decorative.
//!
//! # Byte-identity, and what is honestly reachable
//!
//! The gate this module exists to satisfy: *the same variant built on the host
//! and in the container must produce a byte-identical artifact.*
//!
//! **THREE inputs stand between a naive run and that gate, and every one of them
//! is a PATH, not code.** rustc bakes absolute directories into debug info and
//! panic locations, and the three that differ are:
//!
//! | input | host | container | remapped to |
//! |---|---|---|---|
//! | the checkout | `/home/<user>/git` | [`WORKROOT`] | [`REMAP_SRC`] |
//! | the registry | `<CARGO_HOME>/registry` | `/cargo/registry` | [`REMAP_REGISTRY`] |
//! | **the target dir** | `<CARGO_TARGET_DIR>` | [`TARGETDIR`] | [`REMAP_TARGET`] |
//!
//! The third is the one that is easy to miss, because it is not a source
//! directory: `OUT_DIR` lives under the target dir, so every line of
//! build-script-GENERATED code carries it. It cost a measurement to find —
//! `gunnar-server` with the first two remaps in place still differed by 6 808
//! bytes, and the entire difference was one generated file's path repeated
//! through DWARF.
//!
//! [`CargoToolchain::remap_paths`] (default **on**) gives both sides the same
//! synthetic prefixes; [`CargoToolchain::host_rustflags`] is the host half, so
//! the comparison is apples-to-apples instead of a weakened gate. See
//! [`byte_identity_verdict`] for the verdict type the gate reports, which names
//! the reason when identity is NOT reached rather than going quietly green.
//!
//! # …and why HOST-vs-container is the wrong gate for a real repo
//!
//! With all three paths remapped, a dependency-free crate goes byte-identical
//! (`d4da6c40…` / `7b85469a…`, measured). `gunnar-server` does not, and reading
//! the two artifacts turns up **two causes, neither of them the container's**:
//!
//! ```text
//! host: GCC: (Ubuntu 15.2.0-16ubuntu1) 15.2.0 built 2026-08-25T22:43:39Z
//! container: GCC: (Debian 14.2.0-19) 14.2.0 built 2026-08-25T22:44:47Z
//! ```
//!
//! 1. **The C compilers are different**, and that is the POINT rather than a
//! problem. gunnar links C (mimalloc, musl objects), the host's `cc` is
//! whatever Ubuntu shipped, and the container's is the one the image pins.
//! Demanding byte-identity with the host means demanding the container
//! reproduce an environment nobody declared — it makes the UNPINNED side the
//! reference. It is also direct evidence of the defect: the host WAS
//! contributing a C toolchain to the artifact.
//! 2. **The repo stamps the wall clock into every build.**
//! `gunnar/crates/gunnar-common/build.rs:43` runs `date -u +%Y-%m-%dT%H:%M:%SZ`
//! and bakes it as `GUNNAR_BUILD_DATE`. At second resolution, two builds one
//! second apart differ — on one box or two.
//!
//! So the gate that means something is **container-vs-container**: the same
//! digest-pinned image, twice, must produce the same bytes. That is what a
//! content-addressed build environment actually promises, and unlike host-vs-
//! container it does not treat one box's accidents as the specification.
//! [`byte_identity_with_control`] is that gate — and it still goes RED on
//! `gunnar-server`, for cause (2), which is a repo defect the container cannot
//! and should not paper over.
use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::provenance::{BuildOutcome, BuildProvenance, HostInfo, Outcome, SourceRev, Toolchain};
use crate::variant::{BuildVariant, BuildVariantSet};
/// The image name the committed [`CARGO_TOOLCHAIN_CONTAINERFILE`] builds under.
/// A NAME, not a pin — see [`CargoToolchain::image_ref`].
pub const CARGO_TOOLCHAIN_IMAGE: &str = "nordisk-cargo-toolchain";
/// **The committed Containerfile for [`CARGO_TOOLCHAIN_IMAGE`]** — one
/// authority, embedded in the crate so no repo can ship a private twin of the
/// build environment. Every package in it names the STEP it serves.
pub const CARGO_TOOLCHAIN_CONTAINERFILE: &str =
include_str!("containerfiles/cargo-toolchain.Containerfile");
/// The image name the committed [`CARGO_TEST_CONTAINERFILE`] builds under.
/// A NAME, not a pin — the same caveat [`CARGO_TOOLCHAIN_IMAGE`] carries.
pub const CARGO_TEST_IMAGE: &str = "nordisk-cargo-test";
/// **The committed Containerfile for [`CARGO_TEST_IMAGE`]** — the sibling of
/// [`CARGO_TOOLCHAIN_CONTAINERFILE`] for the road that RUNS a repo's tests
/// rather than compiling its variants.
///
/// It is a second image and not three more packages in the first one because
/// that file states the rule it would be breaking: nothing rides "just in
/// case", and an appliance build has no business carrying an ssh client. What
/// it carries instead is exactly what a test suite needs and a compile does
/// not — `openssh-client`, `git-lfs`, `cargo-deny` — each naming the tests it
/// unblocks.
///
/// MEASURED on oden 2026-09-12, gunnar's `cargo test --workspace`: 2064
/// passed / 13 failed as root in the plain toolchain image, 1972 / 110 as an
/// ordinary user in it (no ssh at all), and 2075 / 7 as an ordinary user in
/// this one. None of the differences were gunnar's code.
pub const CARGO_TEST_CONTAINERFILE: &str =
include_str!("containerfiles/cargo-test.Containerfile");
/// **The `cargo-deny` release [`CARGO_TEST_CONTAINERFILE`] installs**, read out
/// of its own `ARG CARGO_DENY_VERSION` / `ARG CARGO_DENY_SHA256` lines:
/// `(version, sha256)`.
///
/// Read rather than restated, for the reason
/// [`cargo_toolchain_image_targets`] is read: the Containerfile is what puts
/// the binary in the image, so it is the only honest source for which bytes.
/// The host-side stager ([`stage_cargo_deny`]) fetches and verifies against
/// this, and the image verifies the staged file again in the layer that
/// installs it — the same digest checked on both sides of the context
/// boundary, so a tarball swapped in between is refused twice.
///
/// # Errors
///
/// Either line missing. Refused rather than defaulted: a default would mean
/// fetching an unpinned binary into an image everything else trusts.
pub fn cargo_test_cargo_deny_pin() -> Result<(String, String)> {
let arg = |name: &str| -> Option<String> {
CARGO_TEST_CONTAINERFILE
.lines()
.filter_map(|l| l.trim().strip_prefix(&format!("ARG {name}=")))
.map(|v| v.trim().trim_matches('"').to_string())
.next()
};
let version = arg("CARGO_DENY_VERSION").ok_or_else(|| {
anyhow!("cargo-test.Containerfile has no `ARG CARGO_DENY_VERSION=` line to pin by")
})?;
let sha256 = arg("CARGO_DENY_SHA256").ok_or_else(|| {
anyhow!("cargo-test.Containerfile has no `ARG CARGO_DENY_SHA256=` line to verify by")
})?;
Ok((version, sha256))
}
/// The `ARG` line [`cargo_test_containerfile_on`] rewrites. Spelled once.
const CARGO_TEST_BASE_ARG: &str = "ARG CARGO_TOOLCHAIN_REF=";
/// **[`CARGO_TEST_CONTAINERFILE`] with its base pinned to `toolchain_ref`.**
///
/// The base is expressed as an `ARG` with a tag default so a human can build
/// the image by hand, and the builder replaces that default with the reference
/// it was handed — normally `nordisk-cargo-toolchain@sha256:…`. Rewritten
/// rather than passed as `--build-arg` because the one container seam this
/// constellation has ([`draupnir::container::ContainerBoot`]) does not carry
/// build args, and teaching it one would be a second repo, a second publish
/// and a second thing to keep in step for a single string.
///
/// Exactly one line changes, and a missing line is a REFUSAL: silently
/// building on the tag default would produce an image whose base nobody can
/// name, which is the whole failure a pin exists to prevent.
///
/// # Errors
///
/// The embedded Containerfile has no `ARG CARGO_TOOLCHAIN_REF=` line.
pub fn cargo_test_containerfile_on(toolchain_ref: &str) -> Result<String> {
let mut found = false;
let out: Vec<String> = CARGO_TEST_CONTAINERFILE
.lines()
.map(|l| {
if l.starts_with(CARGO_TEST_BASE_ARG) && !found {
found = true;
format!("{CARGO_TEST_BASE_ARG}{toolchain_ref}")
} else {
l.to_string()
}
})
.collect();
if !found {
return Err(anyhow!(
"cargo-test.Containerfile has no `{CARGO_TEST_BASE_ARG}` line, so the base image \
cannot be pinned. Refusing to build on whatever the file's FROM resolves to."
));
}
Ok(format!("{}\n", out.join("\n")))
}
/// The URL the pinned `cargo-deny` tarball is fetched from. Derived from the
/// version in the Containerfile, so there is one place to change.
pub fn cargo_deny_url(version: &str) -> String {
format!(
"https://github.com/EmbarkStudios/cargo-deny/releases/download/{version}/\
cargo-deny-{version}-x86_64-unknown-linux-musl.tar.gz"
)
}
/// **Fetch the pinned `cargo-deny` tarball into `context_dir`, verified.**
///
/// Host-side and in pure Rust, the road
/// `cargo-toolchain.Containerfile`'s honest-gap section names for a tool
/// Debian does not package, and the one `kernel.rs` already takes for the
/// kernel source tarball. The file lands as `cargo-deny.tar.gz`, which is the
/// name the Containerfile `COPY`s.
///
/// An existing file that already matches the pin is left alone — a mint should
/// not re-download 4.9 MB it can prove it has. One that does NOT match is
/// removed and refetched, because a stale context file is exactly how an
/// unpinned binary gets into a pinned image.
///
/// # Errors
///
/// The pin cannot be read, the download fails, or the bytes do not match the
/// pin. The mismatch names both digests; nothing is left in the context.
pub fn stage_cargo_deny(fetcher: &dyn crate::kernel::TarballFetcher, context_dir: &Path) -> Result<PathBuf> {
let (version, want) = cargo_test_cargo_deny_pin()?;
let dest = context_dir.join("cargo-deny.tar.gz");
if dest.is_file() && crate::kernel::sha256_file_hex(&dest)? == want {
return Ok(dest);
}
let _ = std::fs::remove_file(&dest);
let url = cargo_deny_url(&version);
fetcher
.fetch(&url, &dest)
.with_context(|| format!("fetch cargo-deny {version}"))?;
let got = crate::kernel::sha256_file_hex(&dest)?;
if got != want {
let _ = std::fs::remove_file(&dest);
return Err(anyhow!(
"cargo-deny {version} does not match its pin: the tarball at {url} hashes to \
{got}, and cargo-test.Containerfile pins {want}. Nothing was staged. Either the \
release was replaced — which is a supply-chain event, not a version bump — or \
`ARG CARGO_DENY_SHA256` is stale and must be updated deliberately."
));
}
Ok(dest)
}
/// **The instant [`CARGO_TOOLCHAIN_CONTAINERFILE`] is minted at**, read out of
/// its own `ENV SOURCE_DATE_EPOCH` line — the one writer of that number.
///
/// It must reach two places: inside the image (`SOURCE_DATE_EPOCH`, honoured by
/// everything that would otherwise read the wall clock) and the BUILDER
/// (podman's `timestamp`, which sets the image config's `created` and every
/// layer's file timestamps — nothing in Dockerfile syntax can reach those).
/// [`DraupnirCargoRunner::build_toolchain_image`] hands it over via
/// `build_image_reproducible`. A missing line is REFUSED, not defaulted: a
/// default would silently mint an image whose id nobody else can reproduce.
///
/// The parse itself is [`crate::kernel::source_date_epoch_of`] — one parser for
/// both toolchain Containerfiles, so they cannot disagree on what the line means.
pub fn cargo_toolchain_source_date_epoch() -> Result<i64> {
crate::kernel::source_date_epoch_of(CARGO_TOOLCHAIN_CONTAINERFILE)
}
/// **The target triples [`CARGO_TOOLCHAIN_CONTAINERFILE`] actually installs std
/// for**, read out of its own `ARG RUST_TARGETS` default.
///
/// Read rather than restated, because a second list would be a twin that drifts:
/// the Containerfile is the only thing that can put a std inside the image, so it
/// is the only honest source for what the image carries.
///
/// This is the half [`CargoToolchain::check_target`] cannot see. That check
/// compares a variant's `--target` against the DECLARATION's `targets` list, so
/// it catches a declaration that is too small — and passes a declaration that is
/// too big. MEASURED on oden 2026-08-27: the image built from this file was
/// `sha256:035814c4…` and had no wasm32 std whatsoever, yet
/// `rustc --print target-list` inside it printed `wasm32-unknown-unknown` among
/// 322 triples. Anyone adding that string to `targets` to "declare it" would
/// have turned a NAMED refusal an hour early into a bare `E0463` an hour late.
/// [`CargoToolchain::check_declared_targets`] is what refuses that.
pub fn cargo_toolchain_image_targets() -> Vec<String> {
CARGO_TOOLCHAIN_CONTAINERFILE
.lines()
.filter_map(|l| l.trim().strip_prefix("ARG RUST_TARGETS="))
.flat_map(|v| {
v.trim()
.trim_matches('"')
.split_whitespace()
.map(str::to_string)
.collect::<Vec<_>>()
})
.collect()
}
/// The default build bound: 1 hour. A cargo build that has produced nothing in
/// an hour on this constellation's hardware is a hang, and a hang is a refusal,
/// never a wait. (The kernel kind's two hours is a KERNEL compile; a workspace
/// that legitimately needs longer raises its own `timeout_secs`.)
pub const DEFAULT_TIMEOUT_SECS: u64 = 60 * 60;
/// The default memory bound in MiB. Bounded by default for the reason recorded
/// in [`crate::kernel`]'s bounded-guest law: 2026-08-20 an unbounded build
/// reached 33 GB, the OOM killer took the service, and the box went down.
pub const DEFAULT_MEM_LIMIT_MB: u32 = 8192;
/// The default cpu bound in whole cores.
pub const DEFAULT_CPUS: u32 = 8;
/// **The container path the repo's PARENT is reproduced at**, not the repo.
///
/// The repo lands at `<WORKROOT>/<repo dir name>` and each declared sibling at
/// `<WORKROOT>/<sibling dir name>`, so the constellation's checkout layout —
/// sibling repos under one parent — survives into the container unchanged.
///
/// MEASURED 2026-08-26, and it is why this is not simply `/work`: binding the
/// repo alone at `/work` builds a leaf crate and cannot build gunnar.
///
/// ```text
/// error: failed to load manifest for workspace member `/work/crates/gunnar-store`
/// Caused by: failed to read `/znippy/git-storage-trait/Cargo.toml`
/// ```
///
/// `crates/gunnar-store/Cargo.toml:33` declares
/// `path = "../../../znippy/git-storage-trait"` — one of 10 such deps reaching
/// into `edda` and `znippy`. From `/work/crates/gunnar-store` those three `..`
/// walk clean out of the mount and land at `/`. From
/// `/work/gunnar/crates/gunnar-store` they land at `/work/znippy/…`, which is
/// where the sibling bind is. The depth is the fix.
pub const WORKROOT: &str = "/work";
/// The container path `CARGO_TARGET_DIR` points at.
pub const TARGETDIR: &str = "/target";
/// The container `CARGO_HOME`.
pub const CARGO_HOME: &str = "/cargo";
/// The synthetic prefix BOTH sides remap [`WORKROOT`] / the host repo's PARENT
/// to, so the source path baked into debug info is a property of the BUILD and
/// not of the checkout. It is the parent and not the repo because a sibling
/// repo's sources get baked in too, and remapping only the repo would leave
/// those pointing at two different places.
pub const REMAP_SRC: &str = "/nordisk/src";
/// The synthetic prefix BOTH sides remap the cargo registry to.
pub const REMAP_REGISTRY: &str = "/nordisk/registry";
/// The synthetic prefix BOTH sides remap the TARGET DIR to.
///
/// The third path input, and the one that is easy to miss because it is not a
/// source directory: `OUT_DIR` lives under the target dir, so every line of
/// build-script-GENERATED code carries that path. MEASURED 2026-08-26 —
/// gunnar-server, with the repo and registry already remapped, still differed by
/// 6 808 bytes, and the whole of it was one string:
///
/// ```text
/// host: ~/scratch/…/gunnar-htarget/…/build/gunnar-proto-ec9b63…/out/gunnar.v1.rs
/// container: /target/…/build/gunnar-proto-ec9b63…/out/gunnar.v1.rs
/// ```
///
/// 31 characters of difference, ~220 times over in DWARF. It was the ONLY
/// host-specific absolute path left in the binary.
pub const REMAP_TARGET: &str = "/nordisk/target";
fn default_image() -> String {
CARGO_TOOLCHAIN_IMAGE.to_string()
}
fn default_timeout_secs() -> u64 {
DEFAULT_TIMEOUT_SECS
}
fn default_mem_limit_mb() -> u32 {
DEFAULT_MEM_LIMIT_MB
}
fn default_cpus() -> u32 {
DEFAULT_CPUS
}
fn default_remap_paths() -> bool {
true
}
fn is_true(b: &bool) -> bool {
*b
}
fn is_false(b: &bool) -> bool {
!*b
}
/// **The `[build.toolchain]` declaration** — what image the repo's cargo
/// variants compile in, and the bounds the container runs under.
///
/// Absent ⇒ the repo still builds on the host, unchanged. Present ⇒ every cargo
/// variant in the repo goes through the container, because "some variants are
/// hermetic" is not a property anyone can audit.
/// **Closed, like the two structs beside it.** This table's whole job is to make
/// the build environment auditable, and a key it silently drops is the one thing
/// that would quietly un-audit it — a `siblings` typo'd to `sibling` would mount
/// nothing and fail an hour later inside cargo, and a misspelled `image_sha256`
/// would read as "unpinned" and be REFUSED for the wrong reason. MEASURED
/// 2026-08-26: gunnar is the only repo declaring `[build.toolchain]` and has zero
/// unknown keys, so this costs nothing today.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CargoToolchain {
/// The toolchain image name (default [`CARGO_TOOLCHAIN_IMAGE`]).
#[serde(default = "default_image")]
pub image: String,
/// **The digest pin.** The MANIFEST digest, 64 lowercase hex, no `sha256:`
/// prefix. Empty is accepted by the parser and REFUSED by
/// [`validate`](Self::validate) — a build environment that can drift under
/// you is not an environment, it is a weather report.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub image_sha256: String,
/// The rustc version the image was built with. Recorded so a provenance
/// reader can tell two images apart, and compared against what the
/// container actually reports (a mismatch is a NAMED refusal, not a shrug).
#[serde(default, skip_serializing_if = "String::is_empty")]
pub rust_version: String,
/// The target triples the image carries std for. Declared so a variant
/// naming a target the image cannot build is refused BY NAME at validation
/// instead of one hour later with `E0463`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub targets: Vec<String>,
/// The build bound in seconds ([`DEFAULT_TIMEOUT_SECS`]).
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
/// The memory bound in MiB ([`DEFAULT_MEM_LIMIT_MB`]). `0` is refused.
#[serde(default = "default_mem_limit_mb")]
pub mem_limit_mb: u32,
/// The cpu bound in whole cores ([`DEFAULT_CPUS`]). `0` is refused.
#[serde(default = "default_cpus")]
pub cpus: u32,
/// Whether the compile container gets a network. `false` (the default) runs
/// `--network none` and adds `--offline --locked`.
#[serde(default, skip_serializing_if = "is_false")]
pub network: bool,
/// Remap the repo root and the registry to [`REMAP_SRC`] / [`REMAP_REGISTRY`]
/// on BOTH sides (default **on**) — the two path inputs that otherwise make
/// a host build and a container build differ in bytes while being identical
/// in meaning.
#[serde(default = "default_remap_paths", skip_serializing_if = "is_true")]
pub remap_paths: bool,
/// **The SIBLING REPOS this repo's `path = "../../.."` dependencies reach
/// into**, by directory name under the repo's own parent — e.g.
/// `siblings = ["edda", "znippy"]`.
///
/// This constellation does not have leaf repos. gunnar declares 10 path
/// dependencies that walk out of its own tree into `edda` and `znippy`
/// (`crates/gunnar-store/Cargo.toml:33`, `crates/gunnar-server/Cargo.toml:171`,
/// …), and a container holding only the repo cannot see any of them:
///
/// ```text
/// error: failed to load manifest for workspace member `/work/crates/gunnar-store`
/// Caused by: failed to read `/znippy/git-storage-trait/Cargo.toml`
/// ```
///
/// Declared, not discovered, and that is the point: each name here is one
/// more tree whose contents end up inside the artifact, so it belongs in the
/// declaration where a reader can see it. A path dep escaping into a sibling
/// that is NOT listed is a refusal BY NAME
/// ([`check_escaping_path_deps`](Self::check_escaping_path_deps)) — the
/// cargo error above, named before the container starts and with the
/// manifest line that caused it.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub siblings: Vec<String>,
}
impl Default for CargoToolchain {
fn default() -> Self {
CargoToolchain {
image: default_image(),
image_sha256: String::new(),
rust_version: String::new(),
targets: Vec::new(),
timeout_secs: default_timeout_secs(),
mem_limit_mb: default_mem_limit_mb(),
cpus: default_cpus(),
network: false,
remap_paths: default_remap_paths(),
siblings: Vec::new(),
}
}
}
impl CargoToolchain {
/// The effective image reference — digest-pinned when [`image_sha256`] is
/// declared. Same spelling the kernel kind resolved to after the 2026-08-23
/// correction: `<name>@sha256:<MANIFEST digest>` is addressable locally AND
/// in a registry; an image id pasted there triggers a registry pull and
/// fails.
///
/// [`image_sha256`]: Self::image_sha256
pub fn image_ref(&self) -> String {
if self.image_sha256.is_empty() {
return self.image.clone();
}
if self.image.is_empty() {
return self.image_sha256.clone();
}
format!("{}@sha256:{}", self.image, self.image_sha256)
}
/// **Refuse an unauditable build environment BY NAME.** Every arm here is a
/// thing that would otherwise surface as a confusing failure an hour into a
/// compile, or — worse — as a green whose meaning nobody can reconstruct.
pub fn validate(&self) -> Result<()> {
if self.image.trim().is_empty() && self.image_sha256.trim().is_empty() {
return Err(anyhow!(
"[build.toolchain]: neither `image` nor `image_sha256` is declared — \
there is nothing for a container engine to run"
));
}
if self.image_sha256.is_empty() {
return Err(anyhow!(
"[build.toolchain] `{}`: no `image_sha256` — a named tag is a convenience, \
NOT a pin, and an unpinned build image can be replaced under you between \
two builds that report the same provenance",
self.image
));
}
if !is_manifest_digest(&self.image_sha256) {
return Err(anyhow!(
"[build.toolchain] `{}`: `image_sha256` must be 64 lowercase hex characters \
(the MANIFEST digest, `podman image inspect --format '{{{{.Digest}}}}'`, \
with no `sha256:` prefix) — got `{}`",
self.image,
self.image_sha256
));
}
if self.mem_limit_mb == 0 {
return Err(anyhow!(
"[build.toolchain] `{}`: `mem_limit_mb = 0` — unbounded is not a bound. \
An unbounded build reached 33 GB on this constellation (2026-08-20), the \
OOM killer took the service, and the box went down",
self.image
));
}
if self.cpus == 0 {
return Err(anyhow!(
"[build.toolchain] `{}`: `cpus = 0` — unbounded is not a bound",
self.image
));
}
if self.timeout_secs == 0 {
return Err(anyhow!(
"[build.toolchain] `{}`: `timeout_secs = 0` — a build with no bound is a \
hang waiting to happen, and a hang is worse than a refusal",
self.image
));
}
// The declaration is checked against the image's own recipe HERE, in the
// one place every loader already goes through — a separate door nobody
// walks through is how this crate's toolchain images spent a month
// unbuildable.
self.check_declared_targets()?;
Ok(())
}
/// Refuse a variant whose `target` the image is not declared to carry —
/// **at validation, not at `E0463`**. An empty [`targets`](Self::targets)
/// is "undeclared", not "none": it accepts anything, exactly the way an
/// empty `offers` does.
pub fn check_target(&self, variant: &str, target: Option<&str>) -> Result<()> {
let (Some(t), false) = (target, self.targets.is_empty()) else {
return Ok(());
};
if self.targets.iter().any(|d| d == t) {
return Ok(());
}
Err(anyhow!(
"variant `{variant}` targets `{t}`, which the toolchain image `{}` does not \
carry std for (it declares: {}). This is the `error[E0463]: can't find crate \
for core` failure, named an hour earlier",
self.image,
self.targets.join(", ")
))
}
/// **Refuse a `targets` list the image cannot back.** The other direction of
/// [`check_target`](Self::check_target), and the one that turns a named
/// refusal back into a bare `E0463`.
///
/// `check_target` trusts the declaration: a variant whose target is listed
/// passes, and the compile then discovers inside the container that the std
/// is not there. So the declaration is checked against the ONLY thing that
/// can put a std in the image — its Containerfile's own `ARG RUST_TARGETS`
/// ([`cargo_toolchain_image_targets`]).
///
/// Skipped when the declared `image` is not [`CARGO_TOOLCHAIN_IMAGE`]: a repo
/// naming some other image is naming an environment this crate does not
/// build, and guessing at its contents would be worse than saying nothing.
/// An empty `targets` is "undeclared" and accepts anything, same as
/// `check_target`.
pub fn check_declared_targets(&self) -> Result<()> {
if self.targets.is_empty() || !self.image.contains(CARGO_TOOLCHAIN_IMAGE) {
return Ok(());
}
let carried = cargo_toolchain_image_targets();
let missing: Vec<&str> = self
.targets
.iter()
.filter(|t| !carried.iter().any(|c| c == *t))
.map(String::as_str)
.collect();
if missing.is_empty() {
return Ok(());
}
Err(anyhow!(
"[build.toolchain] `{}`: declares `targets` the image does NOT carry std for \
({}). The committed cargo-toolchain.Containerfile installs exactly: {}. \
Declaring a triple does not install it — add it to `ARG RUST_TARGETS`, \
rebuild the image and re-pin `image_sha256`, or drop it from `targets`. \
Left as-is, `check_target` would wave the variant through and the compile \
would die inside the container on `error[E0463]: can't find crate for std`",
self.image,
missing.join(", "),
carried.join(", ")
))
}
/// The `RUSTFLAGS` the **container** side needs for a path-stable build.
/// Empty when [`remap_paths`](Self::remap_paths) is off.
pub fn container_rustflags(&self) -> String {
if !self.remap_paths {
return String::new();
}
format!(
"--remap-path-prefix={WORKROOT}={REMAP_SRC} \
--remap-path-prefix={CARGO_HOME}/registry={REMAP_REGISTRY} \
--remap-path-prefix={TARGETDIR}={REMAP_TARGET}"
)
}
/// The `RUSTFLAGS` the **host** side needs to be comparable with a container
/// build of the same variant — the other half of the byte-identity gate.
/// Without this the gate compares a binary that has `~/git/…`
/// baked into it against one that has `/work/…`, calls them different, and
/// blames the container for a difference the COMPARISON introduced.
///
/// Takes the repo's PARENT, matching [`WORKROOT`] — a sibling repo's sources
/// are baked in too, and remapping only the repo would leave those pointing
/// at two different places.
pub fn host_rustflags(
&self,
repo_root: &Path,
cargo_home: &Path,
target_dir: &Path,
) -> String {
if !self.remap_paths {
return String::new();
}
format!(
"--remap-path-prefix={}={REMAP_SRC} \
--remap-path-prefix={}/registry={REMAP_REGISTRY} \
--remap-path-prefix={}={REMAP_TARGET}",
checkout_parent(repo_root).display(),
cargo_home.display(),
target_dir.display()
)
}
/// The container path the repo itself lands at: `<WORKROOT>/<repo dir name>`.
/// The build's working directory.
pub fn container_repo_dir(&self, repo_root: &Path) -> String {
format!("{WORKROOT}/{}", repo_dir_name(repo_root))
}
/// The declared [`siblings`](Self::siblings) with NOTHING at their path beside
/// the repo, as `(name, host path)` — what a forge may clone. A path that exists
/// but is not a checkout is never in it: nothing is cloned over an existing dir.
pub fn absent_siblings(&self, repo_root: &Path) -> Vec<(String, PathBuf)> {
self.siblings
.iter()
.cloned()
.zip(self.sibling_paths(repo_root))
.filter(|(_, p)| std::fs::symlink_metadata(p).is_err())
.collect()
}
/// The host paths of the declared [`siblings`](Self::siblings), resolved
/// under the repo's parent.
pub fn sibling_paths(&self, repo_root: &Path) -> Vec<PathBuf> {
let parent = checkout_parent(repo_root);
self.siblings.iter().map(|s| parent.join(s)).collect()
}
/// The bind mounts the run needs: the repo at
/// [`container_repo_dir`](Self::container_repo_dir), each declared sibling
/// beside it under [`WORKROOT`], the target dir at [`TARGETDIR`], and the
/// host-fetched registry read-only under [`CARGO_HOME`].
///
/// A sibling is mounted **read-only**: this build compiles it, it does not
/// get to edit another repo's checkout.
pub fn binds(
&self,
repo_root: &Path,
target_dir: &Path,
registry: Option<&Path>,
) -> Vec<String> {
let mut b = vec![
format!("{}:{}", repo_root.display(), self.container_repo_dir(repo_root)),
format!("{}:{TARGETDIR}", target_dir.display()),
];
for (name, host) in self.siblings.iter().zip(self.sibling_paths(repo_root)) {
b.push(format!("{}:{WORKROOT}/{name}:ro", host.display()));
}
if let Some(r) = registry {
// Read-only: the container COMPILES the pinned sources, it does not
// get to edit the host's copy of them.
b.push(format!("{}:{CARGO_HOME}/registry:ro", r.display()));
}
b
}
/// **Refuse a path dependency that walks out of the repo into a sibling
/// nobody declared — BY NAME, before the container starts.**
///
/// Without this the failure is cargo's, an hour or a second later, and it
/// names the path it could not read rather than the manifest line that asked
/// for it or the `siblings` entry that would fix it:
///
/// ```text
/// Caused by: failed to read `/znippy/git-storage-trait/Cargo.toml`
/// ```
///
/// A dep reaching ABOVE the parent (`../../../../elsewhere`) is refused
/// too, and separately: a flat sibling mount cannot reproduce that layout,
/// so declaring it would not help and pretending otherwise would be worse
/// than the refusal.
pub fn check_escaping_path_deps(&self, repo_root: &Path) -> Result<()> {
self.check_escaping_path_deps_from(repo_root, repo_root)
}
/// **This declaration as `variant` builds under it.** A row that declares its
/// own [`siblings`](crate::variant::BuildVariant::siblings) gets exactly those
/// in place of the repo-wide list; a row that declares none gets the
/// declaration unchanged. Everything else — image, pin, bounds — stays the
/// toolchain's: a row chooses what it reaches, never what compiles it.
pub fn for_variant(&self, v: &BuildVariant) -> CargoToolchain {
if v.siblings.is_empty() {
return self.clone();
}
CargoToolchain { siblings: v.siblings.clone(), ..self.clone() }
}
/// [`check_escaping_path_deps`](Self::check_escaping_path_deps), scanning the
/// manifests under `scan_root` instead of the whole repo — the root a row's
/// cargo actually loads ([`variant_manifest_root`]). A row built with
/// `--manifest-path ../holger/Cargo.toml` never reads this repo's other
/// manifests, so their path deps must not demand checkouts that row does not
/// mount. The declared siblings still resolve beside `repo_root`, and a dep
/// back into the repo itself (mounted as the workdir) is not an escape.
pub fn check_escaping_path_deps_from(&self, repo_root: &Path, scan_root: &Path) -> Result<()> {
let parent = checkout_parent(repo_root);
let own = repo_dir_name(repo_root);
// A declared sibling that is not on this box would surface as a bind
// failure from the engine, naming a mount and not a declaration. Say it
// here, where the reader is already looking at the list.
let absent: Vec<String> = self
.siblings
.iter()
.zip(self.sibling_paths(repo_root))
.filter(|(_, p)| !p.is_dir())
.map(|(n, p)| format!("{n} ({})", p.display()))
.collect();
if !absent.is_empty() {
let missing: Vec<&str> = self
.siblings
.iter()
.zip(self.sibling_paths(repo_root))
.filter(|(_, p)| !p.is_dir())
.map(|(n, _)| n.as_str())
.collect();
return Err(anyhow!(
"[build.toolchain] `siblings` names {} repo(s) that are not checked out beside \
`{}` — the bind mount would fail, and the engine's error names a path rather \
than this declaration: {}. {}",
absent.len(),
repo_root.display(),
absent.join(", "),
clone_hint(&parent, &missing)
));
}
let mut missing: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut unreachable: Vec<String> = Vec::new();
// ★ The repo's OWN workspace either way: a directory below it whose `Cargo.toml`
// declares its own `[workspace]` is a DETACHED crate cargo does not load when it builds
// this repo's rows, so its path deps demand no checkout of theirs (5.3.2 receipt,
// 2026-10-09: monetize-impl's toolchain was refused "Add `siblings = [\"gunnar\"]`" for
// its detached `gunnar-front-commercial`, which only gunnar's own front rows build — and
// those rows are checked against THAT workspace, with their own siblings).
let escapes = workspace_escaping_path_deps(scan_root);
for (manifest, dep, resolved) in escapes {
let where_ = format!("{} → {dep}", manifest.display());
match resolved.strip_prefix(&parent).ok().and_then(|r| {
r.components().next().and_then(|c| c.as_os_str().to_str()).map(str::to_string)
}) {
Some(sib) if self.siblings.contains(&sib) || sib == own => {}
Some(sib) => missing.entry(sib).or_default().push(where_),
None => unreachable.push(where_),
}
}
if !unreachable.is_empty() {
return Err(anyhow!(
"{} path dependenc{} reach ABOVE the repo's parent ({}), which a flat \
sibling mount cannot reproduce — the container would have to carry an \
unbounded slice of this box's filesystem: {}",
unreachable.len(),
if unreachable.len() == 1 { "y" } else { "ies" },
parent.display(),
unreachable.join("; ")
));
}
if !missing.is_empty() {
let names: Vec<&str> = missing.keys().map(String::as_str).collect();
let detail: Vec<String> = missing
.iter()
.map(|(s, w)| format!("{s} ({} dep(s), e.g. {})", w.len(), w[0]))
.collect();
return Err(anyhow!(
"[build.toolchain]: path dependencies escape this repo into sibling \
repo(s) the declaration does not list — the build container would not \
have them and cargo would fail on `failed to read …/Cargo.toml`. Add \
`siblings = [{}]`. Escaping: {}",
names.iter().map(|n| format!("\"{n}\"")).collect::<Vec<_>>().join(", "),
detail.join("; ")
));
}
Ok(())
}
}
/// **Where an absent sibling is cloned FROM** — read off this box, never guessed.
///
/// 2026-10-16 (loki, ⚒ Build tunnr): the refusal named `tunnr-mullvad` and
/// `tunnr-tailscale` and the path they were missing from, and nothing about where
/// they live — and they do not live where tunnr does (nordisk/, not mikronova/), so
/// "the same forge as this repo" would have been wrong. What the box DOES know is
/// where every checkout beside it came from: each neighbour's `origin`. Those
/// bases, deduplicated, are the candidates, and the hint spells the clone line for
/// each. A box whose neighbours name no remote says so instead of inventing one.
fn clone_hint(parent: &Path, missing: &[&str]) -> String {
let bases = neighbour_bases(parent);
if bases.is_empty() {
return format!(
"No checkout beside it names an `origin`, so where to clone {} from cannot be read off \
this box.",
missing.join(", ")
);
}
let lines: Vec<String> = missing
.iter()
.flat_map(|n| bases.iter().map(move |b| format!("git clone {b}/{n} {}", parent.join(n).display())))
.collect();
format!(
"Clone it beside the others — the checkouts there come from {}; one of: {}",
bases.iter().cloned().collect::<Vec<_>>().join(", "),
lines.join(" | ")
)
}
/// A checkout's `origin` URL as its `.git/config` names it (no `.git`, no trailing
/// `/`); `None` for a dir that is not a checkout or names no `origin`.
fn origin_url(checkout: &Path) -> Option<String> {
let cfg = std::fs::read_to_string(checkout.join(".git").join("config")).ok()?;
let mut in_origin = false;
for line in cfg.lines().map(str::trim) {
if line.starts_with('[') {
in_origin = line == "[remote \"origin\"]";
} else if in_origin {
if let Some(url) = line.strip_prefix("url").map(str::trim_start).and_then(|l| l.strip_prefix('=')) {
return Some(url.trim().trim_end_matches('/').trim_end_matches(".git").to_string());
}
}
}
None
}
/// The base (`<host>/<namespace>`) an `origin` URL clones from: all but its last
/// segment.
fn base_of(url: &str) -> Option<&str> {
url.rsplit_once('/').map(|(b, _)| b)
}
/// Every base the checkouts under `parent` come from (each neighbour's `origin`).
fn neighbour_bases(parent: &Path) -> std::collections::BTreeSet<String> {
let mut bases = std::collections::BTreeSet::new();
if let Ok(rd) = std::fs::read_dir(parent) {
for e in rd.flatten() {
if let Some(b) = origin_url(&e.path()).as_deref().and_then(base_of) {
bases.insert(b.to_string());
}
}
}
bases
}
/// **Where an absent sibling `name` of `repo_root` is cloned FROM, best first** —
/// read off this box, never invented: the repo's own `origin` base (the same host
/// and namespace) first, then every other namespace the neighbouring checkouts come
/// from ON THAT HOST (a mikronova repo's sibling may live in nordisk: tunnr and
/// tunnr-mullvad, loki 2026-10-16). The caller tries them in order and the first
/// that clones is where it lives. A repo with no `origin` falls back to every base
/// its neighbours name; a box whose checkouts name no remote gives none.
pub fn sibling_clone_urls(repo_root: &Path, name: &str) -> Vec<String> {
let neighbours = neighbour_bases(&checkout_parent(repo_root));
let own = origin_url(repo_root).and_then(|u| base_of(&u).map(str::to_string));
let mut out: Vec<String> = Vec::new();
let mut push = |b: &str| {
let url = format!("{b}/{name}");
if !out.contains(&url) {
out.push(url);
}
};
match own {
Some(own) => {
push(&own);
let host = base_of(&own).unwrap_or("");
for b in neighbours.iter().filter(|b| base_of(b).unwrap_or("") == host) {
push(b);
}
}
None => neighbours.iter().for_each(|b| push(b)),
}
out
}
/// The repo's own directory name — the component reproduced under [`WORKROOT`].
fn repo_dir_name(repo_root: &Path) -> String {
repo_root
.file_name()
.and_then(|s| s.to_str())
.filter(|s| !s.is_empty())
.unwrap_or("repo")
.to_string()
}
/// The directory the repo and its siblings share. `~/git` for
/// `~/git/gunnar`.
fn checkout_parent(repo_root: &Path) -> PathBuf {
repo_root.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("/"))
}
/// Every `path = "…"` dependency in the repo's manifests that resolves OUTSIDE
/// the repo root, as `(manifest, dep name, resolved path)`.
///
/// Read with a real TOML parser rather than a regex over the file: `path` also
/// appears in `[[bin]]`, in `[lib]` and inside prose, and a scan that cannot
/// tell those apart reports a dependency that does not exist. Only the four
/// dependency table shapes cargo actually resolves are visited.
pub fn escaping_path_deps(repo_root: &Path) -> Vec<(PathBuf, String, PathBuf)> {
escaping_path_deps_scoped(repo_root, false)
}
/// [`escaping_path_deps`] of the ONE workspace rooted at `root`: a directory
/// below it whose `Cargo.toml` declares its own `[workspace]` is another
/// workspace cargo does not load when it builds this one, so its manifests are
/// not read. MEASURED 2026-09-28: holger excludes `holger-estate-front` (its own
/// workspace) and `cargo metadata` of holger loads neither `holger-ui` nor
/// `monetize`, while a whole-tree scan demanded both for holger's plugin row.
pub fn workspace_escaping_path_deps(root: &Path) -> Vec<(PathBuf, String, PathBuf)> {
escaping_path_deps_scoped(root, true)
}
fn declares_workspace(dir: &Path) -> bool {
std::fs::read_to_string(dir.join("Cargo.toml"))
.ok()
// A whole manifest is a TABLE: toml >= 0.9 parses `Value` as ONE value, so
// `parse::<toml::Value>()` of a Cargo.toml fails and every workspace root
// would read as "declares none" (2026-10-03, the release-3 merge onto toml 1).
.and_then(|t| t.parse::<toml::Table>().ok())
.is_some_and(|d| d.contains_key("workspace"))
}
fn escaping_path_deps_scoped(repo_root: &Path, one_workspace: bool) -> Vec<(PathBuf, String, PathBuf)> {
let mut out = Vec::new();
let mut stack = vec![repo_root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&dir) else { continue };
for e in rd.flatten() {
let p = e.path();
let name = e.file_name();
let name = name.to_string_lossy();
if p.is_dir() {
// `target` is build output and `.git` is history; neither
// declares a dependency, and both are large.
if name != "target" && !name.starts_with('.') && !(one_workspace && declares_workspace(&p)) {
stack.push(p);
}
} else if name == "Cargo.toml" {
collect_escaping(&p, repo_root, &mut out);
}
}
}
out.sort();
out
}
fn collect_escaping(manifest: &Path, repo_root: &Path, out: &mut Vec<(PathBuf, String, PathBuf)>) {
let Ok(text) = std::fs::read_to_string(manifest) else { return };
let Ok(doc) = text.parse::<toml::Table>().map(toml::Value::Table) else { return };
let dir = manifest.parent().unwrap_or(Path::new("."));
let mut tables: Vec<&toml::Value> = Vec::new();
for key in ["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(t) = doc.get(key) {
tables.push(t);
}
}
// `[target.<cfg>.dependencies]` and the workspace's own
// `[workspace.dependencies]` — both resolve paths the same way.
if let Some(t) = doc.get("target").and_then(toml::Value::as_table) {
for v in t.values() {
for key in ["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(d) = v.get(key) {
tables.push(d);
}
}
}
}
if let Some(d) = doc.get("workspace").and_then(|w| w.get("dependencies")) {
tables.push(d);
}
for table in tables {
let Some(t) = table.as_table() else { continue };
for (dep, spec) in t {
let Some(rel) = spec.get("path").and_then(toml::Value::as_str) else { continue };
let resolved = normalize(&dir.join(rel));
if !resolved.starts_with(repo_root) {
out.push((manifest.to_path_buf(), dep.clone(), resolved));
}
}
}
}
/// Lexical `..`/`.` resolution. NOT `canonicalize`: the target of a path dep may
/// not exist yet (that is one of the things being reported), and canonicalize
/// also resolves symlinks, which would silently rewrite a layout the operator
/// deliberately built out of links.
fn normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
std::path::Component::ParentDir => {
out.pop();
}
std::path::Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
/// **The two host paths the container run needs**, resolved in ONE place.
///
/// A consumer that re-reads `CARGO_TARGET_DIR` and `CARGO_HOME` itself becomes a
/// second env-reading twin, and the two drift the first time one of them learns
/// about a new fallback. dwarves already paid for that shape once
/// (`BuildRecordTarget::from_env`, "one resolution, not a second env-reading
/// twin").
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostPaths {
/// The host directory bind-mounted at [`TARGETDIR`]. `CARGO_TARGET_DIR`, else
/// `<repo_root>/target` — cargo's own ladder, in cargo's own order.
///
/// NOTE this deliberately does NOT consult `~/.cargo/config.toml`'s
/// `[build] target-dir`. Reading it would need cargo's whole config merge
/// (hierarchical, per-directory), and half of that ladder is worse than none:
/// the honest statement is "env or the conventional path", and a caller with
/// a configured target dir exports the variable.
pub target_dir: PathBuf,
/// The host cargo registry bind-mounted read-only under [`CARGO_HOME`].
/// `CARGO_HOME/registry`, else `~/.cargo/registry`.
pub registry: PathBuf,
}
impl HostPaths {
/// Resolve from the environment, relative to `repo_root`.
pub fn from_env(repo_root: &Path) -> Self {
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| repo_root.join("target"));
let cargo_home = std::env::var_os("CARGO_HOME").map(PathBuf::from).or_else(|| {
std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cargo"))
});
let registry = cargo_home
.unwrap_or_else(|| PathBuf::from("/nonexistent"))
.join("registry");
HostPaths { target_dir, registry }
}
/// The registry to bind, or `None` when it does not exist on this box. An
/// absent registry is NOT an error here: with `network = true` the container
/// fetches its own, and binding a path that is not there would fail the run
/// for a reason that has nothing to do with the build.
pub fn registry_bind(&self) -> Option<&Path> {
self.registry.is_dir().then_some(self.registry.as_path())
}
}
/// Is `s` a manifest digest — 64 lowercase hex characters, no prefix?
fn is_manifest_digest(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c))
}
/// The sentinel the build script prints before cargo runs, so the provenance
/// records the **container's** toolchain. Recording `Toolchain::detect()` for a
/// container build would attest to the host's rustc for a binary the host's
/// rustc never touched — a provenance that is precisely wrong.
pub const RUSTC_SENTINEL: &str = "##nordisk-rustc##";
/// The sentinel prefix for the cargo version line.
pub const CARGO_SENTINEL: &str = "##nordisk-cargo##";
/// The sentinel prefix for the container's resolved host triple.
pub const HOST_SENTINEL: &str = "##nordisk-host##";
/// **The engine-side name of one build container, scoped to its checkout.**
///
/// draupnir names a container `draupnir-<spec name>` and the name is global to
/// the USER's container engine. Before this, two builds of the same row from two
/// checkouts (two agents, two constellations: `cargo-build-holger-server` from
/// `~/a/holger` and from `~/b/holger`) asked for the same name: the second was
/// refused `[container-name-in-use]` while the first ran, and a crashed one's
/// leftover was reaped by the other's preflight. The name now carries 8 hex of
/// the sha256 of the run's HOST bind paths (checkout, target directory, stage):
/// the same checkout and target keep ONE name (a leftover of an interrupted run
/// is still reaped by the next run of the same place), two places get two.
pub fn scoped_container_name(label: &str, binds: &[String]) -> String {
use sha2::{Digest, Sha256};
let mut hosts: Vec<&str> = binds.iter().map(|b| b.split(':').next().unwrap_or("")).collect();
hosts.sort_unstable();
hosts.dedup();
let d = Sha256::digest(hosts.join("\n").as_bytes());
format!("{label}-{:02x}{:02x}{:02x}{:02x}", d[0], d[1], d[2], d[3])
}
/// **The request handed to the container seam** — everything the run needs and
/// nothing the runner has to guess.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoRunRequest {
/// A short label for the container (the variant name).
pub name: String,
/// The pinned image reference ([`CargoToolchain::image_ref`]).
pub image: String,
/// The `sh -eu -c` script.
pub script: String,
/// Host bind mounts ([`CargoToolchain::binds`]).
pub binds: Vec<String>,
/// The bound; overrun stops the container and returns an error.
pub timeout: Duration,
/// `false` runs airgapped (`--network none`).
pub network: bool,
/// The memory bound in MiB — draupnir `--memory`. Never absent.
pub mem_limit_mb: u32,
/// The cpu bound in whole cores — draupnir `--cpus`.
pub cpus: u32,
}
/// What came back from the container run. A non-zero exit is a VALUE here; the
/// ORCHESTRATOR turns it into a named refusal, so the policy lives in one place.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CargoRunOutcome {
/// The container's exit code (`Some(0)` = clean), `None` if the engine
/// reported none.
pub exit_code: Option<i64>,
/// Captured stdout lines — the `--message-format=json` stream plus the
/// sentinels.
pub stdout: Vec<String>,
/// Captured stderr lines.
pub stderr: Vec<String>,
}
/// **The container seam** the cargo builder drives — one method, so a test
/// injects a fake and proves the orchestration (and every red arm) with no
/// daemon. The production impl is [`DraupnirCargoRunner`] (feature
/// `cargo-container`), over draupnir's `backend-oci` bollard engine.
///
/// Shaped deliberately like [`KernelContainerRunner`](crate::kernel::KernelContainerRunner):
/// two kinds that both run a bounded container should not have two different
/// ideas of what that means.
pub trait CargoContainerRunner {
/// Run the build container to completion within `req.timeout`.
fn run(&self, req: &CargoRunRequest) -> Result<CargoRunOutcome>;
/// The engine label the provenance records.
fn engine(&self) -> &str {
"draupnir-oci"
}
}
/// **The file, under the bind-mounted target dir, that the container writes
/// cargo's json stream to** — the channel this crate actually reads the build
/// from. Named per variant so two builds sharing a target dir cannot overwrite
/// each other's evidence.
///
/// See [`build_script`] for why there is a file at all.
pub fn cargo_stream_file(variant: &str) -> String {
format!(".nornir-cargo-{variant}.jsonl")
}
/// Build the `sh -eu -c` script for one variant.
///
/// It is generated, never hand-written per repo: the argv comes from
/// [`BuildVariant::cargo_args`](crate::variant::BuildVariant::cargo_args), the
/// SAME writer the host path uses, so the container cannot drift into building
/// a different thing than the forge claims it built.
///
/// # Why the output goes to a FILE and is only then echoed
///
/// **A container's stdout, as relayed by the engine, is not a channel you can
/// build a correctness decision on.** MEASURED on t14s 2026-09-12, rootless
/// podman 5.7: 20 runs of a container that printed 901 lines and exited
/// returned `exit_code = Some(0)` with 0, 0, 0 and 749 lines in four of them —
/// and asking the daemon afterwards for the container's whole log, in one
/// `follow=false` request while the container still existed, returned **the
/// same short answer**. The lines were not late; they were gone. The engine
/// simply does not always have what the container printed.
///
/// That matters here more than anywhere else in this crate, because cargo's
/// `--message-format=json` stream is the ONLY thing that says a binary was
/// produced (`"reason":"compiler-artifact"` with a non-null `executable`), and
/// that line is the second-to-last of ~900. A dropped tail is a build that
/// reports success and no binary — the gunnar appliance compose's step-3
/// failure that day.
///
/// So the script writes the sentinels and cargo's stream into
/// [`cargo_stream_file`] under [`TARGETDIR`], which is a HOST directory bound
/// into the container, and the host reads the file. No daemon in the middle.
/// It is still `cat`ed to stdout afterwards so an operator watching the run
/// sees what they always saw, and so the stdout copy remains a fallback.
///
/// The `if { … } > file; then … else … fi` shape is deliberate: the script runs
/// under `sh -eu`, where a plain non-zero cargo would abort before the `cat`
/// and take cargo's own diagnostics with it. A compound in an `if` condition is
/// exempt from `-e`, so a red build still gets its output echoed and still
/// exits with cargo's code.
pub fn build_script(
tc: &CargoToolchain,
workdir: &str,
cargo_args: &[String],
cfg: Option<&str>,
stream_file: &str,
) -> String {
build_script_with_env(tc, workdir, cargo_args, cfg, &RunEnv::new(), stream_file)
}
/// [`build_script`] **plus the environment a build script reads** — exported
/// after the fixed ones, each value single-quoted. A key that is not a plain
/// `[A-Za-z_][A-Za-z0-9_]*` name is dropped: it could not be exported anyway,
/// and must not reach the shell as text.
pub fn build_script_with_env(
tc: &CargoToolchain,
workdir: &str,
cargo_args: &[String],
cfg: Option<&str>,
env: &RunEnv,
stream_file: &str,
) -> String {
let mut rustflags = tc.container_rustflags();
if let Some(c) = cfg {
// The variant's `cfg` rides the same RUSTFLAGS the host path builds
// (`provenance.rs:477`) — one rule, two transports.
if !rustflags.is_empty() {
rustflags.push(' ');
}
rustflags.push_str("--cfg ");
rustflags.push_str(c);
}
let mut args: Vec<String> = cargo_args.to_vec();
args.push("--message-format=json".into());
// `--locked` is what turns Cargo.lock from documentation into a pin, and it
// is not optional in here: a container that is allowed to re-resolve has
// reintroduced the drift the container exists to remove.
args.push("--locked".into());
if !tc.network {
args.push("--offline".into());
}
let quoted: Vec<String> = args.iter().map(|a| shell_single_quote(a)).collect();
let stream = shell_single_quote(&format!("{TARGETDIR}/{stream_file}"));
let extra_env: String = env
.iter()
.filter(|(k, _)| {
let mut c = k.chars();
c.next().is_some_and(|f| f.is_ascii_alphabetic() || f == '_')
&& c.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
})
.map(|(k, v)| format!("export {k}={}\n", shell_single_quote(v)))
.collect();
// The image IS the pinned toolchain (`rust_version`). A repo's
// `rust-toolchain.toml` names the HOST's toolchain, and when it asks for
// anything the image does not carry (a component such as rustfmt or
// clippy) rustup tries to download it — with no network, so the build dies
// before cargo starts. MEASURED on loki 2026-09-26: korp-boot in tunnr
// (whose file lists `components = ["rustfmt", "clippy"]`) failed with
// "syncing channel updates for '1.97.1…' … name resolution". The same rule
// `web_app` already applies; exported before the caller's env so a caller
// can still override it.
let toolchain_env = if tc.rust_version.is_empty() {
String::new()
} else {
format!("export RUSTUP_TOOLCHAIN={}\n", shell_single_quote(&tc.rust_version))
};
format!(
"export CARGO_HOME={CARGO_HOME}\n\
export CARGO_TARGET_DIR={TARGETDIR}\n\
export RUSTFLAGS={}\n\
export CARGO_TERM_COLOR=never\n\
{}\
{}\
cd {}\n\
if {{\n\
echo \"{RUSTC_SENTINEL}$(rustc --version)\"\n\
echo \"{CARGO_SENTINEL}$(cargo --version)\"\n\
echo \"{HOST_SENTINEL}$(rustc -vV | sed -n 's/^host: //p')\"\n\
cargo {}\n\
}} > {stream}; then rc=0; else rc=$?; fi\n\
cat {stream} || true\n\
exit $rc\n",
shell_single_quote(&rustflags),
toolchain_env,
extra_env,
shell_single_quote(workdir),
quoted.join(" ")
)
}
/// POSIX single-quoting. The script is assembled in Rust and handed to `sh -eu
/// -c` INSIDE the container — the same thing the kernel kind does — so a variant
/// name or feature string carrying a quote cannot rewrite the command.
fn shell_single_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
/// **The one container-driving loop.** Validate the declaration and the
/// variant, run the pinned image over `runner`, parse the SAME
/// `--message-format=json` stream the host path parses, map the artifact paths
/// back to the host, and capture provenance recording the CONTAINER's toolchain.
///
/// `target_dir` is the host directory bind-mounted at [`TARGETDIR`]; `registry`
/// is the host cargo registry bind-mounted read-only (`None` only makes sense
/// with `network = true`).
pub fn build_variant_in_container(
set: &BuildVariantSet,
name: &str,
repo_root: &Path,
target_dir: &Path,
registry: Option<&Path>,
tc: &CargoToolchain,
runner: &dyn CargoContainerRunner,
) -> Result<BuildOutcome> {
build_variant_in_container_diagnosed(set, name, repo_root, target_dir, registry, tc, runner, &mut None)
}
/// **What a container cargo run SAID**, for a caller that shows a build to a
/// person: cargo's real exit status, the compiler's rendered errors, and the
/// container's stderr tail (the reason when cargo emitted no diagnostic — a
/// missing system library, an OOM, a bad mount). [`build_variant_in_container`]
/// only printed these on the process's own stderr, so the ⚒ log said a red build
/// had "exited 0" with no error in sight (loki 2026-10-16: openssl, -lgraphblas).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContainerDiagnostics {
/// cargo's exit code inside the container; `None` = it never reported one.
pub exit_code: Option<i64>,
/// The compiler's rendered errors, in order.
pub errors: Vec<String>,
/// The last lines of the container's stderr (at most 30).
pub stderr_tail: Vec<String>,
}
/// [`build_variant_in_container`] that also hands back what the run said
/// ([`ContainerDiagnostics`]) in `diag` — set whenever the container ran, green or
/// red.
#[allow(clippy::too_many_arguments)]
pub fn build_variant_in_container_diagnosed(
set: &BuildVariantSet,
name: &str,
repo_root: &Path,
target_dir: &Path,
registry: Option<&Path>,
tc: &CargoToolchain,
runner: &dyn CargoContainerRunner,
diag: &mut Option<ContainerDiagnostics>,
) -> Result<BuildOutcome> {
tc.validate()?;
let variant = set.require(name)?;
set.validate(variant)?;
// The row's own sibling checkouts, when it declares them (`siblings` on the
// row): only those are mounted and only those must exist.
let tc = &tc.for_variant(variant);
tc.check_target(name, variant.target.as_deref())?;
// Only a row that declares its own siblings is scanned from its own manifest
// root, and only such a row RUNS there (its own `.cargo/config.toml`, e.g.
// holger's sibling patches); every other row keeps the repo (a `manifest_dir`
// inside the repo may belong to the repo's workspace).
let own_root = (!variant.siblings.is_empty()).then(|| variant_manifest_root(repo_root, variant));
let (cargo_args, workdir) = match own_root.as_deref().and_then(|r| container_dir_of(repo_root, r)) {
Some(dir) => (manifest_path_at_root(variant.cargo_args()), Some(dir)),
None => (variant.cargo_args(), None),
};
let run = run_cargo_in_container_scanning(
tc,
&CargoInvocation {
label: name.to_string(),
repo_root: repo_root.to_path_buf(),
target_dir: target_dir.to_path_buf(),
registry: registry.map(Path::to_path_buf),
cargo_args,
cfg: variant.cfg.clone(),
env: host_job_limit(),
extra_binds: Vec::new(),
},
runner,
&own_root.clone().unwrap_or_else(|| repo_root.to_path_buf()),
workdir.as_deref(),
)?;
let (out, parsed) = (run.outcome, run.parsed);
let success = out.exit_code == Some(0);
*diag = Some(ContainerDiagnostics {
exit_code: out.exit_code,
errors: parsed.errors.clone(),
stderr_tail: out.stderr.iter().rev().take(30).rev().cloned().collect(),
});
// **The judge, shared with the host road** (`provenance::ensure_exe_was_produced`)
// and applied BEFORE the provenance record is captured: a run that exited 0
// and staged nothing must be refused HERE, in the words that name the cause,
// not discovered by whoever later reaches for the file. It bites hardest on
// this road, because here cargo's stdout is carried by the engine's log
// stream — MEASURED on t14s 2026-09-12, 4 of 20 runs of a chatty container
// came back with exit 0 and a truncated or EMPTY stdout (draupnir 0.1.13
// closes that hole; this refuses what reaches here regardless of why).
crate::provenance::ensure_exe_was_produced(
variant,
&format!("the container {}", tc.image_ref()),
success,
parsed.units,
&parsed.artifacts,
)?;
let outcome = if success { Outcome::Success } else { Outcome::Failed };
let record = BuildProvenance::capture(
variant,
&repo_name(repo_root),
repo_root,
// The CONTAINER's toolchain, read off the sentinels — not
// `Toolchain::detect()`, which would attest to a rustc that never ran.
Toolchain {
rustc: parsed.rustc.clone(),
host: parsed.host.clone(),
cargo: parsed.cargo.clone(),
..Default::default()
}
// …and WHICH container it was. The sentinels give the rustc that ran;
// only the recipe knows the image and its manifest digest, and that is
// the pair the question "which artifacts were built with the wrong
// toolchain image" is asked of. An unpinned digest stays `None` rather
// than becoming an empty string that looks like an answer.
.with_container_image(
Some(tc.image.clone()),
(!tc.image_sha256.is_empty()).then(|| tc.image_sha256.clone()),
),
HostInfo::detect(),
outcome,
&parsed.artifacts,
)?
// Every tree this run compiled: the repo and each sibling it MOUNTED — the
// container road is the one place that knows the set, because it is the
// set it bound in (`siblings`). Without them a holger binary's provenance
// named one of its twelve source repos.
.with_sources(SourceRev::read_all(&repo_name(repo_root), repo_root, &tc.siblings));
let attestation = record.attest()?;
Ok(BuildOutcome {
variant: name.to_string(),
artifacts: parsed.artifacts,
attestation,
success,
})
}
/// **One cargo invocation for the pinned image** that is not a declared
/// `[[build.variant]]` — a composer's own build (korp-installer's korp-boot and
/// its two PID-1s) — carrying exactly what [`build_variant_in_container`] would
/// otherwise read off the variant.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoInvocation {
/// A short label: the container name suffix and the stream file's name.
pub label: String,
/// The repo mounted at [`CargoToolchain::container_repo_dir`]; cargo runs there.
pub repo_root: PathBuf,
/// The host dir bound at [`TARGETDIR`].
pub target_dir: PathBuf,
/// The host cargo registry, bound read-only.
pub registry: Option<PathBuf>,
/// cargo's arguments (`build --release …`). `--message-format=json`,
/// `--locked` and (airgapped) `--offline` are appended by [`build_script`].
pub cargo_args: Vec<String>,
/// A `--cfg` riding RUSTFLAGS, as a variant's `cfg` does.
pub cfg: Option<String>,
/// Environment the build needs INSIDE the container (a build script's
/// inputs). The container inherits nothing from the host, so this is the
/// whole of it.
pub env: RunEnv,
/// Extra `host:container[:ro]` binds (an input file a build script reads).
pub extra_binds: Vec<String>,
}
/// What [`run_cargo_in_container`] hands back: the engine's outcome and the
/// parsed stream (artifacts already mapped to host paths).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoContainerRun {
/// The engine's verdict and captured streams.
pub outcome: CargoRunOutcome,
/// The stream read back from the target dir.
pub parsed: ParsedRun,
}
impl CargoContainerRun {
/// `true` when the container exited 0.
pub fn success(&self) -> bool {
self.outcome.exit_code == Some(0)
}
}
/// **The one container-driving loop.** Run `inv` in the pinned image over
/// `runner` and parse the SAME `--message-format=json` stream the host path
/// parses. [`build_variant_in_container`] is this plus a variant's judge and
/// provenance; a composer's ad-hoc build is this plus its own output checks.
/// A red build has its compiler errors and the stderr tail printed here.
pub fn run_cargo_in_container(
tc: &CargoToolchain,
inv: &CargoInvocation,
runner: &dyn CargoContainerRunner,
) -> Result<CargoContainerRun> {
run_cargo_in_container_scanning(tc, inv, runner, &inv.repo_root, None)
}
/// The root whose manifests a row's cargo loads: the repo, or — for a row with a
/// [`manifest_dir`](BuildVariant::manifest_dir) — that directory, resolved
/// lexically against the repo (`../holger` → the sibling checkout `holger`).
pub fn variant_manifest_root(repo_root: &Path, v: &BuildVariant) -> PathBuf {
match v.manifest_dir.as_deref().map(str::trim).filter(|d| !d.is_empty()) {
Some(d) => {
let abs = std::path::absolute(repo_root).unwrap_or_else(|_| repo_root.to_path_buf());
normalize(&abs.join(d))
}
None => repo_root.to_path_buf(),
}
}
/// The container path of a host dir that is the repo, inside it, or a sibling
/// beside it (every one is mounted under [`WORKROOT`] by its name); `None` when
/// it is none of them.
fn container_dir_of(repo_root: &Path, dir: &Path) -> Option<String> {
let repo = normalize(&std::path::absolute(repo_root).unwrap_or_else(|_| repo_root.to_path_buf()));
let rel = dir.strip_prefix(repo.parent()?).ok()?;
let rel = rel.to_str().filter(|r| !r.is_empty())?;
Some(format!("{WORKROOT}/{rel}"))
}
/// cargo's arguments with `--manifest-path` naming the manifest of the directory
/// cargo runs in.
fn manifest_path_at_root(mut args: Vec<String>) -> Vec<String> {
if let Some(i) = args.iter().position(|a| a == "--manifest-path") {
if let Some(v) = args.get_mut(i + 1) {
*v = "Cargo.toml".to_string();
}
}
args
}
/// [`run_cargo_in_container`] with the escaping-path-dep check scoped to
/// `scan_root` (the row's manifest root) instead of the whole repo.
fn run_cargo_in_container_scanning(
tc: &CargoToolchain,
inv: &CargoInvocation,
runner: &dyn CargoContainerRunner,
scan_root: &Path,
// The container dir cargo runs in when it is not the repo's: a row with its
// own manifest root runs THERE, because cargo reads `.cargo/config.toml` from
// its working directory and never from `--manifest-path`.
workdir: Option<&str>,
) -> Result<CargoContainerRun> {
tc.validate()?;
let (name, repo_root, target_dir) = (inv.label.as_str(), inv.repo_root.as_path(), inv.target_dir.as_path());
// Named BEFORE the container starts, because cargo's version of this failure
// names a path it could not read and never the manifest line that asked for
// it, nor the one-word declaration that fixes it.
if scan_root == repo_root {
tc.check_escaping_path_deps(repo_root)?;
} else {
let abs = std::path::absolute(repo_root).unwrap_or_else(|_| repo_root.to_path_buf());
tc.check_escaping_path_deps_from(&normalize(&abs), scan_root)?;
}
let workdir = workdir.map(str::to_string).unwrap_or_else(|| tc.container_repo_dir(repo_root));
// **Delete, run, read.** The stream file is the evidence this build is
// judged on, so a leftover from a previous run of the same variant would be
// the worst possible answer — a stale artifact path reported for a build
// that produced nothing. Removing it first is what makes reading it sound.
let stream_name = cargo_stream_file(name);
let host_stream = target_dir.join(&stream_name);
match std::fs::remove_file(&host_stream) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => {
return Err(anyhow!(
"clear the previous cargo stream at {}: {e} — it cannot be left in place, a \
stale one would be read as this build's evidence",
host_stream.display()
))
}
}
let script = build_script_with_env(tc, &workdir, &inv.cargo_args, inv.cfg.as_deref(), &inv.env, &stream_name);
let req = CargoRunRequest {
name: format!("cargo-build-{name}"),
image: tc.image_ref(),
script,
binds: {
let mut b = tc.binds(repo_root, target_dir, inv.registry.as_deref());
b.extend(inv.extra_binds.iter().cloned());
b
},
timeout: Duration::from_secs(tc.timeout_secs),
network: tc.network,
mem_limit_mb: tc.mem_limit_mb,
cpus: tc.cpus,
};
let out = runner.run(&req)?;
// **Read the FILE, not the engine's idea of the container's stdout.** See
// `build_script` for the measurement: podman returned 0 lines for a
// container that printed 901, and said the same when asked for the whole
// log afterwards. The stdout copy is the fallback for a runner that writes
// no file at all (every unit test's fake, and any future transport).
let lines: Vec<String> = match std::fs::read_to_string(&host_stream) {
Ok(text) if !text.trim().is_empty() => text.lines().map(str::to_string).collect(),
_ => out.stdout.clone(),
};
let parsed = parse_container_stdout(&lines, target_dir);
// A declared rust_version that the container does not actually carry is a
// NAMED refusal. It is the one check that catches "the tag moved" even when
// the digest pin is present but stale in the declaration.
if !tc.rust_version.is_empty()
&& !parsed.rustc.is_empty()
&& !parsed.rustc.contains(&tc.rust_version)
{
return Err(anyhow!(
"[build.toolchain] declares `rust_version = \"{}\"` but the image `{}` reports \
`{}` — the declaration and the pinned image disagree about what compiled this",
tc.rust_version,
tc.image_ref(),
parsed.rustc
));
}
let success = out.exit_code == Some(0);
if !success {
// **Say what the compiler said.** The host path gets this for free —
// cargo's `json-render-diagnostics` prints straight to the operator's
// terminal. Inside a container BOTH streams are captured, so a failed
// build that does not re-emit them hands the caller `success = false`
// and nothing else: a red with no reason, which is the shape that makes
// people re-run a build to find out why it broke. Same one-line-per-fact
// convention as `buildgate::run_cargo`'s announcement, on stderr so it
// never mixes into a report on stdout.
eprintln!(
"[cargo-container] {name} FAILED (exit {:?}) in {}",
out.exit_code,
tc.image_ref()
);
for e in &parsed.errors {
eprintln!("[cargo-container] {}", e.trim_end());
}
// The container's own stderr matters even when cargo emitted no
// diagnostic at all — "no such file", a killed OOM, a bad bind mount.
// Tail, not the whole thing: a runaway build must not bury the reason.
for l in out.stderr.iter().rev().take(30).collect::<Vec<_>>().into_iter().rev() {
eprintln!("[cargo-container] err: {}", l.trim_end());
}
}
Ok(CargoContainerRun { outcome: out, parsed })
}
/// What [`parse_container_stdout`] recovers from the run's stdout.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParsedRun {
/// Host-side artifact paths (container `/target/…` rewritten).
pub artifacts: Vec<PathBuf>,
/// Compiler errors, rendered.
pub errors: Vec<String>,
/// The container's `rustc --version` line.
pub rustc: String,
/// The container's `cargo --version` line.
pub cargo: String,
/// The container's host triple.
pub host: String,
/// How many compiler artifacts the stream carried.
pub units: usize,
}
/// Parse the container's stdout: the sentinels, then cargo's
/// `--message-format=json` stream.
///
/// **The path rewrite is the load-bearing part.** cargo reports executables as
/// `/target/release/foo` because that is where they are IN THE CONTAINER; the
/// host sees them at `<target_dir>/release/foo`. A caller handed the container's
/// path would hash a file that does not exist and report `0` artifacts for a
/// build that produced one — a green with nothing behind it.
pub fn parse_container_stdout(lines: &[String], target_dir: &Path) -> ParsedRun {
let mut out = ParsedRun::default();
for line in lines {
let line = line.trim_end();
if let Some(v) = line.strip_prefix(RUSTC_SENTINEL) {
out.rustc = v.trim().to_string();
continue;
}
if let Some(v) = line.strip_prefix(CARGO_SENTINEL) {
out.cargo = v.trim().to_string();
continue;
}
if let Some(v) = line.strip_prefix(HOST_SENTINEL) {
out.host = v.trim().to_string();
continue;
}
if !line.starts_with('{') {
continue;
}
let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) else {
// A malformed line is not a reason to abandon the run: the artifacts
// already collected are still evidence, and the exit code decides.
continue;
};
match msg.get("reason").and_then(|r| r.as_str()) {
Some("compiler-artifact") => {
out.units += 1;
if let Some(exe) = msg.get("executable").and_then(|e| e.as_str()) {
out.artifacts.push(rewrite_target_path(exe, target_dir));
}
}
Some("compiler-message") => {
let m = &msg["message"];
if m.get("level").and_then(|l| l.as_str()) == Some("error") {
let rendered = m
.get("rendered")
.and_then(|r| r.as_str())
.or_else(|| m.get("message").and_then(|r| r.as_str()))
.unwrap_or("")
.to_string();
out.errors.push(rendered);
}
}
_ => {}
}
}
out
}
/// Map a container `/target/…` path onto the host target dir. A path that is
/// NOT under [`TARGETDIR`] is returned unchanged rather than silently rebased —
/// guessing would turn an unexpected layout into a wrong answer.
fn rewrite_target_path(container_path: &str, target_dir: &Path) -> PathBuf {
match container_path.strip_prefix(&format!("{TARGETDIR}/")) {
Some(rest) => target_dir.join(rest),
None => PathBuf::from(container_path),
}
}
fn repo_name(repo_root: &Path) -> String {
repo_root
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("repo")
.to_string()
}
// ── the byte-identity gate ───────────────────────────────────────────────────
/// **The verdict of the host-vs-container gate.** It is an enum and not a bool
/// because "not identical" has causes, and a gate that collapses them into
/// `false` teaches nobody anything — while one that collapses them into `true`
/// has been weakened into a lie.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ByteIdentity {
/// Same sha256. The build is genuinely containerised.
Identical {
/// The shared sha256.
sha256: String,
/// Size in bytes.
len: u64,
},
/// Different bytes. The build has been FORKED, not containerised — the two
/// sides are running different compilations that merely share a name.
Differs {
/// The host artifact's sha256.
host: String,
/// The container artifact's sha256.
container: String,
/// Host size in bytes.
host_len: u64,
/// Container size in bytes.
container_len: u64,
},
/// One of the two sides produced no artifact to compare. NOT a pass: a
/// missing side is the strongest possible red.
Missing {
/// Which side, and why.
why: String,
},
/// **The variant is not reproducible AT ALL**, proven by a control: two
/// builds on the SAME side, same everything, came out different. The
/// host-vs-container comparison is then meaningless — whatever it says, it
/// is not saying anything about the container.
///
/// Still a RED. The build is unauditable either way; this verdict only moves
/// the blame to where it belongs, which is the repo.
NotReproducible {
/// First control build.
first: String,
/// Second control build, same side, same configuration.
second: String,
/// What was measured to differ, when that is known.
cause: String,
},
}
impl ByteIdentity {
/// Is this the green? Only [`Identical`](Self::Identical) is.
pub fn is_identical(&self) -> bool {
matches!(self, ByteIdentity::Identical { .. })
}
/// The crisp one-liner: col-1 identifier, col-2 status, col-3 the tech
/// detail that fills the rest of the line.
pub fn line(&self, variant: &str) -> String {
match self {
ByteIdentity::Identical { sha256, len } => format!(
"{variant:<14} DONE host and container artifacts are the SAME BYTES — \
sha256 {} ({len} B); the build is containerised, not forked",
&sha256[..16.min(sha256.len())]
),
ByteIdentity::Differs { host, container, host_len, container_len } => format!(
"{variant:<14} as dåligt host {}… ({host_len} B) != container {}… \
({container_len} B) — two different compilations sharing a name",
&host[..16.min(host.len())],
&container[..16.min(container.len())]
),
ByteIdentity::Missing { why } => {
format!("{variant:<14} as dåligt no artifact to compare: {why}")
}
ByteIdentity::NotReproducible { first, second, cause } => format!(
"{variant:<14} as dåligt NOT the container: two builds on the SAME side \
differ ({}… vs {}…) — {cause}. Byte-identity is unreachable for this \
variant until the repo stops making it so",
&first[..16.min(first.len())],
&second[..16.min(second.len())]
),
}
}
}
/// Compare a host artifact against a container artifact by sha256 — **the gate**.
///
/// Both paths are hashed with the shared `nornir_hash::sha256_file`, the same
/// hasher provenance seals with, so the gate and the receipt cannot disagree
/// about what a file's identity is.
pub fn byte_identity_verdict(host: &Path, container: &Path) -> ByteIdentity {
let h = match hash_and_len(host) {
Ok(v) => v,
Err(e) => {
return ByteIdentity::Missing { why: format!("host side: {e:#}") };
}
};
let c = match hash_and_len(container) {
Ok(v) => v,
Err(e) => {
return ByteIdentity::Missing { why: format!("container side: {e:#}") };
}
};
if h.0 == c.0 {
ByteIdentity::Identical { sha256: h.0, len: h.1 }
} else {
ByteIdentity::Differs {
host: h.0,
container: c.0,
host_len: h.1,
container_len: c.1,
}
}
}
/// **The gate, with a control** — the form to use on any variant whose build
/// might not be reproducible, which is to say all of them until measured.
///
/// `control_a` and `control_b` are two builds of the same variant on the SAME
/// side with the SAME configuration. If they differ, the variant is not
/// reproducible at all and the host-vs-container question cannot be answered:
/// the comparison would report a difference and silently attribute it to the
/// container.
///
/// MEASURED 2026-08-26, and this is why the verdict exists rather than being a
/// hypothetical: `gunnar-server` came out different on every build, and the
/// cause is in the repo, not in the container.
/// `gunnar/crates/gunnar-common/build.rs:43` runs
/// `date -u +%Y-%m-%dT%H:%M:%SZ` and bakes the result as `GUNNAR_BUILD_DATE`.
/// The two artifacts carried `2026-08-25T22:43:39Z` and `2026-08-25T22:44:47Z`.
/// It is a deliberate, documented identity stamp — and at second resolution off
/// the wall clock it makes two builds one second apart different binaries, on
/// one box or two. A gate without this control would have called that the
/// container's doing.
pub fn byte_identity_with_control(
control_a: &Path,
control_b: &Path,
host: &Path,
container: &Path,
cause: &str,
) -> ByteIdentity {
let (a, b) = match (hash_and_len(control_a), hash_and_len(control_b)) {
(Ok(a), Ok(b)) => (a, b),
(Err(e), _) | (_, Err(e)) => {
return ByteIdentity::Missing { why: format!("control build: {e:#}") };
}
};
if a.0 != b.0 {
return ByteIdentity::NotReproducible {
first: a.0,
second: b.0,
cause: if cause.is_empty() { "cause not identified".into() } else { cause.into() },
};
}
byte_identity_verdict(host, container)
}
fn hash_and_len(p: &Path) -> Result<(String, u64)> {
let len = std::fs::metadata(p)
.with_context(|| format!("stat {}", p.display()))?
.len();
let sha = nornir_hash::sha256_file(p).with_context(|| format!("sha256 {}", p.display()))?;
Ok((sha, len))
}
// ── minting the toolchain without moving anybody's name ─────────────────────
/// **The tag a mint of THIS crate's recipe lands under**:
/// `<image>:recipe-<first 16 hex of sha256(CARGO_TOOLCHAIN_CONTAINERFILE)>`.
///
/// Content-addressed by the recipe. The mint is reproducible, so one
/// Containerfile always lands one image, and two crates carrying two different
/// Containerfiles mint under two different tags: neither can move the other's
/// name. A floating `:live` is the opposite, and on 2026-10-05 it cost Build
/// Thing its image. holger's `cargo xtask build-wasm-plugins`, built against an
/// edda whose recipe had moved on (clang + llvm, manifest `668468e7…`, id
/// `88c4902987c2`), re-minted `nordisk-cargo-toolchain:live` onto that image.
/// The image gunnar and holger PIN (`3d5115ba…`) lost the only name it had, a
/// local `nordisk-cargo-toolchain@sha256:3d5115ba…` lookup needs a name under
/// that repository, and so the engine went to a registry for it: `403`.
#[must_use]
pub fn cargo_toolchain_recipe_tag(image: &str) -> String {
let h = nornir_hash::sha256_hex(CARGO_TOOLCHAIN_CONTAINERFILE.as_bytes());
format!("{image}:recipe-{}", &h[..16])
}
/// A recipe and the manifest digest its reproducible mint lands.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ToolchainPin {
/// First 16 hex of `sha256(CARGO_TOOLCHAIN_CONTAINERFILE)` — the recipe minted.
pub recipe: &'static str,
/// The manifest digest (hex, no `sha256:`) that mint lands, on every box.
pub digest: &'static str,
}
/// **THE pin of this crate's cargo toolchain recipe** — the digest declarations name
/// (`[build.toolchain] image_sha256`) when they build in THIS recipe's image, held
/// beside the recipe hash it was minted from. Change the Containerfile and
/// `toolchain_pin_tests::the_pin_names_this_recipe` goes red until the image is
/// re-minted (`cargo run -p nornir-build-thing --features cargo-container --example
/// mint_cargo_toolchain`) and both halves are moved — in one reviewed commit, with
/// the declarations that pin it.
///
/// **2026-10-16: `668468e7…` (recipe-a6a9f191…) → `86d575e8…` (recipe-53fec13a…)**,
/// the recipe that adds `libgraphblas-dev` (nornir's code room links `-lgraphblas`).
/// Minted on dwarf through `mint_cargo_toolchain` (reproducible; the probe build of
/// the same Containerfile landed the same image id `e97625105590`).
pub const CARGO_TOOLCHAIN_PIN: ToolchainPin = ToolchainPin {
recipe: "53fec13a5c1c7f2c",
digest: "86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762",
};
/// The repository half of an image reference: `name` from `name:tag`,
/// `localhost:5000/name` from `localhost:5000/name:tag`, `name` from
/// `name@sha256:…`.
fn image_repo_of(image_ref: &str) -> &str {
let no_digest = image_ref.split_once('@').map_or(image_ref, |(r, _)| r);
match no_digest.rsplit_once(':') {
Some((repo, tag)) if !tag.contains('/') => repo,
_ => no_digest,
}
}
/// **What minting the toolchain asks of a container engine**: three
/// questions, behind a seam so the decision of WHICH name a mint may write
/// ([`ensure_declared_toolchain`], [`mint_toolchain_into`]) is provable by a
/// fake store with no socket. [`DraupnirCargoRunner`] is the live one.
pub trait ToolchainImageStore {
/// Does the engine resolve `image_ref` (a tag, or `name@sha256:…`)
/// locally? `Err` only when the engine could not be asked, never for
/// "absent".
fn image_present(&self, image_ref: &str) -> Result<bool>;
/// The engine's image id for `image_ref`; `None` when it holds none.
fn image_id_of(&self, image_ref: &str) -> Result<Option<String>>;
/// Mint [`CARGO_TOOLCHAIN_CONTAINERFILE`] reproducibly; the build writes
/// `tag`.
fn mint_recipe(&self, context_dir: &Path, tag: &str) -> Result<()>;
}
/// What [`ensure_declared_toolchain`] did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolchainMint {
/// The pinned reference already resolved: nothing minted, no name written.
AlreadyPresent,
/// Minted under `tag`, and the pin resolves now.
Minted {
/// The tag the mint wrote.
tag: String,
},
}
/// **Step 1 of every cargo road: the declared pin is on this box, or is made
/// to be.** Present: nothing is minted and no name is written. Absent: the
/// embedded recipe is minted under [`cargo_toolchain_recipe_tag`], never under
/// a floating tag, and the pin must resolve afterwards; a recipe that mints
/// some other digest is refused by name, having moved no name anybody else's
/// build stands on.
pub fn ensure_declared_toolchain(
store: &dyn ToolchainImageStore,
context_dir: &Path,
tc: &CargoToolchain,
) -> Result<ToolchainMint> {
let pinned = tc.image_ref();
if store.image_present(&pinned)? {
return Ok(ToolchainMint::AlreadyPresent);
}
let tag = cargo_toolchain_recipe_tag(&tc.image);
store.mint_recipe(context_dir, &tag)?;
if !store.image_present(&pinned)? {
let minted = store.image_id_of(&tag)?.unwrap_or_else(|| "?".to_string());
return Err(anyhow!(
"the declared pin `{pinned}` is not on this box, and this crate's recipe does not \
mint it: it minted `{tag}` (image {minted}). The committed \
cargo-toolchain.Containerfile and `[build.toolchain] image_sha256` disagree. \
Re-pin the recipe to the manifest digest of `{tag}` (in the same commit as the \
Containerfile it came from), or build once in an explicit image with \
{TOOLCHAIN_IMAGE_OVERRIDE_ENV}=<image>@sha256:<digest>. No floating tag was \
written."
));
}
Ok(ToolchainMint::Minted { tag })
}
/// **Mint the recipe into an explicit `tag`, never moving it off another
/// image.** Returns the tag.
///
/// The recipe is minted under [`cargo_toolchain_recipe_tag`] first. Then:
///
/// * `tag` IS the recipe tag: done.
/// * `tag` names nothing: it is written (a second mint of the same reproducible
/// recipe, answered from the layer cache), and must land the same image.
/// * `tag` already names the minted image: nothing to move.
/// * `tag` names ANOTHER image: refused, `tag` left where it is. Moving it
/// would un-name that image, and a pin on it would then resolve to a
/// registry. Creation time cannot say which image is "newer" here: every
/// recipe of this Containerfile carries the same `SOURCE_DATE_EPOCH`
/// (1783900800 in both `3d5115ba…` and `668468e7…`), so the ids decide and a
/// different id is a refusal.
pub fn mint_toolchain_into(
store: &dyn ToolchainImageStore,
context_dir: &Path,
tag: &str,
) -> Result<String> {
let recipe = cargo_toolchain_recipe_tag(image_repo_of(tag));
store.mint_recipe(context_dir, &recipe)?;
if tag == recipe {
return Ok(recipe);
}
let minted = store.image_id_of(&recipe)?.ok_or_else(|| {
anyhow!("the recipe was minted as `{recipe}` and the engine does not hold it")
})?;
match store.image_id_of(tag)? {
Some(held) if held == minted => Ok(tag.to_string()),
Some(held) => Err(anyhow!(
"refused to move `{tag}`: it names image {held}, and this recipe mints image \
{minted} (now tagged `{recipe}`). Moving `{tag}` would un-name {held}, and a \
`name@sha256:` pin on it would then go to a registry and fail (2026-10-05, \
Build Thing, 403). Build in `{recipe}`, or re-pin to its digest; `{tag}` is \
left where it was."
)),
None => {
store.mint_recipe(context_dir, tag)?;
match store.image_id_of(tag)? {
Some(id) if id == minted => Ok(tag.to_string()),
other => Err(anyhow!(
"minted the recipe twice and got two images: `{recipe}` is {minted}, \
`{tag}` is {} — the recipe is not reproducible on this box",
other.unwrap_or_else(|| "absent".to_string())
)),
}
}
}
}
// ── the live leg ─────────────────────────────────────────────────────────────
/// The live [`CargoContainerRunner`]: draupnir's `backend-oci` bollard engine —
/// the SAME container leg dwarves and the kernel kind already drive, reused
/// rather than re-rolled. Call from a plain sync thread (draupnir owns its own
/// tokio runtime; `block_on` inside another runtime panics).
#[cfg(feature = "cargo-container")]
#[derive(Default)]
pub struct DraupnirCargoRunner {
boot: draupnir::container::ContainerBoot,
}
#[cfg(feature = "cargo-container")]
impl DraupnirCargoRunner {
/// A runner over a fresh draupnir container backend.
pub fn new() -> Self {
DraupnirCargoRunner { boot: draupnir::container::ContainerBoot::new() }
}
/// **Build the toolchain image** from the committed
/// [`CARGO_TOOLCHAIN_CONTAINERFILE`] and return the tag it was built under.
///
/// The Containerfile is written into `context_dir` from the crate's own
/// embedded copy — a repo never gets to supply its own build environment,
/// which is the whole point of embedding it.
///
/// Since 2026-09-05 (lane bevis) it mints REPRODUCIBLY, the way the kernel
/// runner has since 2026-08-30: the instant comes from
/// [`cargo_toolchain_source_date_epoch`] and rides podman's `timestamp`
/// build parameter through
/// [`ContainerBoot::build_image_reproducible`](draupnir::container::ContainerBoot::build_image_reproducible).
/// What stood here was the plain `build_image`, and MEASURED that day it
/// gave gunnar's `[build.toolchain] image_sha256` pin a new value to chase
/// on every re-mint — a pin on such an image names a minting event, not a
/// recipe.
///
/// Goes through [`mint_toolchain_into`]; see it for which names a mint may
/// write.
pub fn build_toolchain_image(&self, context_dir: &Path, tag: &str) -> Result<String> {
mint_toolchain_into(self, context_dir, tag)
}
/// [`ensure_declared_toolchain`] on this engine: the cargo road's step 1.
pub fn ensure_declared_toolchain(
&self,
context_dir: &Path,
tc: &CargoToolchain,
) -> Result<ToolchainMint> {
ensure_declared_toolchain(self, context_dir, tc)
}
/// **Build the TEST image** from the committed
/// [`CARGO_TEST_CONTAINERFILE`], on top of `toolchain_ref`.
///
/// `toolchain_ref` should be the **digest-pinned** reference of the
/// toolchain image this test image is to witness runs against
/// (`nordisk-cargo-toolchain@sha256:…`), for the reason a repo pins
/// `[build.toolchain] image_sha256`: a test image built on a moving base
/// says nothing about the run it was used for. A tag is accepted because a
/// human building one by hand has one, and refusing it would only push
/// them to a shell.
///
/// The pinned `cargo-deny` tarball is fetched and verified into the
/// context first ([`stage_cargo_deny`]); everything else comes from the
/// snapshot the base already points apt at. The mint is reproducible the
/// same way [`build_toolchain_image`](Self::build_toolchain_image) is, and
/// off the SAME instant — the base is the one writer of it.
///
/// # Errors
///
/// The context cannot be written, the cargo-deny pin cannot be read or
/// does not match what was fetched, or the image build itself fails.
pub fn build_test_image(
&self,
fetcher: &dyn crate::kernel::TarballFetcher,
context_dir: &Path,
toolchain_ref: &str,
tag: &str,
) -> Result<String> {
std::fs::create_dir_all(context_dir)
.with_context(|| format!("create build context {}", context_dir.display()))?;
let cf = context_dir.join("cargo-test.Containerfile");
std::fs::write(&cf, cargo_test_containerfile_on(toolchain_ref)?)
.with_context(|| format!("write {}", cf.display()))?;
stage_cargo_deny(fetcher, context_dir)?;
let epoch = cargo_toolchain_source_date_epoch()?;
self.boot
.build_image_reproducible(context_dir, "cargo-test.Containerfile", tag, epoch)
.map_err(|e| anyhow!("build cargo test image `{tag}` on `{toolchain_ref}`: {e}"))
}
}
#[cfg(feature = "cargo-container")]
impl ToolchainImageStore for DraupnirCargoRunner {
fn image_present(&self, image_ref: &str) -> Result<bool> {
self.boot
.image_present(image_ref)
.map_err(|e| anyhow!("ask the engine for `{image_ref}`: {e}"))
}
/// Presence first, because draupnir's `image_id` folds "the engine could
/// not be asked" into `None`, and a `None` read as "names nothing" is what
/// lets a tag be written.
fn image_id_of(&self, image_ref: &str) -> Result<Option<String>> {
if !self.image_present(image_ref)? {
return Ok(None);
}
let id = self
.boot
.image_id(image_ref)
.map_err(|e| anyhow!("ask the engine for the id of `{image_ref}`: {e}"))?;
id.map(Some).ok_or_else(|| {
anyhow!("the engine holds `{image_ref}` and would not say which image it is")
})
}
fn mint_recipe(&self, context_dir: &Path, tag: &str) -> Result<()> {
std::fs::create_dir_all(context_dir)
.with_context(|| format!("create build context {}", context_dir.display()))?;
let cf = context_dir.join("cargo-toolchain.Containerfile");
std::fs::write(&cf, CARGO_TOOLCHAIN_CONTAINERFILE)
.with_context(|| format!("write {}", cf.display()))?;
let epoch = cargo_toolchain_source_date_epoch()?;
self.boot
.build_image_reproducible(context_dir, "cargo-toolchain.Containerfile", tag, epoch)
.map_err(|e| anyhow!("build cargo toolchain image `{tag}`: {e}"))?;
Ok(())
}
}
#[cfg(feature = "cargo-container")]
impl CargoContainerRunner for DraupnirCargoRunner {
fn run(&self, req: &CargoRunRequest) -> Result<CargoRunOutcome> {
let mut spec = draupnir::BootSpec::container(&scoped_container_name(&req.name, &req.binds), &req.image)
.with_cmd(["/bin/sh", "-eu", "-c", req.script.as_str()]);
if !req.network {
spec = spec.with_net(draupnir::NetMode::None);
}
// The bounds ride the SAME draupnir builders every other bounded
// container uses (`--cpus` → nano_cpus, `--memory` → memory); nothing
// engine-specific is spelled twice here.
spec = spec.with_cpus(f64::from(req.cpus)).with_mem_limit_mb(req.mem_limit_mb);
spec.validate().map_err(|e| anyhow!("cargo container spec: {e}"))?;
let opts =
draupnir::container::RunOptions::bounded(req.timeout, Duration::from_secs(2));
let out = self.boot.run_to_completion_with_binds(&spec, &opts, &req.binds);
// draupnir may have had to clear the wreckage of an INTERRUPTED run to free
// this job's deterministic container name. It is a library and prints
// nothing; this is the consumer that owes the human the sentence, and it is
// said whether the run then succeeded or failed.
for reap in self.boot.drain_reaps() {
eprintln!("[cargo-container] {reap}");
}
let out = out.map_err(|e| anyhow!("cargo build container `{}`: {e}", req.name))?;
Ok(CargoRunOutcome { exit_code: out.exit_code, stdout: out.stdout, stderr: out.stderr })
}
fn engine(&self) -> &str {
"draupnir-oci"
}
}
/// Environment overrides a caller can thread through for a one-off run without
/// editing the declaration. Kept as a named type rather than a bare map so a
/// reader can see there is exactly one place ad-hoc environment enters.
pub type RunEnv = BTreeMap<String, String>;
/// The host's `CARGO_BUILD_JOBS`, carried into a variant's container when set.
///
/// `cpus` bounds the CPU QUOTA; on a box shared between lanes the operator also
/// caps the job count (`CARGO_BUILD_JOBS=4`), and before this the container
/// never saw it and ran `nproc` jobs inside the quota. A job count changes how
/// long a build takes, not its bytes, so it is not a provenance input.
pub fn host_job_limit() -> RunEnv {
let mut env = RunEnv::new();
if let Ok(j) = std::env::var("CARGO_BUILD_JOBS") {
if !j.is_empty() && j.bytes().all(|b| b.is_ascii_digit()) {
env.insert("CARGO_BUILD_JOBS".to_string(), j);
}
}
env
}
/// **The one override of the declared toolchain image** — `<image>@sha256:<manifest
/// digest>`. For a run that must build in an image the declaration does not pin
/// YET (the image carrying a new std, proven before the repo's pin moves). A tag
/// is refused, same as in the declaration. The bounds, `rust_version` and
/// `siblings` stay the declaration's; `targets` becomes what the committed
/// Containerfile installs when the image is [`CARGO_TOOLCHAIN_IMAGE`], and
/// undeclared otherwise.
pub const TOOLCHAIN_IMAGE_OVERRIDE_ENV: &str = "NORDISK_CARGO_TOOLCHAIN_IMAGE";
/// `declared`, with [`TOOLCHAIN_IMAGE_OVERRIDE_ENV`] applied when it is set —
/// announced on stderr, because a build in an image the declaration does not
/// name must never be silent. Unset: `declared`, unchanged.
pub fn toolchain_with_override(declared: Option<CargoToolchain>) -> Result<Option<CargoToolchain>> {
let Some(spec) = std::env::var(TOOLCHAIN_IMAGE_OVERRIDE_ENV).ok().filter(|s| !s.trim().is_empty()) else {
return Ok(declared);
};
let tc = apply_image_override(declared.unwrap_or_default(), &spec)?;
eprintln!(
"[cargo-container] {TOOLCHAIN_IMAGE_OVERRIDE_ENV}: building in {} — NOT the declaration's pin",
tc.image_ref()
);
Ok(Some(tc))
}
/// Apply one `<image>@sha256:<digest>` override to `tc` (see
/// [`TOOLCHAIN_IMAGE_OVERRIDE_ENV`]).
pub fn apply_image_override(mut tc: CargoToolchain, spec: &str) -> Result<CargoToolchain> {
let spec = spec.trim();
let Some((name, digest)) = spec.split_once("@sha256:") else {
return Err(anyhow!(
"{TOOLCHAIN_IMAGE_OVERRIDE_ENV}=`{spec}`: expected `<image>@sha256:<64 hex manifest digest>` — a tag is not a pin"
));
};
if name.is_empty() || !is_manifest_digest(digest) {
return Err(anyhow!(
"{TOOLCHAIN_IMAGE_OVERRIDE_ENV}=`{spec}`: the image name must be non-empty and the digest 64 lowercase hex"
));
}
tc.image = name.to_string();
tc.image_sha256 = digest.to_string();
tc.targets = if name.contains(CARGO_TOOLCHAIN_IMAGE) { cargo_toolchain_image_targets() } else { Vec::new() };
Ok(tc)
}
/// **Every sibling repo a build of `repo_root` reaches, transitively** — the
/// `siblings` list for a repo whose own path deps walk into repos that walk into
/// more (edda → nornir → holger …). cargo loads every path manifest of a
/// workspace, so a container missing one fails on `failed to read`. Discovered
/// with the same lexical reader [`CargoToolchain::check_escaping_path_deps`]
/// uses; a dep into a directory that is not on this box is skipped (it is not
/// in the graph a host build resolves either). Sorted, `repo_root` excluded.
pub fn sibling_closure(repo_root: &Path) -> Vec<String> {
let parent = checkout_parent(repo_root);
let own = repo_dir_name(repo_root);
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut queue = vec![repo_root.to_path_buf()];
while let Some(r) = queue.pop() {
for (_, _, resolved) in escaping_path_deps(&r) {
let Some(name) = resolved
.strip_prefix(&parent)
.ok()
.and_then(|p| p.components().next())
.and_then(|c| c.as_os_str().to_str())
.map(str::to_string)
else {
continue;
};
if name == own || seen.contains(&name) || !parent.join(&name).is_dir() {
continue;
}
seen.insert(name.clone());
queue.push(parent.join(&name));
}
}
seen.into_iter().collect()
}
/// **The sibling checkouts a manifest's cargo actually loads**, read off `cargo
/// metadata` (`--locked`): every path package in the resolve, by the directory
/// under `checkouts` (the directory the checkouts share — a repo root's parent)
/// it lives in. Sorted, the manifest's own checkout INCLUDED (a row built from `../holger` mounts `holger` too; a caller drops
/// its own repo). This is the measured closure a per-row
/// [`siblings`](crate::variant::BuildVariant::siblings) list is held against —
/// [`sibling_closure`] is the lexical over-approximation of a whole tree, which
/// for a repo that builds two products' plugins is the union of both.
///
/// A path package outside the checkout parent is refused by name: a flat
/// sibling mount cannot carry it.
pub fn metadata_sibling_closure(manifest_path: &Path, checkouts: &Path) -> Result<Vec<String>> {
let manifest = normalize(&std::path::absolute(manifest_path).unwrap_or_else(|_| manifest_path.to_path_buf()));
let parent = normalize(&std::path::absolute(checkouts).unwrap_or_else(|_| checkouts.to_path_buf()));
let md = cargo_metadata::MetadataCommand::new()
.manifest_path(&manifest)
.other_options(vec!["--locked".to_string()])
.exec()
.with_context(|| format!("cargo metadata --locked --manifest-path {}", manifest.display()))?;
let mut out = std::collections::BTreeSet::new();
let mut outside = Vec::new();
for p in md.packages.iter().filter(|p| p.source.is_none()) {
let mp = normalize(p.manifest_path.as_std_path());
match mp.strip_prefix(&parent).ok().and_then(|r| r.components().next()) {
Some(c) => {
out.insert(c.as_os_str().to_string_lossy().into_owned());
}
None => outside.push(format!("{} ({})", p.name, mp.display())),
}
}
if !outside.is_empty() {
return Err(anyhow!(
"{} path package(s) of {} live outside {} — a flat sibling mount cannot carry them: {}",
outside.len(),
manifest.display(),
parent.display(),
outside.join(", ")
));
}
Ok(out.into_iter().collect())
}
#[cfg(test)]
mod scoped_name_tests {
use super::scoped_container_name;
/// Two checkouts of one row get two names; the same checkout and target keep
/// one (so the leftover of an interrupted run is still reaped by the next).
#[test]
fn a_build_container_name_is_scoped_to_its_checkout_and_target() {
let a = vec!["/home/u/a/holger:/work:ro".to_string(), "/home/u/t-a:/target".to_string(), "/home/u/.cargo/registry:/registry:ro".to_string()];
let b = vec!["/home/u/b/holger:/work:ro".to_string(), "/home/u/t-b:/target".to_string(), "/home/u/.cargo/registry:/registry:ro".to_string()];
let na = scoped_container_name("cargo-build-holger-server", &a);
let nb = scoped_container_name("cargo-build-holger-server", &b);
assert_ne!(na, nb, "two constellations, two container names");
assert!(na.starts_with("cargo-build-holger-server-") && na.len() == "cargo-build-holger-server-".len() + 8, "{na}");
let mut a2 = a.clone();
a2.reverse();
assert_eq!(scoped_container_name("cargo-build-holger-server", &a2), na, "the same place, the same name, whatever the bind order");
assert_ne!(scoped_container_name("cargo-build-other", &a), na, "another row, another name");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
// ── the override, the env export and the sibling closure (lane I, 2026-09-15) ──
/// A tag is not a pin on the override either; a digest override takes the
/// committed Containerfile's targets (uefi included) and keeps the bounds.
#[test]
fn the_image_override_takes_a_digest_and_the_images_own_targets() {
let mut declared = tc();
declared.targets = vec!["x86_64-unknown-linux-musl".into()];
declared.mem_limit_mb = 1234;
let e = format!("{:#}", apply_image_override(declared.clone(), "nordisk-cargo-toolchain:live-uefi").unwrap_err());
assert!(e.contains("not a pin"), "{e}");
let d = "3d5115baa07d03ca51eb3e44ebb86b4e64e679bd88f38e156ef9da12c35d2143";
let o = apply_image_override(declared, &format!("nordisk-cargo-toolchain@sha256:{d}")).unwrap();
assert_eq!(o.image_ref(), format!("nordisk-cargo-toolchain@sha256:{d}"));
assert!(o.targets.iter().any(|t| t == "x86_64-unknown-uefi"), "{:?}", o.targets);
assert_eq!(o.mem_limit_mb, 1234, "the bounds stay the declaration's");
o.validate().expect("the overridden declaration validates");
}
/// The env a build script reads reaches the container as quoted exports, and a
/// key that is not a shell name never reaches the script at all.
#[test]
fn the_invocation_env_is_exported_quoted_and_bad_keys_are_dropped() {
let mut env = RunEnv::new();
env.insert("KORP_BAKED_CMDLINE".into(), "korp.entry=gunnar it's".into());
env.insert("BAD;rm -rf".into(), "x".into());
let s = build_script_with_env(&tc(), "/work/edda", &["build".into()], None, &env, "s.json");
assert!(s.contains("export KORP_BAKED_CMDLINE='korp.entry=gunnar it'\\''s'\n"), "{s}");
assert!(!s.contains("BAD"), "{s}");
assert_eq!(build_script(&tc(), "/w", &["build".into()], None, "s.json"),
build_script_with_env(&tc(), "/w", &["build".into()], None, &RunEnv::new(), "s.json"));
}
/// edda → a → b: both siblings are mounted, a dep into a directory that is not
/// on the box is skipped, and the repo itself is not its own sibling.
#[test]
fn the_sibling_closure_follows_siblings_of_siblings() {
let root = scratch("closure");
let _ = std::fs::remove_dir_all(&root);
let w = |p: &str, deps: &str| {
let d = root.join(p);
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname=\"x\"\n[dependencies]\n{deps}")).unwrap();
};
w("edda/crates/k", "a = { path = \"../../../a\" }\n");
w("a", "b = { path = \"../b\" }\nk = { path = \"../edda/crates/k\" }\ngone = { path = \"../gone\" }\n");
w("b", "");
assert_eq!(sibling_closure(&root.join("edda")), vec!["a".to_string(), "b".to_string()]);
}
const PIN: &str = "0d8caf1a2b2312b1b76a7c26527b545ea9b5479bb95e2e97a6b3b0dcc4bd2f4c";
fn tc() -> CargoToolchain {
CargoToolchain { image_sha256: PIN.into(), ..CargoToolchain::default() }
}
fn set_with(v: &str) -> BuildVariantSet {
BuildVariantSet::from_toml_str(v).expect("parse")
}
/// A runner that records the request and answers with a canned stdout.
struct Fake {
seen: Mutex<Vec<CargoRunRequest>>,
out: CargoRunOutcome,
}
impl Fake {
fn new(out: CargoRunOutcome) -> Self {
Fake { seen: Mutex::new(Vec::new()), out }
}
}
impl CargoContainerRunner for Fake {
fn run(&self, req: &CargoRunRequest) -> Result<CargoRunOutcome> {
self.seen.lock().unwrap().push(req.clone());
Ok(self.out.clone())
}
}
/// A runner that also writes the stream file the way a real container does
/// — into the bound target dir, DURING the run, after the road has cleared
/// any previous one.
struct WritesStream {
inner: Fake,
at: PathBuf,
text: String,
}
impl CargoContainerRunner for WritesStream {
fn run(&self, req: &CargoRunRequest) -> Result<CargoRunOutcome> {
let out = self.inner.run(req)?;
std::fs::write(&self.at, &self.text).unwrap();
Ok(out)
}
}
/// A lane-private scratch dir with a REAL file standing in for the produced
/// binary. `BuildProvenance::capture` content-hashes what it is handed, so a
/// test that passes a path to nothing is testing the error path by accident.
fn scratch(sub: &str) -> PathBuf {
let d = std::env::temp_dir().join("nbt-cargo-container").join(sub);
std::fs::create_dir_all(&d).unwrap();
d
}
/// A host target dir holding `debug/v` (or the given relative path), so the
/// rewritten container path lands on a file that exists.
fn target_with(sub: &str, rel: &str) -> PathBuf {
let t = scratch(sub);
let f = t.join(rel);
std::fs::create_dir_all(f.parent().unwrap()).unwrap();
std::fs::write(&f, b"\x7fELF pretend binary").unwrap();
t
}
fn ok_stdout(exe: &str) -> CargoRunOutcome {
CargoRunOutcome {
exit_code: Some(0),
stdout: vec![
format!("{RUSTC_SENTINEL}rustc 1.97.1 (8bab26f4f 2026-07-14)"),
format!("{CARGO_SENTINEL}cargo 1.97.1 (c980f4866 2026-06-30)"),
format!("{HOST_SENTINEL}x86_64-unknown-linux-gnu"),
format!(
r#"{{"reason":"compiler-artifact","executable":"{exe}","target":{{"name":"t"}}}}"#
),
],
stderr: vec![],
}
}
// ── the declaration refuses what cannot be audited ───────────────────────
/// ★ RED-FIRST — **an unpinned build image is refused BY NAME.** Written
/// against a `validate()` that did not exist: the declaration parsed, the
/// build ran, and whatever `nordisk-cargo-toolchain:latest` happened to
/// point at that day compiled the artifact — two builds reporting the same
/// provenance out of two different environments.
#[test]
fn an_unpinned_toolchain_image_is_refused_by_name() {
let t = CargoToolchain::default();
let e = t.validate().expect_err("an unpinned image must be refused");
let m = format!("{e}");
assert!(m.contains("image_sha256"), "{m}");
assert!(m.contains("NOT a pin"), "the refusal says why: {m}");
}
#[test]
fn an_image_id_pasted_where_a_manifest_digest_belongs_is_refused() {
for bad in ["abc", &PIN.to_uppercase(), &format!("sha256:{PIN}"), &format!("{PIN}00")] {
let t = CargoToolchain { image_sha256: bad.to_string(), ..Default::default() };
let e = t.validate().expect_err("malformed digest must be refused");
assert!(format!("{e}").contains("64 lowercase hex"), "{e}");
}
}
#[test]
fn an_unbounded_container_is_refused_by_name() {
let t = CargoToolchain { mem_limit_mb: 0, ..tc() };
assert!(format!("{}", t.validate().unwrap_err()).contains("unbounded is not a bound"));
let t = CargoToolchain { cpus: 0, ..tc() };
assert!(format!("{}", t.validate().unwrap_err()).contains("unbounded is not a bound"));
let t = CargoToolchain { timeout_secs: 0, ..tc() };
assert!(format!("{}", t.validate().unwrap_err()).contains("hang"));
}
#[test]
fn the_digest_pin_becomes_a_name_at_sha256_reference() {
assert_eq!(tc().image_ref(), format!("{CARGO_TOOLCHAIN_IMAGE}@sha256:{PIN}"));
let unpinned = CargoToolchain::default();
assert_eq!(unpinned.image_ref(), CARGO_TOOLCHAIN_IMAGE, "no pin ⇒ the bare name");
}
/// ★ RED-FIRST — **the E0463 failure, named an hour earlier.** This is the
/// oden measurement turned into a refusal: a variant targeting musl against
/// an image that carries no musl std used to compile for however long it
/// took to reach `can't find crate for core`.
#[test]
fn a_target_the_image_does_not_carry_is_refused_before_the_build() {
let t = CargoToolchain {
targets: vec!["x86_64-unknown-linux-gnu".into()],
..tc()
};
let e = t
.check_target("gunnar-server", Some("x86_64-unknown-linux-musl"))
.expect_err("a target the image lacks must be refused");
let m = format!("{e}");
assert!(m.contains("gunnar-server") && m.contains("x86_64-unknown-linux-musl"), "{m}");
assert!(m.contains("E0463"), "the refusal names the failure it replaces: {m}");
}
#[test]
fn an_undeclared_target_list_accepts_anything() {
assert!(tc().check_target("v", Some("wasm32-unknown-unknown")).is_ok());
}
// ── the script ───────────────────────────────────────────────────────────
/// The airgapped run must be airgapped IN CARGO TOO. `--network none` stops
/// the socket; `--offline --locked` is what stops cargo from deciding it can
/// re-resolve — without them the run fails deep inside a resolver instead of
/// building the pinned graph.
#[test]
fn an_airgapped_run_is_offline_and_locked() {
let s = build_script(&tc(), "/work/repo", &["build".into(), "--release".into()], None, "s.jsonl");
assert!(s.contains("--locked"), "{s}");
assert!(s.contains("--offline"), "{s}");
assert!(s.contains(&format!("export CARGO_TARGET_DIR={TARGETDIR}")), "{s}");
assert!(s.contains("cd '/work/repo'"), "{s}");
let net = CargoToolchain { network: true, ..tc() };
let s = build_script(&net, "/work/repo", &["build".into()], None, "s.jsonl");
assert!(!s.contains("--offline"), "a networked run may resolve: {s}");
assert!(s.contains("--locked"), "…but never re-lock: {s}");
}
/// The image's toolchain wins over the repo's `rust-toolchain.toml`: a file
/// naming a component the image lacks must not send rustup to the network
/// (tunnr's korp-boot, loki 2026-09-26).
#[test]
fn the_image_toolchain_overrides_rust_toolchain_toml() {
let t = CargoToolchain { rust_version: "1.97.1".into(), ..tc() };
let s = build_script(&t, "/work/repo", &["build".into()], None, "s.jsonl");
assert!(s.contains(&format!("export RUSTUP_TOOLCHAIN='{}'\n", t.rust_version)), "{s}");
assert!(s.find("RUSTUP_TOOLCHAIN").unwrap() < s.find("cd '/work/repo'").unwrap(), "{s}");
}
/// Both sides remap to the SAME synthetic prefixes, which is the only reason
/// the byte-identity gate is comparing like with like.
#[test]
fn both_sides_remap_to_the_same_prefixes() {
let t = tc();
let c = t.container_rustflags();
let h = t.host_rustflags(
Path::new("/home/x/git/repo"),
Path::new("/home/x/.cargo"),
Path::new("/scratch/lane"),
);
assert!(c.contains(&format!("{WORKROOT}={REMAP_SRC}")), "{c}");
assert!(h.contains(&format!("/home/x/git={REMAP_SRC}")), "{h}");
assert!(c.contains(REMAP_REGISTRY) && h.contains(REMAP_REGISTRY), "{c} / {h}");
let off = CargoToolchain { remap_paths: false, ..tc() };
assert!(off.container_rustflags().is_empty());
}
#[test]
fn a_variant_cfg_rides_the_same_rustflags_the_host_path_uses() {
let s = build_script(&tc(), "/work/repo", &["build".into()], Some("holger_mvp"), "s.jsonl");
assert!(s.contains("--cfg holger_mvp"), "{s}");
assert!(s.contains(REMAP_SRC), "…alongside the remap, not instead of it: {s}");
}
#[test]
fn a_quote_in_an_argument_cannot_rewrite_the_command() {
let s = build_script(
&tc(),
"/work/repo",
&["build".into(), "--features".into(), "a'; rm -rf /; '".into()],
None,
"s.jsonl",
);
// The dangerous half is the CLOSING quote the injection tries to smuggle
// in. `'\''` closes, escapes a literal quote and reopens, so `rm -rf /`
// stays a substring of one argument and never becomes a command.
assert!(
s.contains(r"'a'\''; rm -rf /; '\'''"),
"the argument must survive as ONE single-quoted word: {s}"
);
assert!(
!s.contains("\n; rm") && !s.contains("' ; rm"),
"…and must not have escaped into command position: {s}"
);
}
#[test]
fn the_binds_are_a_workdir_its_output_and_the_pinned_sources() {
let b = tc().binds(
Path::new("/home/x/git/repo"),
Path::new("/scratch/target"),
Some(Path::new("/home/x/.cargo/registry")),
);
assert_eq!(b.len(), 3, "{b:?}");
assert_eq!(b[0], format!("/home/x/git/repo:{WORKROOT}/repo"));
assert_eq!(b[1], format!("/scratch/target:{TARGETDIR}"));
assert!(b[2].ends_with(":ro"), "the registry is READ-ONLY: {}", b[2]);
}
// ── the stdout parse ─────────────────────────────────────────────────────
/// ★ RED-FIRST — **the artifact path is rewritten back onto the host.**
/// cargo says `/target/release/foo` because that is the path IN THE
/// CONTAINER. Handing that to a caller makes it hash a file that does not
/// exist: a build that produced a binary reports zero artifacts, and the
/// byte-identity gate goes `Missing` on a run that actually succeeded.
#[test]
fn the_container_target_path_is_rewritten_onto_the_host() {
let p = parse_container_stdout(
&ok_stdout("/target/x86_64-unknown-linux-musl/release/gunnar").stdout,
Path::new("/scratch/lane"),
);
assert_eq!(
p.artifacts,
vec![PathBuf::from("/scratch/lane/x86_64-unknown-linux-musl/release/gunnar")]
);
assert_eq!(p.rustc, "rustc 1.97.1 (8bab26f4f 2026-07-14)");
assert_eq!(p.host, "x86_64-unknown-linux-gnu");
assert_eq!(p.units, 1);
}
#[test]
fn a_path_outside_the_target_mount_is_left_alone_not_guessed() {
let p = parse_container_stdout(
&[r#"{"reason":"compiler-artifact","executable":"/usr/local/bin/weird"}"#.to_string()],
Path::new("/scratch/lane"),
);
assert_eq!(p.artifacts, vec![PathBuf::from("/usr/local/bin/weird")]);
}
#[test]
fn compiler_errors_are_collected_and_a_malformed_line_does_not_abandon_the_run() {
let lines = vec![
"not json at all".to_string(),
"{ this is not json either".to_string(),
r#"{"reason":"compiler-message","message":{"level":"error","rendered":"error[E0463]: can't find crate for `core`"}}"#.to_string(),
r#"{"reason":"compiler-artifact","executable":"/target/debug/x"}"#.to_string(),
];
let p = parse_container_stdout(&lines, Path::new("/t"));
assert_eq!(p.errors.len(), 1);
assert!(p.errors[0].contains("E0463"));
assert_eq!(p.artifacts.len(), 1, "the artifact after the junk was still collected");
}
// ── the orchestration ────────────────────────────────────────────────────
#[test]
fn the_run_carries_the_pinned_image_the_bounds_and_the_variants_own_argv() {
let set = set_with(
"[build]\n[[build.variant]]\nname = \"srv\"\npackage = \"gunnar-server\"\n\
profile = \"release\"\ntarget = \"x86_64-unknown-linux-musl\"\n",
);
let fake = Fake::new(ok_stdout("/target/x86_64-unknown-linux-musl/release/gunnar-server"));
let repo = scratch("repo-srv");
let target =
target_with("target-srv", "x86_64-unknown-linux-musl/release/gunnar-server");
build_variant_in_container(
&set,
"srv",
&repo,
&target,
Some(Path::new("/home/x/.cargo/registry")),
&tc(),
&fake,
)
.expect("build");
let seen = fake.seen.lock().unwrap();
let req = seen.first().expect("the runner was called");
assert_eq!(req.image, format!("{CARGO_TOOLCHAIN_IMAGE}@sha256:{PIN}"));
assert!(!req.network, "the default run is airgapped");
assert_eq!(req.mem_limit_mb, DEFAULT_MEM_LIMIT_MB);
assert_eq!(req.cpus, DEFAULT_CPUS);
assert_eq!(req.timeout, Duration::from_secs(DEFAULT_TIMEOUT_SECS));
// The variant's OWN argv — same writer as the host path, so the two
// cannot drift into building different things.
assert!(req.script.contains("-p' 'gunnar-server"), "{}", req.script);
assert!(req.script.contains("--target' 'x86_64-unknown-linux-musl"), "{}", req.script);
assert!(req.script.contains("--profile' 'release"), "{}", req.script);
}
/// ★ RED-FIRST — **the provenance records the CONTAINER's rustc.** The
/// obvious implementation reuses `Toolchain::detect()`, which probes the
/// HOST — attesting that this box's rustc built a binary this box's rustc
/// never touched. A provenance that is precisely wrong is worse than none.
#[test]
fn the_provenance_attests_the_container_toolchain_not_the_hosts() {
let set = set_with("[build]\n[[build.variant]]\nname = \"v\"\n");
let mut out = ok_stdout("/target/debug/v");
out.stdout[0] = format!("{RUSTC_SENTINEL}rustc 9.9.9 (deadbeef 2099-01-01)");
let fake = Fake::new(out);
let o = build_variant_in_container(
&set,
"v",
&scratch("repo-prov"),
&target_with("target-prov", "debug/v"),
None,
&tc(),
&fake,
)
.expect("build");
assert_eq!(o.attestation.record.toolchain.rustc, "rustc 9.9.9 (deadbeef 2099-01-01)");
assert_ne!(
o.attestation.record.toolchain.rustc,
Toolchain::detect().rustc,
"this must NOT be the host's rustc"
);
}
/// A declared `rust_version` the image does not carry is a named refusal —
/// the check that catches a stale declaration even when the digest is valid.
#[test]
fn a_declaration_that_disagrees_with_the_image_is_refused_by_name() {
let set = set_with("[build]\n[[build.variant]]\nname = \"v\"\n");
let fake = Fake::new(ok_stdout("/target/debug/v"));
let t = CargoToolchain { rust_version: "1.90.0".into(), ..tc() };
let repo = scratch("repo-ver");
let target = target_with("target-ver", "debug/v");
let e = build_variant_in_container(&set, "v", &repo, &target, None, &t, &fake)
.expect_err("a version disagreement must refuse");
let m = format!("{e}");
assert!(m.contains("1.90.0") && m.contains("1.97.1"), "{m}");
}
/// ★ RED-FIRST — **a digest that cannot be resolved never degrades into the
/// tag.** The pin's whole value is that it is the ONLY thing the engine is
/// asked for; a road that answers "the digest did not resolve, I'll take the
/// tag" gives back exactly the drift the pin was bought to stop, and gives
/// it back silently, under a receipt that still names a digest.
///
/// Measured through the runner rather than asserted about a string, because
/// "no fallback" is a claim about what the ENGINE was asked to run. Two
/// halves, and both matter:
///
/// * a pinned declaration asks for `<name>@sha256:<digest>` and for nothing
/// else — the bare tag appears in no request;
/// * an UNPINNED declaration never reaches the runner at all. It is refused
/// by name first, so there is no request in which a tag could stand in.
/// Before `validate()` was called at the top of
/// [`build_variant_in_container`], this second half was the live hole:
/// `image_ref()` answers the bare name when the pin is empty (by design —
/// it has nothing else to say), and every caller that skipped validation
/// turned that into a tag-resolved build wearing pinned-build clothes.
#[test]
fn an_unresolvable_pin_is_never_silently_replaced_by_the_tag() {
let set = set_with("[build]\n[[build.variant]]\nname = \"v\"\n");
// Half one: pinned ⇒ the engine is asked for the digest, only.
let fake = Fake::new(ok_stdout("/target/debug/v"));
let repo = scratch("repo-pin");
let target = target_with("target-pin", "debug/v");
build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect("a pinned declaration builds");
let seen = fake.seen.lock().unwrap();
let asked = &seen.first().expect("the runner was asked to run").image;
assert_eq!(asked, &format!("{CARGO_TOOLCHAIN_IMAGE}@sha256:{PIN}"), "{asked}");
assert!(asked.contains('@'), "a bare tag reached the engine: {asked}");
drop(seen);
// Half two: unpinned ⇒ refused BY NAME, and the runner is never reached.
let fake = Fake::new(ok_stdout("/target/debug/v"));
let unpinned = CargoToolchain { image_sha256: String::new(), ..tc() };
let e = build_variant_in_container(&set, "v", &repo, &target, None, &unpinned, &fake)
.expect_err("an unresolvable pin must refuse, not fall back");
let m = format!("{e}");
assert!(m.contains("image_sha256") && m.contains("NOT a pin"), "{m}");
assert!(m.contains(CARGO_TOOLCHAIN_IMAGE), "the refusal names the image: {m}");
assert!(
fake.seen.lock().unwrap().is_empty(),
"the engine was asked to run something anyway — that is the fallback"
);
// Half three: a digest that is not a MANIFEST digest (an image ID pasted
// where a repo digest belongs is the one that actually happened) is the
// same refusal, not a shrug back onto the tag.
let fake = Fake::new(ok_stdout("/target/debug/v"));
let idish = CargoToolchain {
image_sha256: "72abf41e32e24d1d0423598f4040b72bddaee5c5a62213edb188a513f19be66".into(),
..tc()
};
let e = build_variant_in_container(&set, "v", &repo, &target, None, &idish, &fake)
.expect_err("a malformed digest must refuse");
assert!(format!("{e}").contains("64 lowercase hex"), "{e}");
assert!(fake.seen.lock().unwrap().is_empty(), "it ran anyway");
}
/// ★ RED-FIRST — **the build is read from the FILE the container wrote,
/// not from the engine's idea of what it printed.**
///
/// MEASURED on t14s 2026-09-12, rootless podman 5.7: 20 runs of a container
/// that printed 901 lines and exited returned `exit_code = Some(0)` with 0,
/// 0, 0 and 749 lines — and asking the daemon for the container's WHOLE log
/// afterwards, in one `follow=false` request while it still existed,
/// returned the same short answer. The lines were not late, they were gone.
/// Cargo's `--message-format=json` stream is the only thing that reports a
/// produced binary, so on this road the engine's log was a correctness
/// channel that silently drops data.
///
/// The fake runner here is the measured engine: it exits 0 and reports a
/// stdout that has lost the tail, while the container wrote the whole
/// stream to the bound target dir. The assertion is the ARTIFACT.
#[test]
fn the_stream_is_read_from_the_bound_file_when_the_engine_dropped_it() {
let set = set_with("[build]\n[[build.variant]]\nname = \"v\"\n");
let repo = scratch("repo-file");
let target = target_with("target-file", "debug/v");
// What the container really wrote, on the bind mount the host owns.
let whole = format!(
"{RUSTC_SENTINEL}rustc 1.97.1 (8bab26f4f 2026-07-14)\n\
{CARGO_SENTINEL}cargo 1.97.1 (c980f4866 2026-06-30)\n\
{HOST_SENTINEL}x86_64-unknown-linux-gnu\n\
{{\"reason\":\"compiler-artifact\",\"executable\":null,\"target\":{{\"name\":\"dep\"}}}}\n\
{{\"reason\":\"compiler-artifact\",\"executable\":\"{TARGETDIR}/debug/v\",\"target\":{{\"name\":\"v\"}}}}\n\
{{\"reason\":\"build-finished\",\"success\":true}}\n"
);
// The fake WRITES it during the run, the way the container does — the
// road removes any previous file first, so a test that wrote it up
// front would be testing nothing.
let fake = WritesStream {
inner: Fake::new(CargoRunOutcome {
exit_code: Some(0),
// What the engine says it printed: the tail is simply missing.
stdout: vec![format!("{RUSTC_SENTINEL}rustc 1.97.1 (8bab26f4f 2026-07-14)")],
stderr: vec![],
}),
at: target.join(cargo_stream_file("v")),
text: whole.clone(),
};
let o = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect("the file has the whole stream, so this is a real build");
assert_eq!(
o.artifacts,
vec![target.join("debug/v")],
"the build was judged on the engine's truncated stdout, not on the file"
);
assert_eq!(o.attestation.record.toolchain.cargo, "cargo 1.97.1 (c980f4866 2026-06-30)");
// And the script really does route cargo through that file, so the two
// halves of this cannot drift apart.
let req = fake.inner.seen.lock().unwrap().first().cloned().expect("the runner ran");
assert!(
req.script.contains(&format!("{TARGETDIR}/{}", cargo_stream_file("v"))),
"the script never wrote the stream anywhere the host can read: {}",
req.script
);
// A STALE file from a previous run must not be read as this build's
// evidence — it is removed before the run, so a container that writes
// nothing leaves nothing to find, and the judge refuses.
let stale = target.join(cargo_stream_file("v"));
std::fs::write(&stale, &whole).unwrap();
let silent =
Fake::new(CargoRunOutcome { exit_code: Some(0), stdout: vec![], stderr: vec![] });
let e = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &silent)
.expect_err("a stale stream file must not become this build's artifact");
assert!(format!("{e}").contains("no executable"), "{e}");
assert!(!stale.exists(), "the stale file survived the run");
}
/// ★ RED-FIRST — **the container road refuses its own false green.**
///
/// This is the appliance bug, MEASURED on t14s 2026-09-12 and reproduced
/// here at the seam: a run that exits 0 and hands back no `executable`.
/// Before the judge, this test's first half returned `Ok` with
/// `artifacts == []` and `success == true`, and the operator met the failure
/// two crates away as `chained variant 'gunnar-server' has kind 'Exe', which
/// produced no single artifact to stage`.
///
/// It asserts the ARTIFACT, never `success`: `success` is the exit code and
/// was ALREADY true in the broken run, so a guard resting on it is exactly
/// the check that could not fail.
#[test]
fn exit_zero_with_no_executable_is_refused_on_the_container_road() {
let set = set_with("[build]\n[[build.variant]]\nname = \"v\"\n");
let repo = scratch("repo-nothing");
let target = target_with("target-nothing", "debug/v");
// Half one: the TRANSPORT delivered nothing. 3 of 20 live runs of a
// chatty container came back exactly like this — exit 0, zero stdout
// lines — because the engine's log stream carries cargo's stdout here.
let fake =
Fake::new(CargoRunOutcome { exit_code: Some(0), stdout: vec![], stderr: vec![] });
let e = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect_err("exit 0 with nothing staged must refuse HERE");
let m = format!("{e}");
assert!(m.contains("no executable"), "{m}");
assert!(m.contains("0 compiler artifact"), "{m}");
assert!(m.contains("transport"), "an empty stream is named as a transport cause: {m}");
assert!(m.contains("bound target dir"), "the refusal names the channel it read: {m}");
assert!(m.contains(PIN), "the refusal names the image it ran in: {m}");
// Half two: the stream arrived and cargo really did build no binary.
let mut out = ok_stdout("/target/debug/v");
out.stdout.pop();
out.stdout.push(
r#"{"reason":"compiler-artifact","executable":null,"target":{"name":"t"}}"#.into(),
);
let fake = Fake::new(out);
let e = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect_err("a build with no binary target must refuse too");
let m = format!("{e}");
assert!(m.contains("1 compiler artifact"), "the unit count is real: {m}");
assert!(m.contains("no binary target"), "{m}");
// …and the green is still green, asserted on the PATH cargo reported —
// rewritten onto this host — and not on a boolean.
let fake = Fake::new(ok_stdout("/target/debug/v"));
let o = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect("a run that produced a binary builds");
assert_eq!(o.artifacts, vec![target.join("debug/v")], "the staged path is the host's");
}
#[test]
fn a_nonzero_exit_is_an_unsuccessful_outcome_not_an_error() {
let set = set_with("[build]\n[[build.variant]]\nname = \"v\"\n");
let mut out = ok_stdout("/target/debug/v");
out.exit_code = Some(101);
let fake = Fake::new(out);
let repo = scratch("repo-red");
let target = target_with("target-red", "debug/v");
let o = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect("a failed build is a VALUE, not an Err");
assert!(!o.success);
}
// ── the gate ─────────────────────────────────────────────────────────────
#[test]
fn the_gate_is_green_only_on_the_same_bytes() {
let dir = std::env::temp_dir().join("nbt-byte-identity");
std::fs::create_dir_all(&dir).unwrap();
let a = dir.join("a");
let b = dir.join("b");
let c = dir.join("c");
std::fs::write(&a, b"same").unwrap();
std::fs::write(&b, b"same").unwrap();
std::fs::write(&c, b"different").unwrap();
let v = byte_identity_verdict(&a, &b);
assert!(v.is_identical(), "{v:?}");
assert!(v.line("v").contains("DONE"), "{}", v.line("v"));
let v = byte_identity_verdict(&a, &c);
assert!(!v.is_identical(), "{v:?}");
assert!(matches!(v, ByteIdentity::Differs { .. }));
assert!(v.line("v").contains("as dåligt"), "{}", v.line("v"));
}
/// ★ **A non-reproducible build is not the container's fault, and the gate
/// must not say it is — nor go green.**
///
/// MEASURED 2026-08-26 on `gunnar-server`: every build came out different.
/// `gunnar/crates/gunnar-common/build.rs:43` runs
/// `date -u +%Y-%m-%dT%H:%M:%SZ` and bakes it as `GUNNAR_BUILD_DATE`; the
/// host and container artifacts carried `2026-08-25T22:43:39Z` and
/// `2026-08-25T22:44:47Z`. A deliberate identity stamp, at second resolution
/// off the wall clock — so two builds one second apart differ, on one box or
/// two.
///
/// Without the control the gate reports `Differs` and the reader concludes
/// the container forked the build. It did not. The blame belongs to the
/// repo, and it is still a red.
#[test]
fn a_build_that_is_not_reproducible_at_all_is_named_as_such_and_not_blamed_on_the_container() {
let dir = scratch("control");
let (a, b, host, cont) =
(dir.join("a"), dir.join("b"), dir.join("h"), dir.join("c"));
std::fs::write(&a, b"stamped 22:43:39").unwrap();
std::fs::write(&b, b"stamped 22:44:47").unwrap();
std::fs::write(&host, b"stamped 22:43:39").unwrap();
std::fs::write(&cont, b"stamped 22:44:47").unwrap();
let v = byte_identity_with_control(&a, &b, &host, &cont, "GUNNAR_BUILD_DATE");
assert!(!v.is_identical(), "a non-reproducible build is never green: {v:?}");
assert!(matches!(v, ByteIdentity::NotReproducible { .. }), "{v:?}");
let line = v.line("gunnar-server");
assert!(line.contains("NOT the container"), "{line}");
assert!(line.contains("GUNNAR_BUILD_DATE"), "…and names the cause: {line}");
// With a reproducible control, the SAME comparison is answerable again —
// the control gates the question, it does not replace it.
std::fs::write(&b, b"stamped 22:43:39").unwrap();
let v = byte_identity_with_control(&a, &b, &host, &cont, "");
assert!(matches!(v, ByteIdentity::Differs { .. }), "now it IS the container: {v:?}");
std::fs::write(&cont, b"stamped 22:43:39").unwrap();
assert!(byte_identity_with_control(&a, &b, &host, &cont, "").is_identical());
}
/// ★ **A missing side is a RED, never a pass.** The tempting shape is
/// `host_sha == container_sha` over two `Option`s, where `None == None` is
/// `true` — a gate that goes green loudest exactly when neither build
/// produced anything.
#[test]
fn a_missing_artifact_is_red_and_not_a_vacuous_green() {
let dir = std::env::temp_dir().join("nbt-byte-identity");
std::fs::create_dir_all(&dir).unwrap();
let there = dir.join("a");
std::fs::write(&there, b"same").unwrap();
let gone = dir.join("does-not-exist");
let v = byte_identity_verdict(&there, &gone);
assert!(!v.is_identical(), "{v:?}");
assert!(matches!(v, ByteIdentity::Missing { .. }), "{v:?}");
let v = byte_identity_verdict(&gone, &gone);
assert!(!v.is_identical(), "neither side existing is the STRONGEST red: {v:?}");
}
/// The embedded Containerfile is the crate's own, and it states the law it
/// obeys. A build image whose provenance is "someone's Dockerfile" is the
/// thing this constant exists to prevent.
#[test]
fn the_committed_containerfile_states_the_host_free_law_and_names_every_step() {
let cf = CARGO_TOOLCHAIN_CONTAINERFILE;
assert!(cf.contains("NOTHING else"), "the host-free law is stated");
assert!(cf.contains("E0463"), "…and the measurement it closes is named");
for step in ["step: COMPILE", "step: LINK", "step: FETCH", "step: PROVENANCE"] {
assert!(cf.contains(step), "every package names its STEP; missing `{step}`");
}
assert!(
cf.contains("musl-tools"),
"the musl C toolchain is IN the image, not on the host"
);
// mold appears ONLY as the named absence. The host's
// `~/.cargo/config.toml` adds `-C link-arg=-fuse-ld=mold` for the gnu
// target, and a host linker choice leaking into a container build is
// precisely the drift this image removes — so the file says so, and
// never installs it.
assert!(cf.contains("mold is"), "the absent host linker is named: {cf}");
assert!(
!cf.lines().any(|l| l.trim_start().starts_with("mold")),
"…and never installed as a package"
);
}
/// `HostPaths` is cargo's own ladder in cargo's own order, and it does NOT
/// invent a third place to look.
#[test]
fn the_host_paths_are_resolved_once_and_from_cargos_own_ladder() {
// SAFETY-of-intent: these two are read-only lookups; the test sets them
// for the process and asserts the resolution, not the environment.
let repo = Path::new("/home/x/git/repo");
unsafe {
std::env::set_var("CARGO_TARGET_DIR", "/scratch/lane");
std::env::set_var("CARGO_HOME", "/home/x/.cargo");
}
let p = HostPaths::from_env(repo);
assert_eq!(p.target_dir, PathBuf::from("/scratch/lane"));
assert_eq!(p.registry, PathBuf::from("/home/x/.cargo/registry"));
unsafe {
std::env::remove_var("CARGO_TARGET_DIR");
}
let p = HostPaths::from_env(repo);
assert_eq!(p.target_dir, repo.join("target"), "the conventional fallback");
assert_eq!(
p.registry_bind(),
None,
"a registry that is not on this box is not bound, and that is not an error"
);
}
/// **THE GATE, MEASURED — both arms, on t14s 2026-08-26.**
///
/// This records what the live run produced, because the numbers are the
/// argument and a prose claim of "reproducible" is not:
///
/// | arm | host sha256 | container sha256 | verdict |
/// |---|---|---|---|
/// | host build with NO remap | `ae9e570e2ade15ed…` (610 688 B) | `7b85469ae1c483b7…` (610 696 B) | **Differs** |
/// | host build with `host_rustflags` | `7b85469ae1c483b7…` (610 696 B) | `7b85469ae1c483b7…` (610 696 B) | **Identical** |
///
/// Same crate, same variant (`--release --target x86_64-unknown-linux-musl`,
/// 11 crates.io dependencies compiled from the read-only registry bind with
/// `--network none --offline --locked`), same rustc 1.97.1 on both sides.
/// The ONLY difference in the red arm is the paths rustc baked in — 8 bytes,
/// the length delta between `~/…/redproof` and `/work`.
///
/// So: **byte-identity is reachable, and it is reachable only because both
/// sides remap.** Had the gate been run without
/// [`host_rustflags`](CargoToolchain::host_rustflags) it would have gone red
/// on a difference the COMPARISON introduced, and the honest response to
/// that red would have been to weaken the gate. The two halves of the remap
/// are therefore a load-bearing part of the gate, not a convenience — which
/// is what this test pins.
#[test]
fn the_two_remap_halves_describe_the_same_rewrite_from_both_sides() {
let t = tc();
let repo = Path::new("/home/alice/git/gunnar");
let home = Path::new("/home/alice/.cargo");
let target = Path::new("/scratch/lane");
let (c, h) = (t.container_rustflags(), t.host_rustflags(repo, home, target));
// Both name the SAME two destinations. A gate whose two sides remap to
// different synthetic prefixes compares two rewrites, not two builds.
for dest in [REMAP_SRC, REMAP_REGISTRY, REMAP_TARGET] {
assert_eq!(
c.matches(dest).count(),
1,
"container side must remap to {dest} exactly once: {c}"
);
assert_eq!(
h.matches(dest).count(),
1,
"host side must remap to {dest} exactly once: {h}"
);
}
// …from the paths each side actually has.
assert!(c.contains(WORKROOT) && !c.contains("/home/alice"), "{c}");
assert!(h.contains("/home/alice/git=") && !h.contains(WORKROOT), "{h}");
}
// ── per-row siblings (2026-09-28: holger's plugin row must not need gunnar) ──
/// A constellation shaped like monetize-impl's: the repo `mi` path-deps into
/// `gunnar` (NOT checked out here), and its `hg` row builds `../holger`, which
/// path-deps `edda`. The repo-wide list is the union; the row's own list is
/// holger's closure.
fn row_constellation(sub: &str) -> PathBuf {
let root = scratch(sub);
let _ = std::fs::remove_dir_all(&root);
let w = |rel: &str, text: &str| {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
};
w("mi/crates/g/Cargo.toml", "[package]\nname = \"g\"\n[dependencies]\ngx = { path = \"../../../gunnar/gx\" }\n");
w("holger/Cargo.toml", "[package]\nname = \"hg\"\n[dependencies]\nex = { path = \"../edda/ex\" }\n");
w("edda/ex/Cargo.toml", "[package]\nname = \"ex\"\n");
// holger's own excluded workspace (holger-estate-front's shape): cargo
// building holger never loads it, so its reach is not the row's.
w("holger/front/Cargo.toml", "[package]\nname = \"front\"\n[workspace]\n[dependencies]\nui = { path = \"../../holger-ui/ui\" }\n");
root
}
const ROW_SET: &str = "[build]\n\
[[build.variant]]\nname = \"v\"\nmanifest_dir = \"../holger\"\nsiblings = [\"holger\", \"edda\"]\n\
[[build.variant]]\nname = \"w\"\n";
/// ★ The row's own `siblings` REPLACE the repo-wide list: holger's row builds
/// with no gunnar checkout on the box, mounts holger and edda only, and the
/// escape check reads holger's manifests, not the repo's.
#[test]
fn a_row_with_its_own_siblings_builds_without_the_repo_wide_union() {
let root = row_constellation("row-siblings");
let repo = root.join("mi");
let set = set_with(ROW_SET);
let union = CargoToolchain {
siblings: vec!["edda".into(), "gunnar".into(), "holger".into()],
..tc()
};
let fake = Fake::new(ok_stdout("/target/debug/v"));
let target = target_with("row-siblings-target", "debug/v");
build_variant_in_container(&set, "v", &repo, &target, None, &union, &fake)
.expect("holger's row needs no gunnar checkout");
let seen = fake.seen.lock().unwrap();
let binds = &seen[0].binds;
assert!(binds.iter().any(|b| b.ends_with(&format!("{WORKROOT}/holger:ro"))), "{binds:?}");
assert!(binds.iter().any(|b| b.ends_with(&format!("{WORKROOT}/edda:ro"))), "{binds:?}");
assert!(!binds.iter().any(|b| b.contains("gunnar")), "gunnar mounted for holger's row: {binds:?}");
assert!(!binds.iter().any(|b| b.contains("holger-ui")), "a nested workspace's reach was mounted: {binds:?}");
drop(seen);
// The row WITHOUT its own list keeps the repo-wide one — and on this box
// that is refused by name, because gunnar is not checked out.
let e = build_variant_in_container(&set, "w", &repo, &target, None, &union, &fake)
.expect_err("the union needs gunnar");
assert!(format!("{e:#}").contains("gunnar"), "{e:#}");
}
/// ★ **A row with its own manifest root runs cargo IN that root.** cargo finds
/// `.cargo/config.toml` from the directory it runs in, never from
/// `--manifest-path`. MEASURED 2026-10-23 on dwarf: monetize-impl's
/// `holger-guest` ran in `/work/monetize-impl` with `--manifest-path
/// ../holger/Cargo.toml`, so holger's sibling patches (`.cargo/siblings.toml`,
/// included by its config) never loaded and the build answered "no matching
/// package named `appliance-grow`".
#[test]
fn a_row_with_its_own_manifest_root_runs_cargo_there() {
let root = row_constellation("row-cwd");
let repo = root.join("mi");
let set = set_with(ROW_SET);
let fake = Fake::new(ok_stdout("/target/debug/v"));
let target = target_with("row-cwd-target", "debug/v");
build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake).expect("the row builds");
let seen = fake.seen.lock().unwrap();
let script = &seen[0].script;
assert!(script.contains(&format!("cd '{WORKROOT}/holger'")), "{script}");
assert!(script.contains("'--manifest-path' 'Cargo.toml'"), "{script}");
assert!(!script.contains("../holger/Cargo.toml"), "{script}");
}
/// ★ An ABSENT declared sibling is refused with WHERE TO CLONE IT FROM: every
/// forge base the neighbouring checkouts' `origin`s name (tunnr is mikronova/,
/// tunnr-mullvad is nordisk/ — 2026-10-16 on loki), never one guessed from this
/// repo's own remote alone.
#[test]
fn an_absent_sibling_names_where_to_clone_it_from() {
let root = scratch("absent-sibling-hint");
let _ = std::fs::remove_dir_all(&root);
let w = |rel: &str, text: &str| {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
};
w("tunnr/Cargo.toml", "[package]\nname = \"tunnr\"\n");
w("tunnr/.git/config", "[core]\n\tbare = false\n[remote \"origin\"]\n\turl = ssh://git@forge:2222/mikronova/tunnr\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n");
w("edda/.git/config", "[remote \"upstream\"]\n\turl = https://elsewhere/x/edda\n[remote \"origin\"]\n\turl = ssh://git@forge:2222/nordisk/edda.git\n");
let t = CargoToolchain { siblings: vec!["edda".into(), "tunnr-mullvad".into()], ..tc() };
let m = format!("{:#}", t.check_escaping_path_deps(&root.join("tunnr")).expect_err("tunnr-mullvad is absent"));
assert!(m.contains("tunnr-mullvad"), "{m}");
let want = root.join("tunnr-mullvad");
for base in ["ssh://git@forge:2222/nordisk", "ssh://git@forge:2222/mikronova"] {
assert!(m.contains(&format!("git clone {base}/tunnr-mullvad {}", want.display())), "missing {base}: {m}");
}
assert!(!m.contains("elsewhere"), "only `origin` is a source: {m}");
}
/// ★ **Where an absent sibling is cloned from, best first**: the repo's own host
/// and namespace, then the other namespaces its neighbours come from on THAT host
/// — never a base on another host, never a sibling already there.
#[test]
fn an_absent_siblings_sources_are_the_repos_own_namespace_then_the_neighbours() {
let root = scratch("absent-sibling-urls");
let _ = std::fs::remove_dir_all(&root);
let w = |rel: &str, text: &str| {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
};
let origin = |url: &str| format!("[remote \"origin\"]\n\turl = {url}\n");
w("tunnr/.git/config", &origin("ssh://git@forge:2222/mikronova/tunnr"));
w("edda/.git/config", &origin("ssh://git@forge:2222/nordisk/edda.git"));
w("far/.git/config", &origin("https://elsewhere/x/far"));
w("plain/README", "not a checkout\n");
let urls = sibling_clone_urls(&root.join("tunnr"), "tunnr-mullvad");
assert_eq!(urls, ["ssh://git@forge:2222/mikronova/tunnr-mullvad", "ssh://git@forge:2222/nordisk/tunnr-mullvad"]);
let t = CargoToolchain { siblings: vec!["edda".into(), "tunnr-mullvad".into(), "plain".into()], ..tc() };
let absent: Vec<String> = t.absent_siblings(&root.join("tunnr")).into_iter().map(|(n, _)| n).collect();
assert_eq!(absent, ["tunnr-mullvad"], "a dir that is there — a checkout or not — is never cloned over");
}
/// A row's list that misses what ITS manifests reach is refused by name —
/// the scan follows the row's manifest root.
#[test]
fn a_row_list_missing_its_own_reach_is_refused_by_name() {
let root = row_constellation("row-siblings-missing");
let repo = root.join("mi");
let set = set_with(
"[build]\n[[build.variant]]\nname = \"v\"\nmanifest_dir = \"../holger\"\nsiblings = [\"holger\"]\n",
);
let fake = Fake::new(ok_stdout("/target/debug/v"));
let target = target_with("row-siblings-missing-target", "debug/v");
let e = build_variant_in_container(&set, "v", &repo, &target, None, &tc(), &fake)
.expect_err("edda is reached and not listed");
let m = format!("{e:#}");
assert!(m.contains("siblings = [\"edda\"]"), "{m}");
assert!(fake.seen.lock().unwrap().is_empty(), "the container was started anyway");
}
/// The measured closure: `cargo metadata --locked` of `a` loads `b` (a path
/// dep into a sibling checkout) and `a` itself — and nothing the tree merely
/// mentions in a nested workspace.
#[test]
fn the_metadata_closure_is_what_cargo_loads() {
let root = scratch("metadata-closure");
let _ = std::fs::remove_dir_all(&root);
let w = |rel: &str, text: &str| {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
};
w("a/Cargo.toml", "[package]\nname = \"a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n[workspace]\nexclude = [\"nested\"]\n[dependencies]\nb = { path = \"../b/b\" }\n");
w("a/src/lib.rs", "");
w("a/Cargo.lock", "version = 4\n\n[[package]]\nname = \"a\"\nversion = \"0.1.0\"\ndependencies = [\n \"b\",\n]\n\n[[package]]\nname = \"b\"\nversion = \"0.1.0\"\n");
w("a/nested/Cargo.toml", "[package]\nname = \"n\"\nversion = \"0.1.0\"\n[workspace]\n[dependencies]\nc = { path = \"../../c\" }\n");
w("b/b/Cargo.toml", "[package]\nname = \"b\"\nversion = \"0.1.0\"\nedition = \"2021\"\n");
w("b/b/src/lib.rs", "");
let c = metadata_sibling_closure(&root.join("a/Cargo.toml"), &root).expect("cargo metadata");
assert_eq!(c, ["a", "b"]);
assert!(
workspace_escaping_path_deps(&root.join("a")).iter().all(|(_, d, _)| d != "c"),
"the nested workspace's dep was scanned"
);
assert!(escaping_path_deps(&root.join("a")).iter().any(|(_, d, _)| d == "c"), "the whole-tree scan is unchanged");
}
/// `for_variant` touches the sibling list only, and only when the row says so.
#[test]
fn for_variant_replaces_only_the_siblings() {
let set = set_with(ROW_SET);
let t = CargoToolchain { siblings: vec!["gunnar".into()], mem_limit_mb: 77, ..tc() };
let v = t.for_variant(set.require("v").unwrap());
assert_eq!(v.siblings, ["holger", "edda"]);
assert_eq!((v.image_ref(), v.mem_limit_mb), (t.image_ref(), 77));
assert_eq!(t.for_variant(set.require("w").unwrap()), t);
assert_eq!(
variant_manifest_root(Path::new("/c/mi"), set.require("v").unwrap()),
PathBuf::from("/c/holger")
);
assert_eq!(variant_manifest_root(Path::new("/c/mi"), set.require("w").unwrap()), PathBuf::from("/c/mi"));
}
/// The key round-trips, asks for schema 6, and a path or a duplicate is refused.
#[test]
fn row_siblings_parse_emit_and_validate() {
let set = set_with(ROW_SET);
let v = set.require("v").unwrap();
set.validate(v).expect("valid");
let text = crate::emit::recipe_toml("# t", &set).expect("emits and reads back");
assert!(text.contains("siblings = [\n \"holger\",") || text.contains("siblings = [\"holger\", \"edda\"]"), "{text}");
assert!(text.contains("schema = 6"), "{text}");
let (n, why) = crate::schema::required_by(&set);
assert_eq!(n, 6, "{why:?}");
assert!(why.iter().any(|w| w.contains("siblings") && w.contains("'v'")), "{why:?}");
for bad in ["[\"../edda\"]", "[\"edda\", \"edda\"]", "[\"\"]"] {
let s = set_with(&format!("[build]\n[[build.variant]]\nname = \"v\"\nsiblings = {bad}\n"));
assert!(s.validate(s.require("v").unwrap()).is_err(), "{bad} was accepted");
}
}
/// Build a fake checkout: `<root>/gunnar` with a crate whose manifest reaches
/// `../../../znippy/git-storage-trait`, plus `<root>/znippy` beside it. This
/// is gunnar's real shape, reduced to two files.
fn fake_checkout(sub: &str) -> PathBuf {
let root = scratch(sub);
let _ = std::fs::remove_dir_all(&root);
let crate_dir = root.join("gunnar/crates/gunnar-store");
std::fs::create_dir_all(&crate_dir).unwrap();
std::fs::create_dir_all(root.join("znippy/git-storage-trait")).unwrap();
std::fs::write(
crate_dir.join("Cargo.toml"),
"[package]\nname = \"gunnar-store\"\n\
[dependencies]\n\
git-storage-trait = { version = \"0.1.0\", path = \"../../../znippy/git-storage-trait\" }\n\
serde = \"1\"\n\
[[bin]]\nname = \"x\"\npath = \"src/main.rs\"\n",
)
.unwrap();
root
}
/// ★ RED-FIRST, MEASURED 2026-08-26 — **a single repo bind cannot build this
/// constellation, and cargo's failure named the wrong thing.**
///
/// Binding gunnar alone at `/work` and running its `gunnar-server` variant in
/// the real image gave, in 2.7 s:
///
/// ```text
/// error: failed to load manifest for workspace member `/work/crates/gunnar-store`
/// Caused by: failed to read `/znippy/git-storage-trait/Cargo.toml`
/// ```
///
/// Ten path dependencies in gunnar walk out of its own tree into `edda` and
/// `znippy`. cargo's message names a path that does not exist, and neither
/// the manifest line that asked for it nor the one word that fixes it. This
/// is that failure, named before the container starts.
#[test]
fn a_path_dep_escaping_into_an_undeclared_sibling_is_refused_by_name() {
let root = fake_checkout("escape");
let repo = root.join("gunnar");
let e = tc()
.check_escaping_path_deps(&repo)
.expect_err("an undeclared sibling must be refused, not discovered at runtime");
let m = format!("{e}");
assert!(m.contains("znippy"), "the refusal names the SIBLING: {m}");
assert!(m.contains("siblings = [\"znippy\"]"), "…and the fix, spelled: {m}");
assert!(m.contains("gunnar-store"), "…and the manifest that caused it: {m}");
// Declared ⇒ green, and the bind that makes it true is present.
let ok = CargoToolchain { siblings: vec!["znippy".into()], ..tc() };
ok.check_escaping_path_deps(&repo).expect("a declared sibling is fine");
let binds = ok.binds(&repo, Path::new("/scratch/t"), None);
assert_eq!(binds[0], format!("{}:{WORKROOT}/gunnar", repo.display()));
assert!(
binds.iter().any(|b| b.ends_with(&format!("{WORKROOT}/znippy:ro"))),
"the sibling is mounted BESIDE the repo, read-only: {binds:?}"
);
// …and from /work/gunnar/crates/gunnar-store the dep's own
// `../../../znippy/…` lands inside that mount. The depth is the fix.
assert_eq!(ok.container_repo_dir(&repo), format!("{WORKROOT}/gunnar"));
}
/// ★ **A detached crate in the repo demands nothing of the repo's own rows** (the 5.3.2
/// receipt on the box, 2026-10-09): monetize-impl carries `gunnar-front-commercial`, its own
/// `[workspace]`, path-depending on gunnar; monetize-impl's rows never load it, and its
/// toolchain was refused "Add `siblings = [\"gunnar\"]`". The repo-wide pre-flight reads the
/// repo's ONE workspace; the detached crate is checked as its own workspace, by the rows that
/// build it, against their own list.
#[test]
fn a_detached_crate_in_the_repo_demands_no_sibling_of_the_repos_own_rows() {
let root = fake_checkout("escape-detached");
let repo = root.join("gunnar");
let ok = CargoToolchain { siblings: vec!["znippy".into()], ..tc() };
ok.check_escaping_path_deps(&repo).expect("the repo as it is");
let d = repo.join("crates/front-commercial");
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"fc\"\nversion = \"0.0.0\"\n[workspace]\n[dependencies]\nother = { path = \"../../../other/crates/x\" }\n").unwrap();
ok.check_escaping_path_deps(&repo).expect("the detached crate's dep is not the repo's rows' business");
// The rows that BUILD the detached crate see its dep, and need its sibling declared.
let m = format!("{}", ok.check_escaping_path_deps_from(&repo, &d).unwrap_err());
assert!(m.contains("siblings = [\"other\"]"), "{m}");
let built = CargoToolchain { siblings: vec!["other".into()], ..tc() };
let _ = std::fs::create_dir_all(root.join("other"));
built.check_escaping_path_deps_from(&repo, &d).expect("declared for the row that builds it");
// A crate of the repo's OWN workspace still demands its sibling.
let own = repo.join("crates/own");
std::fs::create_dir_all(&own).unwrap();
std::fs::write(own.join("Cargo.toml"), "[package]\nname = \"o\"\nversion = \"0.0.0\"\n[dependencies]\nother = { path = \"../../../other/crates/x\" }\n").unwrap();
let m = format!("{}", ok.check_escaping_path_deps(&repo).unwrap_err());
assert!(m.contains("siblings = [\"other\"]"), "the repo's own crates are read as before: {m}");
}
/// A declared sibling that is not checked out is named HERE, where the
/// reader is already looking at the list — not as a bind-mount failure from
/// the engine, which names a path and has never heard of `siblings`.
#[test]
fn a_declared_sibling_that_is_not_checked_out_is_refused_by_name() {
let root = fake_checkout("escape-absent");
let repo = root.join("gunnar");
let t = CargoToolchain { siblings: vec!["znippy".into(), "nope".into()], ..tc() };
let m = format!("{}", t.check_escaping_path_deps(&repo).unwrap_err());
assert!(m.contains("nope"), "the missing one is named: {m}");
assert!(!m.contains("znippy ("), "…and the present one is not: {m}");
assert!(m.contains("bind mount would fail"), "…with the consequence: {m}");
}
/// A dep reaching ABOVE the shared parent is refused separately: a flat
/// sibling mount cannot reproduce that layout, so `siblings` would not help
/// and implying it would is worse than the refusal.
#[test]
fn a_path_dep_reaching_above_the_parent_is_a_different_refusal() {
let root = fake_checkout("escape-high");
let repo = root.join("gunnar");
std::fs::write(
repo.join("crates/gunnar-store/Cargo.toml"),
"[package]\nname = \"g\"\n[dependencies]\n\
far = { path = \"../../../../../../../../../opt/far\" }\n",
)
.unwrap();
let m = format!("{}", tc().check_escaping_path_deps(&repo).unwrap_err());
assert!(m.contains("ABOVE the repo's parent"), "{m}");
assert!(m.contains("unbounded slice"), "…and why that cannot be mounted: {m}");
}
/// The scan uses a real TOML parser, not a regex over the text: `path` also
/// spells `[[bin]]` and `[lib]`, and a scan that cannot tell those apart
/// reports dependencies that do not exist. The fixture's
/// `[[bin]] path = "src/main.rs"` and its registry `serde` must both be
/// invisible to it.
#[test]
fn the_scan_reads_dependency_tables_and_not_every_key_named_path() {
let root = fake_checkout("escape-parse");
let found = escaping_path_deps(&root.join("gunnar"));
assert_eq!(found.len(), 1, "exactly the one escaping DEPENDENCY: {found:?}");
assert_eq!(found[0].1, "git-storage-trait");
assert!(found[0].2.ends_with("znippy/git-storage-trait"), "{:?}", found[0].2);
}
/// ★ **The real adopted declaration loads**, with every field reaching the
/// struct. gunnar is the first repo to declare `[build.toolchain]`, and this
/// pins the shape it actually wrote — including the five siblings measured off
/// its own manifests — against the now-closed schema. A key this struct did not
/// know used to be dropped in silence; `siblings` typo'd to `sibling` would
/// have mounted nothing and failed an hour later inside cargo.
#[test]
fn the_declaration_shape_a_repo_actually_wrote_parses_whole() {
let set = BuildVariantSet::from_toml_str(
"[build]\n[build.toolchain]\n\
image = \"nordisk-cargo-toolchain\"\n\
image_sha256 = \"f29e517c1ba34b920c1234fbd628046d64c0f1072d85ae7dee52b5cce054a917\"\n\
rust_version = \"1.97.1\"\n\
targets = [\"x86_64-unknown-linux-gnu\", \"x86_64-unknown-linux-musl\"]\n\
mem_limit_mb = 8192\ncpus = 8\ntimeout_secs = 3600\nnetwork = false\n\
siblings = [\"edda\", \"znippy\", \"draupnir\", \"tunnr\", \"znippy-zoomies\"]\n\
[[build.variant]]\nname = \"srv\"\ntarget = \"x86_64-unknown-linux-musl\"\n",
)
.expect("gunnar's real declaration must load");
let t = set.toolchain.clone().expect("the table reached the struct");
t.validate().expect("and it is a valid, pinned environment");
assert_eq!(t.siblings.len(), 5, "{:?}", t.siblings);
assert!(!t.network, "airgapped");
t.check_target("srv", Some("x86_64-unknown-linux-musl")).expect("declared target");
// …and a misspelling is a refusal, not a silent drop.
let e = BuildVariantSet::from_toml_str(
"[build]\n[build.toolchain]\nimage = \"x\"\nsibling = [\"edda\"]\n",
)
.expect_err("`sibling` is not `siblings`");
assert!(format!("{e:#}").contains("sibling"), "{e:#}");
}
/// ★ **The image's target set is READ, not restated** — and the WASM road is
/// in it.
///
/// The refusal that opened this lane, verbatim from the chained build on oden
/// 2026-08-27: `variant gunnar-build-thing-wasm targets
/// wasm32-unknown-unknown, which the toolchain image nordisk-cargo-toolchain
/// does not carry std for (it declares: x86_64-unknown-linux-gnu,
/// x86_64-unknown-linux-musl)`. Correct then; this asserts the fix it named.
#[test]
fn the_containerfile_names_the_four_roads_it_installs_std_for() {
let carried = cargo_toolchain_image_targets();
assert_eq!(
carried,
vec![
"x86_64-unknown-linux-gnu".to_string(),
"x86_64-unknown-linux-musl".to_string(),
"wasm32-unknown-unknown".to_string(),
"x86_64-unknown-uefi".to_string(),
],
"read out of ARG RUST_TARGETS, which is the only thing that installs a std"
);
// …and the parse is not a constant: a Containerfile with no such ARG
// yields NOTHING, so a rename cannot leave this returning the old list.
assert!(
CARGO_TOOLCHAIN_CONTAINERFILE.contains("ARG RUST_TARGETS="),
"the parse anchors on this exact spelling"
);
// The hollow check it replaced must be GONE from every EXECUTED line —
// it survives only as the prose recording why (`# What stood here was`).
let hollow: Vec<&str> = CARGO_TOOLCHAIN_CONTAINERFILE
.lines()
.filter(|l| !l.trim_start().starts_with('#'))
.filter(|l| l.contains("--print target-list"))
.collect();
assert!(
hollow.is_empty(),
"target-list prints 322 triples in an image carrying 3 stds — it can never go \
red, and it is still RUN here: {hollow:?}"
);
assert!(
CARGO_TOOLCHAIN_CONTAINERFILE.contains("--print target-libdir"),
"the image asserts the libdir rustup target add actually creates"
);
}
/// ★ RED-FIRST on both sides. A declaration may not name a triple the image
/// cannot build — that lie converts a named refusal into a bare `E0463` an
/// hour into a compile, which is the whole failure mode this table exists to
/// remove.
#[test]
fn a_declared_target_the_image_cannot_build_is_refused_by_name() {
// RED: the three carried triples plus one nobody installed.
let mut t = tc();
t.targets = vec![
"x86_64-unknown-linux-musl".into(),
"wasm32-unknown-unknown".into(),
"aarch64-unknown-linux-musl".into(),
];
let e = format!("{:#}", t.check_declared_targets().expect_err("must refuse"));
assert!(e.contains("aarch64-unknown-linux-musl"), "the offender is NAMED: {e}");
assert!(!e.contains("(wasm32"), "…and the carried ones are not blamed: {e}");
assert!(e.contains("E0463"), "…with the failure it prevents: {e}");
// The same refusal must come out of `validate`, or the door has no caller.
let e = format!("{:#}", t.validate().expect_err("validate carries it"));
assert!(e.contains("aarch64-unknown-linux-musl"), "{e}");
// GREEN, and not vacuously: every carried triple passes, INCLUDING the
// wasm32 one that was a refusal before this lane.
let mut ok = tc();
ok.targets = cargo_toolchain_image_targets();
ok.check_declared_targets().expect("the image's own list must pass");
ok.validate().expect("and validate must accept it");
ok.check_target("gunnar-build-thing-wasm", Some("wasm32-unknown-unknown"))
.expect("the WASM road the chained build refused BY NAME is now declarable");
// An UNDECLARED list stays permissive, and a foreign image is not guessed
// at — two silent-refusal traps this must not spring.
let mut undeclared = tc();
undeclared.targets.clear();
undeclared.check_declared_targets().expect("empty is undeclared, not none");
let mut foreign = tc();
foreign.image = "registry.example/some-other-toolchain".into();
foreign.targets = vec!["riscv64gc-unknown-none-elf".into()];
foreign
.check_declared_targets()
.expect("this crate does not build that image and must not claim to know it");
}
/// ★ RED-FIRST, and it cost a full image build to learn. The per-package
/// rationale rides `` `# …` `` — a shell command substitution whose output is
/// empty, which is how a `RUN apt-get install` line carries prose. A backtick
/// INSIDE that prose closes the substitution early, and the rest of the
/// comment becomes arguments to `apt-get install`. MEASURED 2026-08-25:
/// three such comments (quoting `` `failed to find tool …` ``,
/// `` `-C link-arg=-fuse-ld=mold` `` and `` `git = ` ``) made the whole image
/// build die with `while running runtime: exit status 2` — a 30-line error
/// that names the RUN step and not the backtick.
///
/// The rule the file must hold: on any line carrying a backtick comment,
/// backticks come in exactly one pair.
#[test]
fn no_comment_in_the_containerfile_nests_a_backtick() {
for (i, line) in CARGO_TOOLCHAIN_CONTAINERFILE.lines().enumerate() {
let n = line.matches('`').count();
if n == 0 || !line.trim_start().starts_with('`') {
continue;
}
assert_eq!(
n,
2,
"line {} has {n} backticks — a nested one closes the `# …` \
substitution early and the prose becomes apt-get arguments: {line}",
i + 1
);
}
}
/// The EXECUTED lines of a Containerfile — comments and blanks dropped —
/// because a rule about what the image DOES must not be satisfied by prose
/// recording why it once did something else.
fn executed(cf: &str) -> Vec<&str> {
cf.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
/// ★ **The TEST image is the toolchain image plus exactly the tools a
/// test road needs — and it is built ON the toolchain, not beside it.**
///
/// The danger this guards is not a missing package; it is the test image
/// drifting into a second build environment. A suite that passes in an
/// image whose rustc, targets or libc differ from the one the artefact was
/// compiled in has measured something else.
#[test]
fn the_test_image_is_the_toolchain_plus_the_test_road_and_nothing_else() {
let test = executed(CARGO_TEST_CONTAINERFILE);
// It is FROM the toolchain, through the ARG the builder pins.
let from = test
.iter()
.find(|l| l.starts_with("FROM "))
.expect("a Containerfile has a FROM");
assert_eq!(
*from, "FROM ${CARGO_TOOLCHAIN_REF}",
"the test image must be built on the toolchain image, not on its own base: {from}"
);
assert!(
test.iter().any(|l| l.starts_with(CARGO_TEST_BASE_ARG)),
"there is no `{CARGO_TEST_BASE_ARG}` line for the builder to pin the base through"
);
// The three tools, each of which unblocks a named set of tests.
for tool in ["openssh-client", "git-lfs", "cargo-deny", "libssl-dev", "rustup component add rustfmt clippy"] {
assert!(
test.iter().any(|l| l.contains(tool)),
"the test image does not carry {tool}"
);
}
// And NOT a compiler environment of its own: the moment this file
// installs rust, sets RUSTUP_HOME or adds a target, there are two
// toolchains and the suite stops witnessing the build.
// `rustup component add` is allowed: it adds rustfmt/clippy to the
// toolchain the image is FROM, and installs no rustc and no target.
for forbidden in [
"rustup toolchain",
"rustup default",
"rustup override",
"rustup-init",
"RUSTUP_HOME",
"RUSTUP_TOOLCHAIN",
"rustup target add",
"ARG RUST_TARGETS",
"ARG RUST_VERSION",
"--toolchain",
] {
assert!(
!test.iter().any(|l| l.contains(forbidden)),
"the test image defines its own toolchain ({forbidden}); it must inherit the \
one it is FROM, or a suite can pass against a rustc no artefact was built with"
);
}
// The snapshot and the epoch are the BASE's, not restated here — one
// writer for the instant, per the base's own reproducibility section.
assert!(
!test.iter().any(|l| l.starts_with("ENV SOURCE_DATE_EPOCH=")),
"the test image restates SOURCE_DATE_EPOCH; it must inherit the base's, or the \
two can drift"
);
}
/// ★ **The base is pinned by rewriting one line, and a file that lost the
/// line is refused rather than built on a tag.**
#[test]
fn the_test_containerfile_takes_the_pin_it_is_handed() {
let pinned = cargo_test_containerfile_on("nordisk-cargo-toolchain@sha256:deadbeef")
.expect("the committed file has the ARG");
assert!(
pinned.contains("ARG CARGO_TOOLCHAIN_REF=nordisk-cargo-toolchain@sha256:deadbeef"),
"the pin did not reach the file"
);
// Exactly one line changed, and the default is GONE — a leftover tag
// default under a pinned build is the ambiguity this closes.
assert!(
!pinned.contains("ARG CARGO_TOOLCHAIN_REF=nordisk-cargo-toolchain:live"),
"the tag default survived the pin"
);
assert_eq!(
pinned.lines().count(),
CARGO_TEST_CONTAINERFILE.lines().count(),
"the rewrite added or dropped lines"
);
}
/// ★ **cargo-deny is pinned by version AND by digest, and the two sides of
/// the context boundary check the same one.**
///
/// A prebuilt binary fetched over the network into an image everything
/// else trusts is exactly where an unpinned dependency hides, so the pin
/// is read from the Containerfile (one writer), the host verifies what it
/// fetched, and the image verifies what it was handed.
#[test]
fn the_cargo_deny_pin_is_read_from_the_containerfile_and_checked_twice() {
let (version, sha) = cargo_test_cargo_deny_pin().expect("the file pins cargo-deny");
assert!(!version.is_empty(), "no version");
assert_eq!(sha.len(), 64, "a sha256 is 64 hex characters: {sha:?}");
assert!(
sha.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"the pin is not lowercase hex: {sha:?}"
);
// The URL is derived from the same version, so a bump cannot leave the
// fetch pointing at the old release.
let url = cargo_deny_url(&version);
assert!(url.contains(&version), "the URL does not name the pinned version: {url}");
assert!(
url.ends_with("x86_64-unknown-linux-musl.tar.gz"),
"the fetched artefact must be the STATIC musl build, or the image gains a glibc \
dependency the base may not satisfy: {url}"
);
// The image checks the same value it was pinned with.
assert!(
CARGO_TEST_CONTAINERFILE.contains("sha256sum -c -"),
"the image does not verify the staged tarball"
);
assert!(
CARGO_TEST_CONTAINERFILE.contains("${CARGO_DENY_SHA256} /tmp/cargo-deny.tar.gz"),
"the image verifies against something other than the pin"
);
}
/// ★ **The two toolchain Containerfiles pin ONE world, and the cargo one
/// pins it at all.**
///
/// SEEN RED 2026-09-05 (lane bevis), the state this test was written
/// against: `grep -c '^ENV SOURCE_DATE_EPOCH=' cargo-toolchain.Containerfile`
/// → 0, `FROM debian:trixie` with no digest, apt against whatever the mirror
/// said that day, and `build_toolchain_image` calling the plain `build_image`
/// — while the kernel file had all four moves since 2026-08-30. gunnar moved
/// its `[build.toolchain] image_sha256` pin twice that day chasing the
/// re-mints. Every assert below fails against that tree.
///
/// An OCI build has no include, so the two files must state the base digest,
/// the snapshot instant and the epoch separately. This is the guard that
/// makes the duplication safe: they may not disagree. Drift in either file
/// alone — a base bumped in one, a snapshot moved in the other — fails
/// here by name rather than as two toolchains quietly built from two
/// Debians.
#[test]
fn the_two_toolchains_pin_one_world() {
use crate::kernel::{kernel_toolchain_source_date_epoch, KERNEL_TOOLCHAIN_CONTAINERFILE};
let cargo = executed(CARGO_TOOLCHAIN_CONTAINERFILE);
let kernel = executed(KERNEL_TOOLCHAIN_CONTAINERFILE);
// The BASE: one digest, not a tag someone can move. Executed lines only —
// the prose above the FROM legitimately quotes the bare tag as history.
let from = |lines: &[&str]| -> String {
lines
.iter()
.find(|l| l.starts_with("FROM "))
.map(|l| l.to_string())
.expect("a Containerfile has a FROM")
};
let (cf, kf) = (from(&cargo), from(&kernel));
assert!(
cf.contains("@sha256:"),
"the cargo toolchain's base is a moving tag, not a digest: {cf}"
);
assert_eq!(cf, kf, "the two toolchains are built on two different Debians");
// The ARCHIVE: the same snapshot instant, or apt installs from two
// different days and the packages differ under one `FROM`.
let snapshot = |lines: &[&str]| -> Option<String> {
lines.iter().find_map(|l| {
let i = l.find("snapshot.debian.org/archive/debian/")?;
let rest = &l[i..];
let end = rest.find('\'').unwrap_or(rest.len());
Some(rest[..end].to_string())
})
};
let cs = snapshot(&cargo).expect("the cargo toolchain does not pin apt to a snapshot");
let ks = snapshot(&kernel).expect("the kernel toolchain pins apt to a snapshot");
assert_eq!(cs, ks, "the two toolchains install from two different snapshot instants");
// The CLOCK: one epoch, read from each file's own line by the one parser,
// and the cargo file's line is the one the value came from.
let ce = cargo_toolchain_source_date_epoch()
.expect("the cargo toolchain Containerfile declares the instant it is minted at");
let ke = kernel_toolchain_source_date_epoch().expect("the kernel one already did");
assert_eq!(ce, ke, "the two toolchains are minted at two different instants");
assert!(
cargo.iter().any(|l| *l == format!("ENV SOURCE_DATE_EPOCH={ce}")),
"the epoch must be the Containerfile's own EXECUTED line, not a second constant"
);
// …and the clock's fingerprints are scrubbed in an executed line, not
// merely described. Each of these files carries a wall-clock stamp.
for stamp in ["/var/lib/apt/lists", "/var/log/apt", "/var/cache/ldconfig/aux-cache"] {
assert!(
cargo.iter().any(|l| l.contains("rm") && l.contains(stamp)),
"{stamp} is left in the layer that made it — its mtime moves the image id"
);
}
}
}
/// Which names a toolchain mint may write — driven over a fake engine store, no
/// socket. The store models what podman does with names: a tag names exactly one
/// image, a mint WRITES its tag (moving it off whatever it named), and
/// `name@sha256:<manifest>` resolves only while some tag under `name` still
/// names that image. That last rule is the 2026-10-05 `403`.
#[cfg(test)]
mod toolchain_mint_tests {
use super::*;
use std::cell::RefCell;
const NAME: &str = "nordisk-cargo-toolchain";
/// The pinned image gunnar and holger build in (id → manifest digest).
const PINNED_ID: &str = "id-3d5115ba";
const PINNED_DIGEST: &str = "3d5115baa07d03ca51eb3e44ebb86b4e64e679bd88f38e156ef9da12c35d2143";
/// What THIS crate's recipe mints (the clang + llvm recipe).
const RECIPE_ID: &str = "id-88c4902987c2";
const RECIPE_DIGEST: &str = "668468e775ad80f32d700cbc9709d41cac726f40d00649461a0932f30d1b2067";
struct FakeStore {
/// tag → image id
tags: RefCell<BTreeMap<String, String>>,
/// image id → manifest digest
images: RefCell<BTreeMap<String, String>>,
/// every tag a mint wrote, in order
mints: RefCell<Vec<String>>,
The file goes on; the console shows all of it.