crates/nornir-build-thing/src/image_build.rs
//! **Image / VM build RUNNER + provenance sealer** — the IMAGE-target peer of
//! [`build_variant`](crate::provenance::build_variant).
//!
//! `build_variant` runs a `cargo build` and seals provenance over the produced
//! executables. This module is its sibling for the *other* two build target
//! kinds — [`ContainerBuild`](crate::container::ContainerBuild) images and
//! [`VmBuild`](crate::vm::VmBuild) appliances — which had NO build+seal peer.
//! tunnr flagged the gap: consumers fell back to
//! [`ContainerBuild::content_id`](crate::container::ContainerBuild::content_id) —
//! a *descriptor* hash — because nothing sealed provenance over the actually
//! **produced artifact** (the built image's content digest, or the kernel+rootfs
//! sha256).
//!
//! # What it seals
//!
//! A [`TargetProvenance`] binds the **produced-artifact** hash, not just the
//! descriptor fingerprint:
//!
//! * **container** — the image's engine-reported content digest (`sha256:…`).
//! * **vm** — the sha256 of each produced kernel / rootfs / disk file.
//!
//! The [`TargetProvenance::descriptor_id`] still records the descriptor's own
//! `content_id`, so a reader can tell the SPEC hash (what was asked for) from the
//! ARTIFACT hash (what came out). [`TargetAttestation`] folds the record to a
//! SHA-256 digest and seals it with the same [`ProvenanceSealer`] seam
//! `build_variant` uses — so a tampered artifact digest breaks the seal.
//!
//! # Seam-based / dep-light default
//!
//! The build itself rides a **seam** so the default crate stays dependency-light:
//!
//! * [`ImageBuilder`] — actually builds a container image and returns its digest.
//! The heavy real impl [`BollardImageBuilder`] (feature `image-build`) drives
//! podman/Docker over the REST API (bollard, zero-shell); tests inject a fake.
//! * [`VmImageBuilder`] — the VM handoff. The heavy Buildroot kernel/rootfs build
//! stays **tunnr's**; the dep-light [`PrebuiltVmBuilder`] seals provenance over
//! the kernel+rootfs/disk artifacts tunnr already produced (the blessed "hash
//! the produced artifacts if present" path).
//!
//! ```no_run
//! use nornir_build_thing::image_build::{build_vm, PrebuiltVmBuilder};
//! use nornir_build_thing::VmBuild;
//! # fn main() -> anyhow::Result<()> {
//! let vm = VmBuild { name: "appliance".into(), kernel: "artifacts/bzImage".into(),
//! rootfs: "artifacts/rootfs.cpio.gz".into(), ..Default::default() };
//! let att = build_vm(&PrebuiltVmBuilder::new(), &vm, "tunnr", std::path::Path::new("."))?;
//! // `att` binds the produced kernel+rootfs sha256 — seal it with a ProvenanceSealer.
//! println!("{}", att.attestation.digest_hex());
//! # Ok(()) }
//! ```
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::container::ContainerBuild;
use crate::iso::IsoBuild;
use crate::provenance::{
self, Artifact, BuildProvenance, HostInfo, Inputs, Outcome, ProvenanceSealer, Seal, Toolchain,
};
use crate::variant::BuildVariant;
use crate::vm::VmBuild;
/// Schema tag stamped into every image/vm provenance record.
pub const TARGET_PROVENANCE_SCHEMA: &str = "nornir.target-provenance/v1";
/// Which build target kind produced the artifacts a [`TargetProvenance`] seals.
///
/// This was a bespoke `{Container, Vm}` enum — a strict SUBSET of the suite's
/// output-kind axis. It is now an alias for the first-class
/// [`ArtifactKind`](crate::variant::ArtifactKind); image/vm provenance records
/// only ever carry the `Container` / `Vm` members. `ArtifactKind`'s string serde
/// yields `"container"` / `"vm"` — byte-identical to the old
/// `#[serde(rename_all = "snake_case")]` output — so existing sealed
/// target-provenance JSON keeps deserializing.
///
/// Kept as a deprecated alias for external callers that import the old name.
pub use crate::variant::ArtifactKind as TargetKind;
/// The identity of a **built container image** an [`ImageBuilder`] hands back —
/// the produced artifact, not the descriptor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuiltImage {
/// The image reference (primary tag) that was built.
pub image_ref: String,
/// The image content digest reported by the engine (e.g. `sha256:abcd…`).
pub digest: String,
/// Image size in bytes (0 when unknown).
pub bytes: u64,
/// The exported distributable archive, when the descriptor declared
/// [`ContainerBuild::archive`] — a real file artifact beside the in-storage
/// image. `None` when no archive was requested.
pub archive: Option<Artifact>,
}
impl BuiltImage {
/// Fold the built image into a produced-artifact [`Artifact`]: path = the image
/// reference, sha256 = the engine digest with any `sha256:` prefix stripped.
pub fn to_artifact(&self) -> Artifact {
let sha = self.digest.strip_prefix("sha256:").unwrap_or(&self.digest).to_ascii_lowercase();
Artifact { path: self.image_ref.clone(), bytes: self.bytes, sha256: sha }
}
/// Every produced artifact: the image identity, plus the exported archive
/// file when one was declared. THE list [`build_container`] records — so an
/// archive can never be produced yet missing from the record, or recorded yet
/// never produced (one writer).
pub fn to_artifacts(&self) -> Vec<Artifact> {
let mut v = vec![self.to_artifact()];
v.extend(self.archive.clone());
v
}
}
/// The **container image build seam**: actually build a [`ContainerBuild`] and
/// return the produced image's identity (digest). The dep-light default crate ships
/// only the trait; the heavy real impl is [`BollardImageBuilder`] (feature
/// `image-build`), tests inject a fake.
pub trait ImageBuilder {
/// Build `image` in `repo_root` (the build context root) and return the built
/// image's [`BuiltImage`] (ref + content digest).
fn build(&self, image: &ContainerBuild, repo_root: &Path) -> Result<BuiltImage>;
/// The engine label recorded in provenance — `bollard` for the real
/// [`BollardImageBuilder`], a fake's own label in tests. Defaulted so existing
/// impls need no change, but read by [`build_container`] so the record names the
/// engine that actually ran (not a hard-coded literal).
fn engine(&self) -> &str {
"bollard"
}
/// **Every name the built image answers to** — its image id and each manifest digest,
/// `sha256:`-less. What [`build_container`] compares a pin against: a pin may be either
/// (`toolchain_archive` pins image ids, the kernel rows manifest digests). Defaulted to the
/// one digest [`BuiltImage`] carries, for a builder that knows no more.
fn identities(&self, built: &BuiltImage) -> Result<Vec<String>> {
Ok(vec![crate::kernel::normalize_sha256(&built.digest)])
}
/// **Apply `tags` to the image built as `built`** — the second half of a pinned build,
/// run only after the image matched its pin. Defaulted to nothing, for a builder that
/// applies no tags of its own (a fake; a prebuilt resolver).
fn tag(&self, built: &BuiltImage, tags: &[String]) -> Result<()> {
let _ = (built, tags);
Ok(())
}
/// **Drop the tag `reference`, never the image under another name.** Used to take a pinned
/// build's staging tag off again, match or not.
fn untag(&self, reference: &str) -> Result<()> {
let _ = reference;
Ok(())
}
/// **The pinned image, when the engine ALREADY holds it under every declared tag**
/// (2026-10-13). `Some` only when each of `image.tags` names an image that answers to
/// `pin`. Asked ONLY of an engine that [`cannot_mint_reproducibly`](Self::cannot_mint_reproducibly);
/// an engine that can is never asked, because there a mint is the recipe-drift gate.
/// Defaulted to `None`, for a builder that cannot look.
fn held(&self, image: &ContainerBuild, pin: &str) -> Result<Option<BuiltImage>> {
let _ = (image, pin);
Ok(None)
}
/// **The engine, named, when it cannot mint a pinned recipe as its pin** — read from the
/// engine itself ([`engine_that_cannot_reproduce`]), never assumed. `None` (the default,
/// and the answer for an engine that could not be asked) keeps the rule every pinned
/// build has had since 2026-09-22: always mint, always compare.
fn cannot_mint_reproducibly(&self) -> Option<String> {
None
}
}
/// The libpod major [`reproducible_build_uri`] targets (`/v5.0.0/libpod/build`): the first
/// whose build door honours `timestamp` the way the pins were minted.
pub const REPRODUCIBLE_LIBPOD_MAJOR: u32 = 5;
/// **Is this an engine that cannot reproduce a pinned recipe?** Pure, over the `/version`
/// components the engine answered (`(name, version)`): `Some("podman 4.9.3")` for a Podman
/// Engine below [`REPRODUCIBLE_LIBPOD_MAJOR`], `None` for one at or above it — and `None` for
/// anything that is not recognisably podman or whose version does not parse, so an unknown
/// engine keeps minting and comparing rather than being excused.
///
/// MEASURED 2026-10-13: podman 4.9.3 (buildah 1.33.7) answers `ea2c07ca` for the kernel
/// toolchain recipe podman 5.7 mints as `3f632569`/`78eaa55a` — no `force-mtime` on its
/// layers, and an `io.buildah.version` label in the config.
pub fn engine_that_cannot_reproduce(components: &[(String, String)]) -> Option<String> {
let (_, version) = components.iter().find(|(name, _)| name == "Podman Engine")?;
let major: u32 = version.trim().split('.').next()?.parse().ok()?;
(major < REPRODUCIBLE_LIBPOD_MAJOR).then(|| format!("podman {}", version.trim()))
}
/// The words a held pinned image is recorded with — in the provenance record's `engine` and
/// on the step's own line — so a release can tell a MINTED pin from a COURIERED one.
pub fn held_evidence(engine: &str) -> String {
format!("held, not minted on this engine ({engine})")
}
/// The tag a PINNED image is built under before it is checked (2026-09-22): never one a
/// reader resolves, so an image that does not match its pin is never reachable by any tag
/// the declaration names.
pub fn staging_tag(image: &str) -> String {
format!("{image}:unverified-pin-check")
}
/// **The minting instant a container cell's recipe states**, or `None` when it states none.
///
/// A FILE-mode cell whose Containerfile carries `ENV SOURCE_DATE_EPOCH=<seconds>` answers that
/// number (parsed by the one parser, [`crate::kernel::source_date_epoch_of`], so a malformed
/// value is refused by name rather than dropped); a Containerfile without the line, and every
/// synthesized cell, answers `None` and builds as it always did.
pub fn source_date_epoch_of_cell(image: &ContainerBuild, repo_root: &Path) -> Result<Option<i64>> {
if !image.is_file_mode() {
return Ok(None);
}
let path = repo_root.join(image.containerfile.trim());
let text = std::fs::read_to_string(&path)
.with_context(|| format!("container '{}': reading {}", image.name, path.display()))?;
if !text.lines().any(|l| l.trim().starts_with("ENV SOURCE_DATE_EPOCH=")) {
return Ok(None);
}
crate::kernel::source_date_epoch_of(&text).map(Some)
}
/// **The build request, at the recipe's instant.** The Docker-compat `/build` that bollard
/// sends has no `timestamp`; podman's own `libpod/build` does. Rewritten to it with
/// `layers=true`, `timestamp=<epoch>` and the OCI output format — the request draupnir's
/// `build_image_reproducible` sends, which is what `podman build --timestamp <epoch>`
/// does. Every other request, and a `/build` already on `libpod`, is left alone (`None`).
pub fn reproducible_build_uri(uri: &str, epoch: i64) -> Option<String> {
let (scheme, rest) = uri.split_once("://")?;
let (authority, path_query) = rest.split_once('/').map(|(a, p)| (a, format!("/{p}")))?;
let (path, query) = match path_query.split_once('?') {
Some((p, q)) => (p.to_string(), Some(q.to_string())),
None => (path_query.clone(), None),
};
if !path.ends_with("/build") || path.contains("/libpod/") {
return None;
}
let q = query.map(|q| format!("{q}&")).unwrap_or_default();
Some(format!(
"{scheme}://{authority}/v5.0.0/libpod/build?{q}layers=true×tamp={epoch}&outputformat=application%2Fvnd.oci.image.manifest.v1%2Bjson"
))
}
/// Does `identities` (image id and manifest digests of a built image) include `pin`?
pub fn built_matches_pin(identities: &[String], pin: &str) -> bool {
let pin = crate::kernel::normalize_sha256(pin);
!pin.is_empty() && identities.iter().any(|i| crate::kernel::normalize_sha256(i) == pin)
}
/// A PINNED build: build under [`staging_tag`], check against the pin, and only on a match
/// apply the declared tags. On a mismatch nothing the declaration names moves and the build
/// FAILS by name.
///
/// **The one exception is an engine that says it cannot reproduce**
/// ([`ImageBuilder::cannot_mint_reproducibly`], 2026-10-13). There a mint can only ever fail,
/// so the pinned image must already be in the store under the declared tags (couriered):
/// it is taken as HELD — nothing minted, no tag moved, the outcome says so
/// ([`held_evidence`]) — and when it is not there the build fails naming the two fixes. An
/// engine that CAN reproduce is never excused: it mints and compares even when the store
/// already holds the pin, which is what catches a changed recipe with a forgotten pin.
fn build_pinned(builder: &dyn ImageBuilder, image: &ContainerBuild, repo_root: &Path) -> Result<(BuiltImage, Option<String>)> {
let pin = crate::kernel::normalize_sha256(&image.expected_sha256);
if let Some(engine) = builder.cannot_mint_reproducibly() {
let fixes = format!(
"courier the pinned image into this box's store under {} (`podman save --format oci-archive \
--uncompressed <image> | podman load` keeps its digest), or build on a podman \
{REPRODUCIBLE_LIBPOD_MAJOR} builder",
image.tags.join(", "),
);
// An exported archive is a mint's business, and this engine's mint is never the pin.
if !image.archive.trim().is_empty() {
anyhow::bail!(
"'{}' declares an exported archive ({}) and this engine ({engine}) cannot mint the \
pinned image {} reproducibly, so it has nothing verified to export — build on a \
podman {REPRODUCIBLE_LIBPOD_MAJOR} builder",
image.name,
image.archive.trim(),
pin.get(..12).unwrap_or(&pin),
);
}
return match builder.held(image, &pin)? {
Some(held) => Ok((held, Some(held_evidence(&engine)))),
None => Err(anyhow::anyhow!(
"the pinned image for '{}' ({}) is not in this box's store, and this engine \
({engine}) cannot mint it: its builder does not reproduce a pinned recipe. {fixes}",
image.name,
pin.get(..12).unwrap_or(&pin),
)),
};
}
let staging = staging_tag(&image.name);
let mut staged = image.clone();
staged.tags = vec![staging.clone()];
let mut built = builder.build(&staged, repo_root)?;
let ids = builder.identities(&built);
let ids = match ids {
Ok(ids) => ids,
Err(e) => {
let _ = builder.untag(&staging);
return Err(e.context(format!("reading back what {staging} names")));
}
};
if !built_matches_pin(&ids, &pin) {
builder.untag(&staging).with_context(|| format!("taking the staging tag {staging} off again"))?;
anyhow::bail!(
"the image built for '{}' is NOT the pinned one: it answers to {} and the pin is {} — \
its tags ({}) were NOT moved. The recipe or an input changed since the pin was taken, \
or the engine's layer cache answered with an older build; re-pin deliberately or \
mint the pinned image (`toolchain_archive`), never by retagging",
image.name,
ids.iter().map(|i| i.get(..12).unwrap_or(i)).collect::<Vec<_>>().join(" / "),
pin.get(..12).unwrap_or(&pin),
image.tags.join(", "),
);
}
builder.tag(&built, &image.tags).with_context(|| format!("tagging the verified image {}", image.tags.join(", ")))?;
builder.untag(&staging).with_context(|| format!("taking the staging tag {staging} off again"))?;
if let Some(primary) = image.primary_tag() {
built.image_ref = primary.to_string();
}
Ok((built, None))
}
/// The **ISO build seam**: produce (or locate) an [`IsoBuild`]'s mastered `.iso`
/// artifact and return its content hash. Real ISO-9660 mastering is done in **pure
/// Rust** by [`PureRustIsoBuilder`] (no external ISO tool); the default
/// [`PrebuiltIsoBuilder`] hashes an ISO **already produced** on disk (the blessed
/// "hash the produced artifact" path, mirroring [`PrebuiltVmBuilder`]).
pub trait IsoImageBuilder {
/// Resolve the mastered `.iso` for `iso` under `repo_root` and return its
/// [`Artifact`] hash (plus any staged `from_variant` payload).
fn build(&self, iso: &IsoBuild, repo_root: &Path) -> Result<Vec<Artifact>>;
/// The engine label recorded in provenance — `prebuilt` for the dep-light
/// [`PrebuiltIsoBuilder`], `pure-rust` for the [`PureRustIsoBuilder`] masterer.
/// Defaulted so existing impls need no change.
fn engine(&self) -> &str {
"prebuilt"
}
}
/// The **VM build seam**: produce (or locate) a [`VmBuild`]'s kernel + rootfs/disk
/// artifacts and return their content hashes. The heavy Buildroot build stays
/// tunnr's; the default [`PrebuiltVmBuilder`] hashes the artifacts tunnr already
/// produced.
pub trait VmImageBuilder {
/// Resolve the produced kernel + rootfs/disk artifacts for `vm` under
/// `repo_root` and return their [`Artifact`] hashes (in kernel, rootfs, disk
/// order for whichever are present).
fn build(&self, vm: &VmBuild, repo_root: &Path) -> Result<Vec<Artifact>>;
/// The engine label recorded in provenance — `prebuilt` for the dep-light
/// [`PrebuiltVmBuilder`], `tunnr` for a real `build.sh` handoff impl. Defaulted
/// so existing impls need no change; read by [`build_vm`] so a real VM build is
/// not mislabeled `prebuilt`.
fn engine(&self) -> &str {
"prebuilt"
}
}
/// A build-provenance record for a container/vm target — the image/vm analogue of
/// [`BuildProvenance`](crate::provenance::BuildProvenance). Fully serde; the seal
/// binds to the PRODUCED-artifact hashes in [`Self::artifacts`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetProvenance {
/// Always [`TARGET_PROVENANCE_SCHEMA`].
pub schema: String,
/// Stable per-record identity (UUID v4).
pub id: String,
/// The repo whose target was built.
pub repo: String,
/// The build target kind.
pub kind: TargetKind,
/// The target's stable name (image/vm name).
pub target: String,
/// The DESCRIPTOR's own `content_id` — the SPEC fingerprint (what was asked
/// for), kept distinct from the produced-artifact hashes below.
pub descriptor_id: String,
/// The full target descriptor, verbatim (a `ContainerBuild` / `VmBuild`).
pub descriptor: serde_json::Value,
/// The PRODUCED artifacts, content-addressed — the image digest, or the
/// kernel+rootfs/disk sha256s. THIS is what the seal binds to.
pub artifacts: Vec<Artifact>,
/// The builder engine that produced the artifact (`bollard`, `prebuilt`, …).
pub engine: String,
/// The build host (cores + memory).
pub host: HostInfo,
/// VCS commit the build was cut from.
pub vcs_rev: String,
/// VCS branch (best-effort).
pub vcs_branch: String,
/// `true` when the working tree had uncommitted changes at build time.
pub vcs_dirty: bool,
/// Build outcome.
pub outcome: Outcome,
/// RFC 3339 UTC timestamp of capture.
pub built_at: String,
/// Best-effort `user@host` of the builder.
pub builder: String,
}
impl TargetProvenance {
/// Capture a record for a built target from its descriptor + produced artifacts.
fn capture(
kind: TargetKind,
target: &str,
descriptor_id: String,
descriptor: serde_json::Value,
repo: &str,
repo_root: &Path,
engine: &str,
artifacts: Vec<Artifact>,
outcome: Outcome,
) -> Self {
let (vcs_rev, vcs_branch) = provenance::git_head(repo_root);
let (vcs_dirty, _worktree) = provenance::git_worktree_freshness(repo_root);
TargetProvenance {
schema: TARGET_PROVENANCE_SCHEMA.to_string(),
id: crate::provenance::record_id(),
repo: repo.to_string(),
kind,
target: target.to_string(),
descriptor_id,
descriptor,
artifacts,
engine: engine.to_string(),
host: HostInfo::detect(),
vcs_rev,
vcs_branch,
vcs_dirty,
outcome,
built_at: chrono::Utc::now().to_rfc3339(),
builder: provenance::builder_identity(),
}
}
/// Pretty JSON serialization.
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).context("serialize target provenance")
}
/// Parse a record from JSON.
pub fn from_json(text: &str) -> Result<Self> {
serde_json::from_str(text).context("parse target provenance")
}
/// The canonical byte string the attestation digest is taken over: compact,
/// key-sorted JSON so the same record folds to the same bytes anywhere.
pub fn canonical_bytes(&self) -> Result<Vec<u8>> {
let value: serde_json::Value =
serde_json::to_value(self).context("target provenance to value")?;
let canon = provenance::canonicalize(&value);
serde_json::to_vec(&canon).context("canonical target provenance bytes")
}
/// Fold the record into a [`TargetAttestation`] (canonical bytes + SHA-256 digest).
pub fn attest(self) -> Result<TargetAttestation> {
let canonical = self.canonical_bytes()?;
let digest: [u8; 32] = Sha256::digest(&canonical).into();
Ok(TargetAttestation { record: self, canonical, digest, seal: None })
}
/// **Fold this target record into the ONE record type the `build_provenance`
/// ledger holds.**
///
/// A container / ISO / VM cell is described by a [`TargetProvenance`], a cargo
/// cell by a [`BuildProvenance`] — and the warehouse has exactly one build-record
/// table, fed by exactly one submit
/// (`Telemetry.SubmitBuildProvenance`). That asymmetry is why an assembled cell
/// filed nothing at all: there was no record of the right shape to hand the
/// writer, so the producer returned early and ⚒ Build Thing's records pane stayed
/// empty however well the ISO mastered.
///
/// This is that missing shape — a projection, **not a second provenance path**.
/// Every field is carried across rather than re-derived, so what lands is what
/// the builder actually sealed:
///
/// * `id` is the TARGET record's id verbatim, so the `build_info` lineage rows
/// (already keyed by it via [`target_manifest_rows`](crate::build_info::target_manifest_rows))
/// join the provenance row instead of forming a second disconnected pile;
/// * `artifacts` are the PRODUCED artifacts — the image reference + its engine
/// digest + size, or the mastered ISO's path + sha256 + size;
/// * `variant` is the declared cell, so the row names WHICH cell and which
/// [`ArtifactKind`](crate::variant::ArtifactKind).
///
/// What has no home in `BuildProvenance` — the `engine`, the `descriptor_id` and
/// the descriptor itself — is not lost either: `target_manifest_rows` already
/// lowers all three into `build_info` under the same `build_id`. `cargo_args` is
/// EMPTY rather than a fabricated argv: no cargo ran for the target itself, and a
/// plausible-looking lie in a provenance record is worse than a blank.
pub fn to_build_provenance(&self, variant: &BuildVariant) -> BuildProvenance {
BuildProvenance {
schema: provenance::PROVENANCE_SCHEMA.to_string(),
id: self.id.clone(),
repo: self.repo.clone(),
variant: variant.clone(),
inputs: Inputs {
cargo_args: Vec::new(),
features: variant.features.clone(),
profile: String::from(variant.profile.clone()),
target: variant.target.clone(),
rustflags_cfg: variant.cfg.clone(),
vcs_rev: self.vcs_rev.clone(),
vcs_branch: self.vcs_branch.clone(),
vcs_dirty: self.vcs_dirty,
// The target capture reads `dirty` but not the porcelain digest;
// an empty string is "not captured", never a forged "clean".
worktree_digest: String::new(),
sources: Vec::new(),
},
toolchain: Toolchain::detect(),
host: self.host.clone(),
outcome: self.outcome,
artifacts: self.artifacts.clone(),
built_at: self.built_at.clone(),
builder: self.builder.clone(),
// A target build (image / VM / ISO) is not planned by a cargo
// build-thing variant recipe, so there is no coordinate to name.
// `None` rather than a plausible-looking one, for the same reason
// `cargo_args` above is empty.
build_thing: None,
builder_id: None,
}
}
}
/// An attestation over an image/vm build: the record, the canonical bytes folded,
/// the digest, and (once sealed) the detached seal. Mirrors
/// [`Attestation`](crate::provenance::Attestation) for the image/vm targets.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetAttestation {
/// The provenance record.
pub record: TargetProvenance,
/// The exact canonical bytes the digest was taken over.
#[serde(skip)]
pub canonical: Vec<u8>,
/// SHA-256 over [`Self::canonical`]. Serialized as lowercase hex.
#[serde(with = "crate::provenance::digest_hex")]
pub digest: [u8; 32],
/// The detached seal, once applied.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seal: Option<Seal>,
}
impl TargetAttestation {
/// Seal this attestation with `sealer`, binding a detached signature to the
/// [`Self::digest`] (which covers the produced-artifact hashes).
pub fn seal_with(mut self, sealer: &dyn ProvenanceSealer) -> Result<Self> {
self.seal = Some(sealer.seal(&self.digest)?);
Ok(self)
}
/// True once a real seal has been applied.
pub fn is_sealed(&self) -> bool {
self.seal.is_some()
}
/// Lowercase-hex form of the digest.
pub fn digest_hex(&self) -> String {
provenance::hex(&self.digest)
}
/// Recompute the digest from the record and confirm it still matches — the
/// tamper check (a mutated produced-artifact hash breaks this).
pub fn digest_matches_record(&self) -> Result<bool> {
let canonical = self.record.canonical_bytes()?;
let digest: [u8; 32] = Sha256::digest(&canonical).into();
Ok(digest == self.digest)
}
/// Serialize the whole attestation (record + digest + seal) to pretty JSON.
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).context("serialize target attestation")
}
/// Parse an attestation from JSON, recomputing the canonical bytes.
pub fn from_json(text: &str) -> Result<Self> {
let mut att: TargetAttestation =
serde_json::from_str(text).context("parse target attestation")?;
att.canonical = att.record.canonical_bytes()?;
Ok(att)
}
}
/// **Build the cargo variants a target CHAINS and hand back their artifact paths**
/// — the `staged` map every one of the three builders takes through
/// `with_variant_artifacts`.
///
/// A `from_variant = "x"` layer/kernel/payload means *the payload IS the artifact
/// cargo variant `x` produced*, and the renderers emit a context-relative
/// `COPY ./x` (or stage `x` into the ISO/VM tree). Nothing puts `./x` there unless
/// the variant is built first, so this is the step between "the declaration names
/// a cargo variant" and "the image build finds its payload".
///
/// One writer for all three target kinds: pass
/// [`ContainerBuild::variant_deps`](crate::container::ContainerBuild::variant_deps),
/// [`IsoBuild::variant_deps`](crate::iso::IsoBuild::variant_deps) or
/// [`VmBuild::variant_deps`](crate::vm::VmBuild::variant_deps). The path is read
/// off the build's OWN [`BuildOutcome::artifacts`](crate::provenance::BuildOutcome)
/// — what cargo actually wrote — never guessed from `target/<profile>/<bin>`.
///
/// `label` names the target in the error text (`container 'gunnar-scratch': …`).
pub fn stage_variant_artifacts(
set: &crate::variant::BuildVariantSet,
label: &str,
variant_deps: &[&str],
repo_root: &Path,
sink: &dyn crate::build_info::BuildInfoSink,
) -> Result<BTreeMap<String, PathBuf>> {
let mut staged: BTreeMap<String, PathBuf> = BTreeMap::new();
for vname in variant_deps {
if staged.contains_key(*vname) {
continue;
}
let v = set.require(vname).with_context(|| {
format!(
"{label}: chains cargo variant '{vname}', which this repo does not declare"
)
})?;
set.validate(v)?;
// The road is the declaration's, not the caller's: a set that pins a
// toolchain image builds its chained payload INSIDE that image, the same
// road a standalone cargo variant takes through the forge. MEASURED
// 2026-09-05 (gunnar, the owner: "building aws, azure etc all is done in
// containers not on host system — that is a pattern"): every 1.3.0
// medium's kernel and wasm recipe went through the container road while
// the `gunnar-server` payload they packed was a HOST build off
// `target/x86_64-unknown-linux-musl/release`, because this function
// only knew the host road. A payload whose toolchain is whatever the
// box has is not the payload the declaration describes.
// The one override (`NORDISK_CARGO_TOOLCHAIN_IMAGE`) applies here too, so a
// compose proven in a not-yet-pinned image builds its payload in that image.
let effective_tc = crate::cargo_container::toolchain_with_override(set.toolchain.clone())?;
let outcome = match effective_tc.as_ref() {
Some(tc) => {
eprintln!(
"[nornir] {label}: chained variant '{vname}' builds in {} (the declaration's toolchain)",
tc.image_ref()
);
chained_variant_in_container(set, vname, repo_root, tc)
.with_context(|| format!("{label}: build chained variant '{vname}' in {}", tc.image_ref()))?
}
None => provenance::build_variant_with_info(set, vname, repo_root, sink)
.with_context(|| format!("{label}: build chained variant '{vname}'"))?,
};
anyhow::ensure!(
outcome.success,
"{label}: chained variant '{vname}' failed to build"
);
let path = outcome.artifacts.first().cloned().with_context(|| {
format!(
"{label}: chained variant '{vname}' has kind '{:?}', which produced no single \
artifact to stage",
v.kind
)
})?;
staged.insert((*vname).to_string(), path);
}
Ok(staged)
}
/// A chained payload built inside the declaration's pinned toolchain image —
/// the same call the forge makes for a standalone cargo variant
/// (`dwarves::build_thing::build_in_container_observed`), so the two roads
/// share one writer: `--locked --offline`, the registry bound read-only, the
/// target dir bound at the container's `/target`.
#[cfg(feature = "cargo-container")]
fn chained_variant_in_container(
set: &crate::variant::BuildVariantSet,
name: &str,
repo_root: &Path,
tc: &crate::cargo_container::CargoToolchain,
) -> Result<crate::provenance::BuildOutcome> {
use crate::cargo_container::{build_variant_in_container, DraupnirCargoRunner, HostPaths};
let paths = HostPaths::from_env(repo_root);
std::fs::create_dir_all(&paths.target_dir)
.with_context(|| format!("create the target dir {}", paths.target_dir.display()))?;
let runner = DraupnirCargoRunner::new();
build_variant_in_container(set, name, repo_root, &paths.target_dir, paths.registry_bind(), tc, &runner)
}
/// Without the container road compiled in, a toolchain-pinned declaration is
/// REFUSED rather than quietly built on the host: the host road would produce
/// a payload the declaration does not describe, and the receipt would say
/// otherwise.
#[cfg(not(feature = "cargo-container"))]
fn chained_variant_in_container(
_set: &crate::variant::BuildVariantSet,
name: &str,
_repo_root: &Path,
tc: &crate::cargo_container::CargoToolchain,
) -> Result<crate::provenance::BuildOutcome> {
anyhow::bail!(
"chained variant '{name}': the declaration pins the toolchain image {} and this build \
of nornir-build-thing carries no container road (feature `cargo-container` is off). A \
host build here would be a payload whose toolchain is whatever the box has, under a \
receipt that names the image — refused. Enable `cargo-container`, or drop the \
`[build.toolchain]` pin if a host build is what you mean.",
tc.image_ref()
)
}
/// The result of [`build_container`] / [`build_vm`]: the produced artifacts + the
/// unsealed provenance attestation. Mirrors
/// [`BuildOutcome`](crate::provenance::BuildOutcome).
#[derive(Debug, Clone)]
pub struct TargetBuildOutcome {
/// The target name that was built.
pub target: String,
/// The target kind.
pub kind: TargetKind,
/// The produced artifacts (image digest / kernel+rootfs hashes).
pub artifacts: Vec<Artifact>,
/// The unsealed provenance attestation (seal it via [`TargetAttestation::seal_with`]).
pub attestation: TargetAttestation,
/// Whether the build succeeded.
pub success: bool,
/// Why it did not, when [`success`](Self::success) is `false` — the builder's own
/// error, rendered with its context chain.
///
/// The cargo peer has no equivalent field because cargo's reasons reach a caller
/// as `CompilerMessage`s on the observer stream. A target builder has no such
/// stream: this string is the ONLY account of why an image or ISO did not build,
/// and it used to be thrown away with the record it belonged to.
pub error: Option<String>,
}
impl TargetBuildOutcome {
/// **This outcome as a hard failure** — `Err` carrying the builder's own reason —
/// for a caller that wants the build to STOP rather than to be recorded.
///
/// A producer filing provenance wants the record and must not use this. A test or
/// a script that only proceeds if the artifact exists wants the reason at the
/// point of failure, and without this it would hand-roll the same three lines and
/// eventually get them subtly different (LAW 5). Note what it does NOT do: it
/// consumes the outcome, so the record is gone — which is exactly why it is a
/// deliberate call and not the default any more.
pub fn into_result(self) -> Result<Self> {
match &self.error {
Some(e) => anyhow::bail!("{} '{}' did not build: {e}", self.kind.as_str(), self.target),
None => Ok(self),
}
}
}
/// **The ONE place a target build's outcome becomes a record** — the writer
/// [`build_container`], [`build_vm`] and [`build_iso`] all route through.
///
/// # Why this exists rather than three copies
///
/// It used to be three. `build_container` captured a `Failed` attestation and then
/// dropped it on the floor with `built?`; `build_vm` and `build_iso` did not even
/// capture one, because their `?` fired before the capture. So a red image or ISO
/// build filed **nothing**, while the cargo arm
/// ([`build_variant_observed_with_info`](crate::provenance::build_variant_observed_with_info))
/// returns `Ok(BuildOutcome { success: false, .. })` and its record is filed like any
/// other. A green-only ledger is the one that cannot answer the question you actually
/// ask it — *what happened the night the build broke* — and three copies of this
/// decision is how the arms drifted apart in the first place (LAW 5).
///
/// # The contract, which this changes deliberately
///
/// A builder error is **no longer propagated**. It is recorded — `Outcome::Failed`,
/// no artifacts, the error text in [`TargetBuildOutcome::error`] — and returned as
/// `Ok`, exactly as the cargo arm returns `Ok` for a red cargo exit. `Ok` here means
/// *the build reached a terminal outcome and there is a record of it*, not *it
/// worked*; [`TargetBuildOutcome::success`] is what says which.
///
/// `Err` is still returned, and now means only what it should: the build never
/// reached an outcome. An invalid descriptor, a descriptor that will not serialise,
/// an attestation that will not fold. There is genuinely nothing to file for those.
fn capture_target_build(
kind: TargetKind,
target: &str,
descriptor_id: String,
descriptor: serde_json::Value,
repo: &str,
repo_root: &Path,
engine: &str,
built: Result<Vec<Artifact>>,
) -> Result<TargetBuildOutcome> {
let (artifacts, outcome, success, error) = match built {
Ok(artifacts) => (artifacts, Outcome::Success, true, None),
// `{e:#}` and not `{e}`: the context chain is the useful half of an
// anyhow error, and this string is the whole account of the failure.
Err(e) => (Vec::new(), Outcome::Failed, false, Some(format!("{e:#}"))),
};
let attestation = TargetProvenance::capture(
kind,
target,
descriptor_id,
descriptor,
repo,
repo_root,
engine,
artifacts.clone(),
outcome,
)
.attest()?;
Ok(TargetBuildOutcome { target: target.to_string(), kind, artifacts, attestation, success, error })
}
/// **Build a container image + seal provenance over the PRODUCED image digest.**
/// The IMAGE-target peer of [`build_variant`](crate::provenance::build_variant):
/// validate the descriptor, build it through `builder` (the seam), then capture an
/// unsealed [`TargetAttestation`] binding the built image's content digest (not the
/// descriptor hash).
///
/// A builder failure is **recorded, not propagated** — see
/// [`capture_target_build`] for the contract and why it changed.
pub fn build_container(
builder: &dyn ImageBuilder,
image: &ContainerBuild,
repo: &str,
repo_root: &Path,
) -> Result<TargetBuildOutcome> {
// `validate_in`, not `validate`: a `containerfile = "…"` cell whose file is not
// in this checkout must be refused BEFORE anything is captured, so the outcome is
// an honest "the declaration is broken" rather than a recorded engine failure
// several minutes downstream. Synthesis-mode cells touch no disk and are
// unaffected.
image.validate_in(repo_root).context("validate container descriptor")?;
let (built, held) = if image.expected_sha256.trim().is_empty() {
(builder.build(image, repo_root), None)
} else {
match build_pinned(builder, image, repo_root) {
Ok((b, held)) => (Ok(b), held),
Err(e) => (Err(e), None),
}
};
// A held image is recorded AS held: the record's `engine` is what a release reads to tell
// a minted pin from a couriered one, and the line is the step's own account of it.
let engine = match &held {
Some(h) => {
eprintln!(
" pinned image '{}': {h} — {} answers to the pin {}",
image.name,
image.tags.join(", "),
image.expected_sha256.trim().trim_start_matches("sha256:").get(..12).unwrap_or(""),
);
format!("{} — {h}", builder.engine())
}
None => builder.engine().to_string(),
};
capture_target_build(
TargetKind::Container,
&image.name,
image.content_id(),
serde_json::to_value(image).context("container descriptor to value")?,
repo,
repo_root,
&engine,
built.map(|b| b.to_artifacts()),
)
}
/// **Seal provenance over a VM's PRODUCED kernel+rootfs/disk artifacts.** The
/// VM-target peer of [`build_variant`](crate::provenance::build_variant): validate
/// the descriptor, resolve the produced artifacts through `builder` (the default
/// [`PrebuiltVmBuilder`] hashes what tunnr already built), then capture an unsealed
/// [`TargetAttestation`] binding those file hashes.
pub fn build_vm(
builder: &dyn VmImageBuilder,
vm: &VmBuild,
repo: &str,
repo_root: &Path,
) -> Result<TargetBuildOutcome> {
vm.validate().context("validate vm descriptor")?;
capture_target_build(
TargetKind::Vm,
&vm.name,
vm.content_id(),
serde_json::to_value(vm).context("vm descriptor to value")?,
repo,
repo_root,
builder.engine(),
builder.build(vm, repo_root).context("resolve vm artifacts"),
)
}
/// **Seal provenance over an ISO's PRODUCED `.iso` artifact.** The ISO-target peer
/// of [`build_vm`]: validate the descriptor, resolve the mastered ISO through
/// `builder` (the default [`PrebuiltIsoBuilder`] hashes an ISO already mastered on
/// disk), then capture an unsealed [`TargetAttestation`] binding that file hash. The
/// provenance `kind` is [`TargetKind::Iso`].
pub fn build_iso(
builder: &dyn IsoImageBuilder,
iso: &IsoBuild,
repo: &str,
repo_root: &Path,
) -> Result<TargetBuildOutcome> {
build_iso_as(builder, iso, TargetKind::Iso, repo, repo_root)
}
/// [`build_iso`] with the provenance `kind` CHOSEN by the caller — the seam
/// that lets an INSTALLER composition record `TargetKind::Installer` instead
/// of `Iso`. Until this existed the attestation spelled both artifacts
/// `"iso"`, so an auditor could not tell a bootable product from the tool
/// that installs it (the gap analysis's item 20, verbatim).
pub fn build_iso_as(
builder: &dyn IsoImageBuilder,
iso: &IsoBuild,
kind: TargetKind,
repo: &str,
repo_root: &Path,
) -> Result<TargetBuildOutcome> {
iso.validate().context("validate iso descriptor")?;
// Applied staged-file parameters land on the staging tree BEFORE mastering
// — the per-site hub.conf becomes a real file the ISO carries, and the
// same applied values already ride the descriptor serialized below (the
// provenance: which ISO holds which hub key, readable off the record).
for (rel, contents) in iso.staged_param_files() {
let dst = repo_root.join(&iso.staging).join(&rel);
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create {}", parent.display()))?;
}
std::fs::write(&dst, contents)
.with_context(|| format!("land staged parameter file {}", dst.display()))?;
}
capture_target_build(
kind,
&iso.name,
iso.content_id(),
serde_json::to_value(iso).context("iso descriptor to value")?,
repo,
repo_root,
builder.engine(),
builder.build(iso, repo_root).context("resolve iso artifact"),
)
}
/// **The one allowance for a seeded `/data`**: set to anything, and the
/// appliance builder grafts whatever the staging tree holds under `data/`
/// instead of refusing it. The forge's own e2e sets it; a release compose never
/// does, so a release medium cannot carry a fixture control store or a
/// test-minted key by accident.
///
/// It lives HERE, beside [`iso_output_path`], rather than beside the refusal it
/// gates, because it decides two things that must never disagree: **whether the
/// seed is grafted** and **what the medium is called**. MEASURED 2026-09-05
/// (gunnar): the seed was refused correctly and the fixture medium was still
/// written to `.iso-out/gunnar-appliance-iso.iso` — the release name — so for
/// four minutes the only thing saying it was a fixture was a file inside it.
pub const APPLIANCE_SEED_FIXTURE_ENV: &str = "KORP_APPLIANCE_SEED_FIXTURE";
/// What a fixture medium's file name carries that a release medium's does not.
pub const FIXTURE_ISO_SUFFIX: &str = "-fixture";
/// Whether this process has declared itself a FIXTURE compose, by
/// [`APPLIANCE_SEED_FIXTURE_ENV`]. The single reader of that variable: every
/// consequence of a fixture run — the graft, the medium's name, the scratch
/// directory — hangs off this one call, so none of them can be true while
/// another is false.
pub fn appliance_seed_fixture_requested() -> bool {
std::env::var_os(APPLIANCE_SEED_FIXTURE_ENV).is_some()
}
/// The file stem an ISO cell's outputs are named from: the cell's own name, or
/// the cell's name plus [`FIXTURE_ISO_SUFFIX`] for a fixture compose. Pure —
/// [`iso_output_path`] is this with the environment read for `fixture`, and the
/// scratch directory (`<stem>.work`) is the same stem, so a fixture run cannot
/// leave a seeded ESP behind under the release name either.
pub fn iso_out_stem(iso_name: &str, fixture: bool) -> String {
if fixture {
format!("{iso_name}{FIXTURE_ISO_SUFFIX}")
} else {
iso_name.to_string()
}
}
/// The conventional output path a mastered ISO lands at: `<repo_root>/.iso-out/<name>.iso`,
/// or `<repo_root>/.iso-out/<name>-fixture.iso` when this run is a fixture compose.
///
/// The forge masters to this path and the buildgate expects a product at this
/// path, both by calling this function — so the fixture name is not something a
/// caller opts into and can forget. A release compose never sets the variable,
/// so it gets the unchanged name and can neither write to the fixture name nor
/// read from it.
pub fn iso_output_path(repo_root: &Path, iso: &IsoBuild) -> PathBuf {
repo_root
.join(".iso-out")
.join(format!("{}.iso", iso_out_stem(&iso.name, appliance_seed_fixture_requested())))
}
/// **Seal provenance over a built kernel's PRODUCED artifacts** (`bzImage`,
/// `.config`, the sha256 pin file). The KERNEL-target peer of [`build_iso`]:
/// validate the descriptor, compile through `builder` (the
/// [`KernelBuilder`](crate::kernel::KernelBuilder) seam — the container-driving
/// [`ContainerKernelBuilder`](crate::kernel::ContainerKernelBuilder) in
/// production, a fake in tests), then capture an unsealed [`TargetAttestation`]
/// binding those file hashes. The record's `kind` is [`TargetKind::Kernel`] and
/// its descriptor carries version + tarball sha256 + profile — the receipt an
/// auditor reads the pin off.
///
/// A builder failure is **recorded, not propagated** — see
/// [`capture_target_build`] for the contract.
pub fn build_kernel(
builder: &dyn crate::kernel::KernelBuilder,
kernel: &crate::kernel::KernelBuild,
repo: &str,
repo_root: &Path,
) -> Result<TargetBuildOutcome> {
// `validate_in`, not `validate`: a declared `kconfig_fragment` missing from
// this checkout is refused BEFORE anything is fetched or captured (the
// container-kind precedent).
kernel.validate_in(repo_root).context("validate kernel descriptor")?;
capture_target_build(
TargetKind::Kernel,
&kernel.name,
kernel.content_id(),
serde_json::to_value(kernel).context("kernel descriptor to value")?,
repo,
repo_root,
builder.engine(),
builder.build(kernel, repo_root).context("compile kernel"),
)
}
/// **Assemble a distribution package + seal provenance over the PRODUCED BYTES.**
///
/// The package peer of [`build_kernel`] and [`build_container`], and the place the
/// provenance rule this kind exists for is ENFORCED rather than documented:
///
/// 1. `validate_in` — a cell naming a source directory that is not in this
/// checkout is refused BEFORE a container starts (the container-kind
/// precedent);
/// 2. the builder produces the distributions;
/// 3. [`write_sha256sums`](crate::package::write_sha256sums) writes the manifest —
/// the ONE writer, so this door and the repo's own publish verb leave the same
/// bytes;
/// 4. [`verify_produced`](crate::package::verify_produced) reads the DIRECTORY back
/// and refuses if a declared member is not there;
/// 5. the attestation is captured over the paths step 4 returned.
///
/// # The order of 3 and 4 is the point
///
/// The manifest is written BEFORE the set is verified, because `SHA256SUMS` is
/// itself a required member: verifying first would refuse every build for missing
/// a file only this function writes. And the verification then reads the manifest
/// it just wrote as evidence, which is exactly right — it is on disk or it is not.
///
/// # What is hashed
///
/// The DISTRIBUTIONS, as they landed. `capture_target_build` receives
/// [`Artifact`]s built by [`Artifact::hash_file`] over the produced paths — never
/// the descriptor, never the recipe. That distinction is the whole reason this
/// kind is not a `Wasm` with extra fields: a record built from the declaration
/// describes what was ASKED FOR, and facett measured two builds of one declaration
/// differing by 1.7 MB when a feature flipped.
///
/// A builder failure is **recorded, not propagated** — see [`capture_target_build`].
pub fn build_package(
builder: &dyn crate::package::PackageBuilder,
pkg: &crate::package::PackageBuild,
repo: &str,
repo_root: &Path,
) -> Result<TargetBuildOutcome> {
pkg.validate_in(repo_root).context("validate package descriptor")?;
let dist = pkg.out_dir(repo_root);
let produced = builder
.build(pkg, repo_root)
.context("assemble package")
.and_then(|dists| {
// ── THE ORDER, and it is load-bearing ────────────────────────────
// `SHA256SUMS` and `provenance.json` are BOTH required members, so
// both have to be written before the set is verified — verifying
// first would refuse every build for missing files only this function
// writes. And they are written HERE, by this crate, so the forge door
// and a repo's own publish verb leave the same release behind.
//
// This is where the end-to-end test earned its place: the manifest
// writer worked and the verification worked, and together they were a
// door that could NEVER succeed, because each assumed the other had
// written the provenance. Nothing but running the whole thing could
// have shown that.
crate::package::write_sha256sums(pkg.format, &dist)
.with_context(|| format!("write the checksum manifest in {}", dist.display()))?;
crate::package::write_provenance(
pkg,
&crate::package::variant_for(pkg),
repo,
repo_root,
// The toolchain that produced the distributions is the CONTAINER's,
// named by the engine and the pinned image. `Toolchain::detect()`
// would attest to the host's rustc, which did not build this.
crate::provenance::Toolchain {
rustc: format!(
"{} in {}",
builder.engine(),
pkg.image.as_deref().unwrap_or("<no pinned image>")
),
host: String::new(),
cargo: String::new(),
..Default::default()
}
// The image also goes in the FIELD, not only inside a prose
// `rustc` line. A sentence cannot be queried; a field can, and
// `to_slsa_statement` emits it as a material where SLSA puts
// the things a build consumed.
.with_container_image(
pkg.image.as_deref().map(|r| {
r.split_once("@sha256:").map_or_else(|| r.to_string(), |(n, _)| n.to_string())
}),
pkg.image
.as_deref()
.and_then(|r| r.split_once("@sha256:").map(|(_, d)| d.to_string())),
),
&dists,
)
.with_context(|| format!("write the provenance record in {}", dist.display()))?;
crate::package::verify_produced(pkg, &dist)
})
.and_then(|paths| {
paths
.iter()
.map(|p| Artifact::hash_file(repo_root, p))
.collect::<Result<Vec<Artifact>>>()
});
capture_target_build(
TargetKind::Package,
&pkg.name,
pkg.content_id(),
serde_json::to_value(pkg).context("package descriptor to value")?,
repo,
repo_root,
builder.engine(),
produced,
)
}
/// The conventional path a built kernel's boot image lands at:
/// `<repo_root>/.kernel-out/<name>/bzImage` (beside `kernel.config` and
/// `<name>.kernel-pin.json` — [`kernel_output_dir`](crate::kernel::kernel_output_dir)).
pub fn kernel_output_path(repo_root: &Path, kernel: &crate::kernel::KernelBuild) -> PathBuf {
crate::kernel::kernel_output_dir(repo_root, kernel).join("bzImage")
}
/// The dep-light default [`IsoImageBuilder`]: seal provenance over the mastered
/// `.iso` an ISO build **already produced** (real xorriso mastering runs elsewhere).
/// Hashes the conventional [`iso_output_path`] (or an override staged via
/// [`with_output`](Self::with_output)); errors if no ISO is present (nothing to
/// seal). A `from_variant` payload staged via
/// [`with_variant_artifacts`](Self::with_variant_artifacts) is hashed FIRST so the
/// seal binds the freshly-built app binary that rode onto the ISO — the exact peer
/// of [`PrebuiltVmBuilder`]'s `from_variant` staging.
#[derive(Default)]
pub struct PrebuiltIsoBuilder {
/// An explicit produced-ISO path override (else the conventional `.iso-out/<name>.iso`).
output: Option<PathBuf>,
/// Cargo-variant artifacts staged for a `from_variant` chain.
staged: BTreeMap<String, PathBuf>,
}
impl PrebuiltIsoBuilder {
/// A builder resolving the conventional output path.
pub fn new() -> Self {
Self::default()
}
/// Override the produced-ISO path to hash (else `<repo_root>/.iso-out/<name>.iso`).
pub fn with_output(mut self, output: PathBuf) -> Self {
self.output = Some(output);
self
}
/// Stage the cargo-variant artifacts a `from_variant` ISO chains: variant name →
/// the produced artifact path on the host.
pub fn with_variant_artifacts(mut self, staged: BTreeMap<String, PathBuf>) -> Self {
self.staged = staged;
self
}
}
impl IsoImageBuilder for PrebuiltIsoBuilder {
fn build(&self, iso: &IsoBuild, repo_root: &Path) -> Result<Vec<Artifact>> {
let mut artifacts = Vec::new();
// `from_variant` staging: hash the app binary that rode onto the ISO FIRST,
// so provenance binds the freshly-built payload, not just the ISO image.
if let Some(variant) = &iso.from_variant {
let src = self.staged.get(variant).with_context(|| {
format!(
"iso '{}': chains cargo variant '{variant}' via `from_variant` but no built \
artifact was staged for it — build the variant first and pass it via \
`with_variant_artifacts`",
iso.name
)
})?;
let abs = if src.is_absolute() { src.clone() } else { repo_root.join(src) };
artifacts.push(Artifact::hash_file(repo_root, &abs)?);
}
let out = self
.output
.clone()
.unwrap_or_else(|| iso_output_path(repo_root, iso));
let abs = if out.is_absolute() { out.clone() } else { repo_root.join(&out) };
if abs.is_file() {
artifacts.push(Artifact::hash_file(repo_root, &abs)?);
}
if artifacts.is_empty() {
anyhow::bail!(
"iso '{}': no produced .iso artifact found on disk to seal at {} \
(master it first — the heavy ISO build is the consumer's)",
iso.name,
abs.display()
);
}
Ok(artifacts)
}
}
/// **The pure-Rust [`IsoImageBuilder`]** (engine `pure-rust`): masters a bootable
/// ISO-9660 + El-Torito image with **ZERO external process** via
/// [`crate::iso_writer::IsoWriter`], then seals provenance over the produced `.iso`
/// (and any `from_variant` payload) by reusing [`PrebuiltIsoBuilder`]'s hashing —
/// one home for the sealing, so this builder adds only the *produce* step.
///
/// This is the law-compliant replacement for the deleted `XorrisoIsoBuilder`: it
/// does **not** shell out to `xorriso` / `mkisofs` / `genisoimage` (or any binary).
/// The ISO structures — Primary Volume Descriptor, path tables, directory records,
/// and the El-Torito boot catalog with a UEFI (`0xEF`) section entry pointing at
/// the `efiboot.img` EFI System Partition — are emitted byte-for-byte from Rust.
///
/// The descriptor maps onto the writer exactly as the old `xorriso -as mkisofs`
/// argv did: the `staging` tree is grafted at the volume root, the `kernel` /
/// `rootfs` are placed at their relative ISO paths, the `kernel` is registered as
/// the BIOS (`-b`) El-Torito boot image, and a UEFI/hybrid `format` registers
/// `efiboot.img` as the EFI (`-e … -no-emul-boot`) boot image. The kernel
/// command-line (`console=ttyS0 …`) rides inside the staged bootloader config
/// (grub.cfg in the ESP), exactly as it did under `xorriso` — the writer masters
/// the filesystem, the staged bootloader consumes the cmdline.
#[derive(Default)]
pub struct PureRustIsoBuilder {
/// An explicit output-ISO path (else the conventional `.iso-out/<name>.iso`).
output: Option<PathBuf>,
/// Cargo-variant artifacts staged for a `from_variant` chain (sealed too).
staged: BTreeMap<String, PathBuf>,
/// A real legacy-BIOS stage to lay as the El-Torito default entry, patched
/// with the payload placements — see [`crate::iso_writer::IsoWriter::set_bios_stage`].
bios_stage: Option<crate::iso_writer::BiosStage>,
/// The 440 bytes of MBR boot code a HYBRID medium carries at LBA 0 — see
/// [`crate::iso_writer::IsoWriter::set_hybrid_mbr`]. The CALLER assembles
/// them (this crate cannot depend on the assembler that does).
hybrid_mbr: Option<Vec<u8>>,
/// Lay the GPT ESP partition whatever the ESP's size
/// ([`crate::iso_writer::GptHybrid::Always`]) — the declaration's
/// `medium_boot = "gpt"`: a medium that boots by UEFI off a cdrom AND off a
/// disk it was imported as, with no legacy stage and no boot code at LBA 0.
gpt_always: bool,
}
impl PureRustIsoBuilder {
/// A builder that masters to the conventional [`iso_output_path`].
pub fn new() -> Self {
Self::default()
}
/// **Lay a real legacy-BIOS stage** as the El-Torito default (80x86) entry
/// beside the EFI section: the staging tree must carry `stage.image`,
/// `stage.kernel` and (if named) `stage.initrd`; the writer fills the
/// stage's patch table with their placements and `stage.cmdline`. One
/// medium, two firmware paths; without this a UEFI ISO gets the refusal stub.
pub fn with_bios_stage(mut self, stage: crate::iso_writer::BiosStage) -> Self {
self.bios_stage = Some(stage);
self
}
/// **Make the mastered medium HYBRID** — bootable as a DISK as well as as
/// a cdrom. `mbr_code` is
/// `korp_boot_bios::emit_mbr(`[`iso_writer::HYBRID_STAGE_LBA`](crate::iso_writer::HYBRID_STAGE_LBA)`,
/// stage_sectors)`. Refused at master time without
/// [`with_bios_stage`](Self::with_bios_stage).
pub fn with_hybrid_mbr(mut self, mbr_code: Vec<u8>) -> Self {
self.hybrid_mbr = Some(mbr_code);
self
}
/// **Lay the GPT ESP partition unconditionally** — the declaration's
/// `medium_boot = "gpt"`. UEFI firmware then boots the medium off a disk
/// (a USB stick, a cloud that imports the image as a disk — Azure Gen2)
/// through the GPT, and off a cdrom through El Torito, with no legacy
/// stage and nothing at LBA 0 a BIOS would run. Under the El Torito ceiling
/// the medium's shape otherwise stays as it was.
pub fn with_gpt_always(mut self) -> Self {
self.gpt_always = true;
self
}
/// Override the produced-ISO output path (else `<repo_root>/.iso-out/<name>.iso`).
pub fn with_output(mut self, output: PathBuf) -> Self {
self.output = Some(output);
self
}
/// Stage the cargo-variant artifacts a `from_variant` ISO chains (sealed FIRST,
/// like [`PrebuiltIsoBuilder::with_variant_artifacts`]).
pub fn with_variant_artifacts(mut self, staged: BTreeMap<String, PathBuf>) -> Self {
self.staged = staged;
self
}
/// The resolved output-ISO path under `repo_root`.
fn out_path(&self, iso: &IsoBuild, repo_root: &Path) -> PathBuf {
let out = self.output.clone().unwrap_or_else(|| iso_output_path(repo_root, iso));
if out.is_absolute() { out } else { repo_root.join(out) }
}
/// Assemble the [`IsoWriter`] tree from the descriptor (no I/O beyond staging
/// the file list) and master the `.iso` to `out`. Pure Rust, no subprocess.
fn master(&self, iso: &IsoBuild, repo_root: &Path, out: &Path) -> Result<()> {
use crate::iso_writer::IsoWriter;
let mut w = IsoWriter::new(iso.volume_id.clone());
// The staging tree is grafted at the volume root (mkisofs' final path arg).
// A DECLARED tree that is not on disk is a hard error, not a skip — see
// `IsoBuild::staging_tree` for the empty ISO that used to master clean.
if let Some(staging) = iso.staging_tree(repo_root)? {
w.add_dir_recursive("", &staging).with_context(|| {
format!("iso '{}': stage tree {}", iso.name, staging.display())
})?;
}
// The kernel rides at its relative ISO path and is the BIOS El-Torito
// boot image (the old `-b <kernel> -no-emul-boot`).
if !iso.kernel.is_empty() {
let src = repo_root.join(&iso.kernel);
w.add_file(&iso.kernel, &src)
.with_context(|| format!("iso '{}': add kernel {}", iso.name, src.display()))?;
w.set_bios_boot(&iso.kernel, 4);
}
// The rootfs/initramfs rides at its relative ISO path (data payload).
if !iso.rootfs.is_empty() {
let src = repo_root.join(&iso.rootfs);
w.add_file(&iso.rootfs, &src)
.with_context(|| format!("iso '{}': add rootfs {}", iso.name, src.display()))?;
}
// A UEFI/hybrid ISO boots the `efiboot.img` EFI System Partition off the
// El-Torito EFI (0xEF) section — the old `-eltorito-alt-boot -e efiboot.img
// -no-emul-boot`. The ESP is provided by the staging tree (as with xorriso);
// if it is missing the writer errors clearly rather than faking a boot.
//
// And it is laid as a GPT HYBRID the moment the ESP outgrows El-Torito's
// u16 sector count (`iso_writer::GptHybrid::WhenEspExceedsElTorito`): a
// protective MBR + GPT whose one EFI System partition spans the whole
// `efiboot.img`, so the medium boots off USB/disk through the GPT with no
// 32 MiB ceiling, and the clamped El-Torito entry is RECORDED on the
// layout line below rather than silently cut. Under the ceiling the medium
// is byte-identical to what this masterer always produced. Decided HERE,
// in the one masterer every composition routes through (the appliance
// forge, the installer composer, a plain `[[build.iso]]`), so no
// composer carries its own ceiling arithmetic. MEASURED 2026-08-22 (oden):
// korp-appliance-iso's ESP is 88 385 024 B — 2.6× the ceiling — and
// shrinks by no wish.
if iso.format.is_uefi() {
w.set_efi_boot("efiboot.img");
w.set_gpt_hybrid(if self.gpt_always {
crate::iso_writer::GptHybrid::Always
} else {
crate::iso_writer::GptHybrid::WhenEspExceedsElTorito
});
}
// A real legacy-BIOS stage (the composer stages it; the writer patches
// it): the default El-Torito entry becomes a loader that reads the
// kernel + initramfs off the volume by the LBAs the writer just chose.
if let Some(stage) = &self.bios_stage {
w.set_bios_stage(stage.clone());
}
// The HYBRID boot sector — the declaration's (`medium_boot = "hybrid"`)
// consequence, carried here by the composer that assembled the code.
if let Some(code) = &self.hybrid_mbr {
w.set_hybrid_mbr(code.clone());
}
w.write(out)
.with_context(|| format!("iso '{}': master pure-Rust ISO to {}", iso.name, out.display()))?;
// The receipt line: what was LAID, from the writer's applied layout — a
// hybrid says so by name, a plain medium says its catalog count.
if let Some(layout) = w.layout() {
eprintln!(" · iso '{}': {}", iso.name, layout.describe());
}
Ok(())
}
}
impl IsoImageBuilder for PureRustIsoBuilder {
fn engine(&self) -> &str {
"pure-rust"
}
fn build(&self, iso: &IsoBuild, repo_root: &Path) -> Result<Vec<Artifact>> {
// 1. resolve the output path + ensure its parent dir exists.
let out = self.out_path(iso, repo_root);
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("iso '{}': create output dir {}", iso.name, parent.display())
})?;
}
// 2. master the bootable ISO in PURE RUST — no `xorriso`, no subprocess.
self.master(iso, repo_root, &out)?;
if !out.is_file() {
anyhow::bail!(
"iso '{}': pure-Rust master reported success but produced no ISO at {}",
iso.name,
out.display()
);
}
// 3. seal provenance over the freshly-mastered ISO (+ staged from_variant
// payload) via the reused prebuilt hashing — one home for the sealing.
PrebuiltIsoBuilder::new()
.with_output(out)
.with_variant_artifacts(self.staged.clone())
.build(iso, repo_root)
}
}
/// The dep-light default [`VmImageBuilder`]: seal provenance over the kernel +
/// rootfs/disk artifacts a VM build **already produced** (tunnr's heavy Buildroot
/// build runs elsewhere). Hashes each of `kernel` / `rootfs` / `disk` that resolves
/// to a real file under `repo_root`; errors if none is present (nothing to seal).
///
/// **`from_variant` staging.** When the descriptor chains a cargo build via
/// [`VmBuild::from_variant`](crate::vm::VmBuild::from_variant) — the "build it,
/// then boot it" chain — the produced artifact of that variant (e.g. the PID-1
/// binary that becomes `/init` in the initramfs) is staged in via
/// [`with_variant_artifacts`](Self::with_variant_artifacts) and hashed as a sealed
/// produced-artifact **first**, so the seal binds the freshly-built app binary and
/// not merely a stand-in kernel/rootfs. A `from_variant` set with nothing staged is
/// a hard error — the chain is unsatisfied, never silently skipped. This is the
/// exact peer of [`BollardImageBuilder::with_variant_artifacts`] on the image path.
///
/// The real tunnr `build.sh` invocation that *produces* the kernel+rootfs is a
/// heavy handoff that stays tunnr's (`tunnr-build`'s `BuildShVmBuilder`, engine
/// `tunnr`, wraps this for the sealing); this dep-light path seals what is on disk.
#[derive(Default)]
pub struct PrebuiltVmBuilder {
/// Cargo-variant artifacts staged for a `from_variant` chain: variant name →
/// the produced artifact path on the host.
staged: BTreeMap<String, PathBuf>,
}
impl PrebuiltVmBuilder {
/// A builder with no staged variant artifacts (the plain prebuilt path).
pub fn new() -> Self {
Self::default()
}
/// Stage the cargo-variant artifacts a `from_variant` VM chains: a map from
/// variant name → the produced artifact path on the host. A VM whose
/// `from_variant` names a staged variant then seals that produced binary as its
/// boot payload (the "build the app, then boot it" chain honoured).
pub fn with_variant_artifacts(mut self, staged: BTreeMap<String, PathBuf>) -> Self {
self.staged = staged;
self
}
}
impl VmImageBuilder for PrebuiltVmBuilder {
fn build(&self, vm: &VmBuild, repo_root: &Path) -> Result<Vec<Artifact>> {
let mut artifacts = Vec::new();
// `from_variant` staging: the boot payload produced by a cargo build (the
// PID-1 binary) is hashed FIRST as a sealed produced-artifact, so provenance
// binds the freshly-built app, not just the kernel/rootfs it rides in.
if let Some(variant) = &vm.from_variant {
let src = self.staged.get(variant).with_context(|| {
format!(
"vm '{}': chains cargo variant '{variant}' via `from_variant` but no built \
artifact was staged for it — build the variant first and pass it via \
`with_variant_artifacts`",
vm.name
)
})?;
let abs = if src.is_absolute() { src.clone() } else { repo_root.join(src) };
artifacts.push(Artifact::hash_file(repo_root, &abs)?);
}
for rel in [vm.kernel.as_str(), vm.rootfs.as_str(), vm.disk.as_str()] {
if rel.is_empty() {
continue;
}
let p = Path::new(rel);
let abs = if p.is_absolute() { p.to_path_buf() } else { repo_root.join(p) };
if abs.is_file() {
artifacts.push(Artifact::hash_file(repo_root, &abs)?);
}
}
if artifacts.is_empty() {
anyhow::bail!(
"vm '{}': no produced kernel/rootfs/disk artifact found on disk to seal \
(build them first — the heavy build is tunnr's)",
vm.name
);
}
Ok(artifacts)
}
}
// ---------------------------------------------------------------------------
// Real container image builder — bollard (podman/Docker REST API), ZERO-SHELL.
// ---------------------------------------------------------------------------
/// **The MANIFEST digest out of an engine's `RepoDigests`** — the `sha256:…`
/// half of the first `<repo>@sha256:<digest>` entry, or `None` when the engine
/// reported none.
///
/// Pure, and deliberately outside the `image-build` feature so its behaviour is
/// provable without a socket. Two callers share it: the build path (which then
/// falls back to the image id, because a provenance record must say *something*
/// about what it built) and
/// [`BollardImageBuilder::manifest_digest`] (which must not, because its answer
/// becomes a pin).
#[must_use]
pub fn manifest_digest_of(repo_digests: Option<&[String]>) -> Option<String> {
repo_digests?
.first()
// `repo@sha256:…` → take the digest half.
.and_then(|rd| rd.rsplit_once('@'))
.map(|(_, d)| d.to_string())
}
/// **The podman API socket URL the image builder connects to** — the pure
/// ladder behind `BollardImageBuilder::socket_url`, every input passed in.
///
/// `DOCKER_HOST` first; then the session's own `XDG_RUNTIME_DIR`; then the
/// runtime directory logind makes for the REAL uid of this process,
/// `/run/user/<uid>`. An empty variable counts as unset (an empty
/// `XDG_RUNTIME_DIR` would otherwise point at `/podman/podman.sock`).
///
/// The third rung used to be the literal `/run/user/1000` (kit-defects (3),
/// 2026-10-05): with no `XDG_RUNTIME_DIR` — a systemd system unit with `User=`,
/// an `ssh box cmd` with no login session — the builder looked in ANOTHER
/// account's runtime directory whenever the build user was not the desktop's
/// first user, and failed with EACCES or ENOENT naming a socket that was never
/// this process's. draupnir's `engine_socket_url_from` made the same move for
/// the container engine; this is the image builder's copy of the ladder, kept
/// here because the `image-build` feature does not link draupnir. Unlike
/// draupnir's, a missing uid is refused by name rather than pointed at the
/// system socket: a rootless build that silently talks to root's podman is not
/// the build that was asked for.
///
/// # Errors
///
/// None of the three is known — no `DOCKER_HOST`, no `XDG_RUNTIME_DIR`, and no
/// uid (no `/proc`).
pub fn podman_socket_url_from(
docker_host: Option<&str>,
xdg_runtime_dir: Option<&str>,
uid: Option<u32>,
) -> Result<String> {
let set = |v: Option<&str>| v.map(str::trim).filter(|v| !v.is_empty()).map(str::to_string);
if let Some(h) = set(docker_host) {
return Ok(h);
}
if let Some(xdg) = set(xdg_runtime_dir) {
return Ok(format!("unix://{}/podman/podman.sock", xdg.trim_end_matches('/')));
}
match uid {
Some(uid) => Ok(format!("unix:///run/user/{uid}/podman/podman.sock")),
None => anyhow::bail!(
"no podman API socket to connect to: DOCKER_HOST is unset, XDG_RUNTIME_DIR is unset, \
and this process's uid could not be read from /proc/self/status — so there is no \
runtime directory to look in. Set XDG_RUNTIME_DIR (a login session sets it; \
`loginctl enable-linger <user>` keeps /run/user/<uid> for a service) or point \
DOCKER_HOST at the socket (unix:///run/user/<uid>/podman/podman.sock)"
),
}
}
/// **The REAL uid of this process**, read from `/proc/self/status` — no `id -u`,
/// no FFI. `None` off Linux or without `/proc`.
#[must_use]
pub fn real_uid() -> Option<u32> {
real_uid_from_status(&std::fs::read_to_string("/proc/self/status").ok()?)
}
/// The real uid out of `/proc/<pid>/status` text: the first field of the `Uid:`
/// line (`real effective saved filesystem`). The REAL uid, because that is the
/// uid logind created `/run/user/<uid>` for.
fn real_uid_from_status(status: &str) -> Option<u32> {
status
.lines()
.find_map(|l| l.strip_prefix("Uid:"))
.and_then(|rest| rest.split_whitespace().next())
.and_then(|uid| uid.parse().ok())
}
/// The production [`ImageBuilder`]: build a [`ContainerBuild`] over the
/// podman/Docker REST API via bollard (feature `image-build`) — no `podman build`
/// subprocess. It streams the descriptor's Containerfile + on-disk layer sources as
/// an in-memory tar build context to the engine, then inspects the built image for
/// its real content digest — the produced-artifact hash the provenance seals.
#[cfg(feature = "image-build")]
pub struct BollardImageBuilder {
docker: bollard::Docker,
rt: tokio::runtime::Runtime,
/// Cargo-variant artifacts to stage into the build context: variant name → the
/// produced artifact path on the host. A `from_variant` image layer is staged
/// under its variant name so a variant-chained image build finds its payload.
staged: BTreeMap<String, PathBuf>,
}
#[cfg(feature = "image-build")]
impl BollardImageBuilder {
/// Resolve the podman API socket URL — [`podman_socket_url_from`] over this
/// process's environment and real uid.
fn socket_url() -> Result<String> {
podman_socket_url_from(
std::env::var("DOCKER_HOST").ok().as_deref(),
std::env::var("XDG_RUNTIME_DIR").ok().as_deref(),
real_uid(),
)
}
/// Connect to the podman/Docker API socket. `Err` when the socket is absent.
pub fn connect() -> Result<Self> {
let url = Self::socket_url()?;
let path = url.strip_prefix("unix://").unwrap_or(&url);
anyhow::ensure!(
std::path::Path::new(path).exists(),
"podman API socket not found at {path} (enable with `systemctl --user enable --now podman.socket`)"
);
let docker = bollard::Docker::connect_with_unix(&url, 120, bollard::API_DEFAULT_VERSION)
.with_context(|| format!("connect podman socket {url}"))?;
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.context("build tokio runtime for bollard image build")?;
Ok(BollardImageBuilder { docker, rt, staged: BTreeMap::new() })
}
/// **What `[build.toolchain] image_sha256` pins, read off the engine** — the
/// MANIFEST digest of `image` as 64 lowercase hex, no `sha256:` prefix.
///
/// A thin door onto the inspection [`build`](ImageBuilder::build) already
/// does: same socket, same `inspect_image`, same
/// [`manifest_digest_of`] extraction. It exists because a declaration that
/// must carry a digest had **no way to learn one** without shelling to
/// `podman image inspect --format '{{.Digest}}'` — the exact reach-around
/// the no-shelling law forbids, and the reason seven repos sat unpinned on
/// 2026-09-10.
///
/// Unlike `build`, this NEVER falls back to the image **id**: an id pasted
/// where a manifest digest belongs is refused by
/// [`CargoToolchain::validate`](crate::cargo_container::CargoToolchain::validate),
/// so answering one here would only move the refusal later. An image the
/// store holds with no repo digest (built locally, never pushed or pulled)
/// is refused BY NAME, naming the fix.
pub fn manifest_digest(&self, image: &str) -> Result<String> {
let docker = &self.docker;
let inspected = self
.rt
.block_on(async { docker.inspect_image(image).await })
.with_context(|| format!("inspect image {image}"))?;
let digest = manifest_digest_of(inspected.repo_digests.as_deref()).ok_or_else(|| {
anyhow::anyhow!(
"image {image} is in the store but carries no repo digest — it was built \
locally and never pushed or pulled, so there is no MANIFEST digest to pin. \
Mint it reproducibly (`--example mint_cargo_toolchain`) or pull it from a \
registry, then re-read"
)
})?;
Ok(digest.trim_start_matches("sha256:").to_string())
}
/// Provide the cargo-variant artifacts to stage into the build context: a map
/// from variant name → the produced artifact path on the host. A `from_variant`
/// image layer (which renders `COPY ./<variant>` in the Containerfile) is then
/// staged under `<variant>`, so a variant-chained image build (build holger,
/// then image it) actually finds its payload instead of failing the COPY.
pub fn with_variant_artifacts(mut self, staged: BTreeMap<String, PathBuf>) -> Self {
self.staged = staged;
self
}
/// Assemble the in-memory tar build context.
///
/// Synthesis mode: the Containerfile as `Dockerfile` plus each layer's on-disk
/// `source` file (best-effort — a missing file simply isn't tarred, so the
/// engine build surfaces the honest COPY error).
///
/// File mode (`containerfile = "…"`): the whole declared context directory,
/// flattened to the archive root (`append_dir_all("")` drops the root entry),
/// which is what a real Containerfile's `COPY` — including a multi-stage
/// `COPY --from=build` — resolves against.
fn tar_context(
image: &ContainerBuild,
repo_root: &Path,
staged: &BTreeMap<String, PathBuf>,
) -> Result<Vec<u8>> {
let mut builder = tar::Builder::new(Vec::new());
// FILE MODE: the build input is a real Containerfile on disk, and its `COPY`
// lines resolve against the declared context — so the context DIRECTORY is
// what must be streamed, not a Containerfile plus a handful of layer files.
// Tarring only the Dockerfile here would hand the engine a multi-stage build
// whose every `COPY` misses, i.e. a wrong image or a late failure.
if image.is_file_mode() {
let ctx = image.context_dir();
let ctx_abs = repo_root.join(&ctx);
anyhow::ensure!(
ctx_abs.is_dir(),
"container '{}': build context '{ctx}' is not a directory under {}",
image.name,
repo_root.display()
);
builder
.append_dir_all("", &ctx_abs)
.with_context(|| format!("tar build context {}", ctx_abs.display()))?;
return builder.into_inner().context("finish tar context");
}
let containerfile = image.containerfile();
let bytes = containerfile.as_bytes();
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder
.append_data(&mut header, "Dockerfile", bytes)
.context("tar Dockerfile")?;
for layer in &image.layers {
// A `from_variant` layer's payload is the artifact a cargo build produced;
// it renders `COPY ./<variant>` in the Containerfile, so stage the staged
// artifact into the context under `<variant>` (else the COPY has no source
// and the engine build fails). This is the `variant_deps` contract honored.
if let Some(variant) = &layer.from_variant {
let src = staged.get(variant).with_context(|| {
format!(
"container '{}': layer chains cargo variant '{variant}' but no built \
artifact was staged for it — build the variant first and pass it via \
`with_variant_artifacts`",
image.name
)
})?;
let mut f = std::fs::File::open(src)
.with_context(|| format!("open staged variant artifact {}", src.display()))?;
let len = f.metadata().map(|m| m.len()).unwrap_or(0);
let mut header = tar::Header::new_gnu();
header.set_size(len);
header.set_mode(if layer.executable { 0o755 } else { 0o644 });
header.set_cksum();
builder
.append_data(&mut header, variant.as_str(), &mut f)
.with_context(|| format!("tar staged variant artifact {variant}"))?;
continue;
}
if layer.source.is_empty() {
continue;
}
let p = Path::new(&layer.source);
let abs = if p.is_absolute() { p.to_path_buf() } else { repo_root.join(p) };
if abs.is_file() {
let mut f = std::fs::File::open(&abs)
.with_context(|| format!("open layer source {}", abs.display()))?;
// Set the mode explicitly rather than letting `append_file` inherit the
// host's. A source checked out 0644 would otherwise land 0644 in the
// image however the layer is declared, and with the `RUN chmod` gone
// this tar header is the only thing that makes the file executable.
let len = f.metadata().map(|m| m.len()).unwrap_or(0);
let mut header = tar::Header::new_gnu();
header.set_size(len);
header.set_mode(if layer.executable { 0o755 } else { 0o644 });
header.set_cksum();
builder
.append_data(&mut header, &layer.source, &mut f)
.with_context(|| format!("tar layer source {}", layer.source))?;
}
}
builder.into_inner().context("finish tar context")
}
}
#[cfg(feature = "image-build")]
impl ImageBuilder for BollardImageBuilder {
fn build(&self, image: &ContainerBuild, repo_root: &Path) -> Result<BuiltImage> {
use bollard::query_parameters::BuildImageOptionsBuilder;
use futures::StreamExt;
let tag = image
.primary_tag()
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{}:latest", image.name));
let context = Self::tar_context(image, repo_root, &self.staged)?;
// A recipe that states its minting instant (`ENV SOURCE_DATE_EPOCH=`) is built AT that
// instant: podman's `timestamp` sets the config's `created` AND every layer file's
// mtime, which no Containerfile line can reach. Without it the apt layer keeps the
// packages' own mtimes and a pinned recipe never reproduces its pin (MEASURED
// 2026-09-22, tunnr's kernel toolchain: 72d66255 vs ed482508, mtimes only).
let epoch = source_date_epoch_of_cell(image, repo_root)?;
let owned;
let docker = match epoch {
None => &self.docker,
Some(epoch) => {
owned = self.docker.clone().with_request_modifier(move |mut req| {
if let Some(uri) = reproducible_build_uri(&req.uri().to_string(), epoch) {
if let Ok(uri) = uri.parse() {
*req.uri_mut() = uri;
}
}
req
});
&owned
}
};
self.rt.block_on(async {
// `Dockerfile` for a synthesized cell (the name we tar it under); the
// real file's context-relative path for a `containerfile = "…"` cell.
let dockerfile = image.containerfile_in_context();
let opts = BuildImageOptionsBuilder::default()
.dockerfile(&dockerfile)
.t(&tag)
.rm(true)
.build();
let body = bollard::body_full(context.into());
let mut stream = docker.build_image(opts, None, Some(body));
while let Some(item) = stream.next().await {
let info = item.map_err(|e| anyhow::anyhow!("build image {tag}: {e}"))?;
if let Some(err) = info.error_detail {
anyhow::bail!("build image {tag}: {}", err.message.unwrap_or_default());
}
}
// Inspect the built image for its real content digest + size.
let inspected = docker
.inspect_image(&tag)
.await
.map_err(|e| anyhow::anyhow!("inspect built image {tag}: {e}"))?;
let digest = manifest_digest_of(inspected.repo_digests.as_deref())
.or(inspected.id.clone())
.unwrap_or_else(|| "sha256:unknown".to_string());
let bytes = inspected.size.unwrap_or(0).max(0) as u64;
// The declared DISTRIBUTABLE archive: stream the engine's image export
// (docker-archive, what `docker save` writes — `podman load` reads it)
// to the repo-root-relative path, hashing as it lands so the recorded
// sha256 is of the bytes actually written, never a re-read.
let archive = if image.archive.trim().is_empty() {
None
} else {
let rel = image.archive.trim();
let out = repo_root.join(rel);
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create archive dir {}", parent.display()))?;
}
let mut f = std::fs::File::create(&out)
.with_context(|| format!("create image archive {}", out.display()))?;
let mut hasher = Sha256::new();
let mut written: u64 = 0;
let mut stream = docker.export_image(&tag);
while let Some(chunk) = stream.next().await {
let chunk =
chunk.map_err(|e| anyhow::anyhow!("export image {tag}: {e}"))?;
std::io::Write::write_all(&mut f, &chunk)
.with_context(|| format!("write image archive {}", out.display()))?;
hasher.update(&chunk);
written += chunk.len() as u64;
}
if written == 0 {
anyhow::bail!(
"export image {tag}: the engine streamed 0 bytes — an empty archive is \
not a produced artifact"
);
}
Some(Artifact {
path: rel.to_string(),
bytes: written,
sha256: crate::provenance::hex(&hasher.finalize()),
})
};
Ok(BuiltImage { image_ref: tag.clone(), digest, bytes, archive })
})
}
/// The engine's own `/version`, read through [`engine_that_cannot_reproduce`]. An engine
/// that does not answer is not excused: `None`, so it mints and compares.
fn cannot_mint_reproducibly(&self) -> Option<String> {
let version = self.rt.block_on(async { self.docker.version().await }).ok()?;
let components: Vec<(String, String)> =
version.components.unwrap_or_default().into_iter().map(|c| (c.name, c.version)).collect();
engine_that_cannot_reproduce(&components)
}
/// Every declared tag inspected: `Some` when ALL of them name an image answering to `pin`.
fn held(&self, image: &ContainerBuild, pin: &str) -> Result<Option<BuiltImage>> {
let Some(primary) = image.primary_tag().map(str::to_string) else { return Ok(None) };
self.rt.block_on(async {
let mut answer = None;
for t in &image.tags {
// "No such image" and "the engine did not answer" both mean: mint.
let Ok(inspected) = self.docker.inspect_image(t).await else { return Ok(None) };
let mut ids: Vec<String> = inspected.id.iter().map(|i| crate::kernel::normalize_sha256(i)).collect();
for rd in inspected.repo_digests.as_deref().unwrap_or_default() {
if let Some((_, d)) = rd.rsplit_once('@') {
ids.push(crate::kernel::normalize_sha256(d));
}
}
if !built_matches_pin(&ids, pin) {
return Ok(None);
}
if *t == primary {
// The record names the image by the PIN when the pin is one of its manifest
// digests (a couriered image answers to several; the first is arbitrary).
let by_pin = ids.iter().skip(inspected.id.iter().count()).any(|d| d == pin).then(|| format!("sha256:{pin}"));
let digest = by_pin
.or_else(|| manifest_digest_of(inspected.repo_digests.as_deref()))
.or(inspected.id.clone())
.unwrap_or_else(|| "sha256:unknown".to_string());
answer = Some(BuiltImage {
image_ref: primary.clone(),
digest,
bytes: inspected.size.unwrap_or(0).max(0) as u64,
archive: None,
});
}
}
Ok(answer)
})
}
/// The image id and every manifest digest the engine knows the built image by.
fn identities(&self, built: &BuiltImage) -> Result<Vec<String>> {
let tag = built.image_ref.clone();
self.rt.block_on(async {
let inspected = self
.docker
.inspect_image(&tag)
.await
.map_err(|e| anyhow::anyhow!("inspect built image {tag}: {e}"))?;
let mut ids: Vec<String> = inspected.id.iter().map(|i| crate::kernel::normalize_sha256(i)).collect();
for rd in inspected.repo_digests.as_deref().unwrap_or_default() {
if let Some((_, d)) = rd.rsplit_once('@') {
ids.push(crate::kernel::normalize_sha256(d));
}
}
Ok(ids)
})
}
/// `repo:tag` for each of `tags`, onto the image `built` names.
fn tag(&self, built: &BuiltImage, tags: &[String]) -> Result<()> {
use bollard::query_parameters::TagImageOptions;
self.rt.block_on(async {
for t in tags {
// The tag half is after the LAST ':' that follows the last '/', so a
// registry port (`host:5000/img:tag`) is not taken for one.
let slash = t.rfind('/').map_or(0, |i| i + 1);
let (repo, tag) = match t[slash..].rfind(':') {
Some(i) => (&t[..slash + i], &t[slash + i + 1..]),
None => (t.as_str(), "latest"),
};
let opts = TagImageOptions { repo: Some(repo.to_string()), tag: Some(tag.to_string()) };
self.docker
.tag_image(&built.image_ref, Some(opts))
.await
.map_err(|e| anyhow::anyhow!("tag {} as {t}: {e}", built.image_ref))?;
}
Ok(())
})
}
/// Remove the tag only: `noprune`, not `force`, so an image another tag still names stays.
fn untag(&self, reference: &str) -> Result<()> {
use bollard::query_parameters::RemoveImageOptions;
self.rt.block_on(async {
let opts = RemoveImageOptions { force: false, noprune: true, ..Default::default() };
self.docker
.remove_image(reference, Some(opts), None)
.await
.map(|_| ())
.map_err(|e| anyhow::anyhow!("untag {reference}: {e}"))
})
}
}
#[cfg(test)]
mod fixture_naming_tests {
use super::{
appliance_seed_fixture_requested, iso_out_stem, iso_output_path, FIXTURE_ISO_SUFFIX,
};
use crate::iso::IsoBuild;
use std::path::Path;
/// The property HO-1 is about: a fixture compose and a release compose of
/// the SAME cell cannot produce a file at the same path. Before this, both
/// wrote `.iso-out/<cell>.iso` and only the medium's CONTENTS said which
/// one it was.
#[test]
fn a_fixture_medium_is_never_named_the_release_medium() {
let cell = "gunnar-appliance-iso";
let release = iso_out_stem(cell, false);
let fixture = iso_out_stem(cell, true);
assert_ne!(release, fixture, "the two composes must not share a file name");
// The release arm is EXACTLY the historical name — a release compose is
// byte-unchanged by this row, which is the half that must not move.
assert_eq!(release, cell, "a release compose keeps the name it always had");
assert_eq!(fixture, format!("{cell}{FIXTURE_ISO_SUFFIX}"));
// And the suffix cannot be empty, or the whole guard is a no-op that
// still passes the `assert_ne!` above only by accident.
assert!(!FIXTURE_ISO_SUFFIX.is_empty(), "an empty suffix makes the two names identical");
}
/// The path function every caller shares — the forge masters to it and the
/// buildgate expects a product at it — so they cannot disagree about which
/// name a fixture run used.
#[test]
fn the_output_path_is_the_stem_under_iso_out() {
let iso = IsoBuild { name: "appliance".into(), ..Default::default() };
let root = Path::new("/repo");
let got = iso_output_path(root, &iso);
let expect_stem = iso_out_stem(&iso.name, appliance_seed_fixture_requested());
assert_eq!(got, root.join(".iso-out").join(format!("{expect_stem}.iso")));
// Both spellings of the stem are exercised against the same shape, so a
// suffix that silently stopped being applied would be caught here even
// when the test process is not itself a fixture run.
assert_eq!(
root.join(".iso-out").join(format!("{}.iso", iso_out_stem(&iso.name, true))),
root.join(".iso-out").join("appliance-fixture.iso")
);
assert_eq!(
root.join(".iso-out").join(format!("{}.iso", iso_out_stem(&iso.name, false))),
root.join(".iso-out").join("appliance.iso")
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::{ContainerBuild, ImageLayer};
/// **The build request carries the recipe's instant** (2026-09-22). The compat `/build`
/// bollard sends is rewritten to podman's `libpod/build` with `layers=true`,
/// `timestamp=<epoch>` and the OCI format, keeping bollard's own parameters; any other
/// request is untouched. Fails before the fix: there was no rewrite, and the kernel
/// toolchain built from its pinned recipe answered caf41863 instead of 3f632569.
#[test]
fn a_recipe_with_an_instant_is_built_at_that_instant() {
let got = reproducible_build_uri("unix://2f72756e/v1.41/build?dockerfile=Containerfile&t=x%3Alatest&rm=true", 1783900800)
.expect("a compat build is rewritten");
assert_eq!(
got,
"unix://2f72756e/v5.0.0/libpod/build?dockerfile=Containerfile&t=x%3Alatest&rm=true&layers=true×tamp=1783900800&outputformat=application%2Fvnd.oci.image.manifest.v1%2Bjson"
);
assert_eq!(reproducible_build_uri("unix://2f72756e/v1.41/images/x/json", 1), None, "only the build is rewritten");
assert_eq!(reproducible_build_uri("unix://2f72756e/v5.0.0/libpod/build?t=x", 1), None, "never twice");
}
/// **The instant comes from the recipe, once**: a file-mode cell whose Containerfile states
/// `ENV SOURCE_DATE_EPOCH` answers it; one that does not, and every synthesized cell,
/// answers `None`; a malformed value is refused by name, never dropped.
#[test]
fn the_instant_is_the_containerfiles_own_line() {
let dir = tempfile::tempdir().unwrap();
let cell = |file: &str| ContainerBuild { name: "c".into(), containerfile: file.into(), ..Default::default() };
std::fs::write(dir.path().join("A"), "FROM x\nENV SOURCE_DATE_EPOCH=1783900800\nRUN true\n").unwrap();
std::fs::write(dir.path().join("B"), "FROM x\nRUN true\n").unwrap();
std::fs::write(dir.path().join("C"), "FROM x\nENV SOURCE_DATE_EPOCH=soon\n").unwrap();
assert_eq!(source_date_epoch_of_cell(&cell("A"), dir.path()).unwrap(), Some(1783900800));
assert_eq!(source_date_epoch_of_cell(&cell("B"), dir.path()).unwrap(), None);
assert!(source_date_epoch_of_cell(&cell("C"), dir.path()).is_err());
assert_eq!(source_date_epoch_of_cell(&appliance(), dir.path()).unwrap(), None, "a synthesized cell states no instant");
}
/// A fake [`ImageBuilder`] that returns a preset [`BuiltImage`] (a known digest)
/// — the "fake builder returns a known artifact" test seam, no daemon.
struct FakeImageBuilder {
built: BuiltImage,
}
impl ImageBuilder for FakeImageBuilder {
fn build(&self, _image: &ContainerBuild, _root: &Path) -> Result<BuiltImage> {
Ok(self.built.clone())
}
}
fn appliance() -> ContainerBuild {
ContainerBuild {
name: "tunnr-appliance".into(),
base: "debian:bookworm-slim".into(),
layers: vec![ImageLayer::from_variant("full", "/usr/local/bin/tunnr")],
entrypoint: vec!["/usr/local/bin/tunnr".into()],
tags: vec!["tunnr-appliance:0.1".into()],
..Default::default()
}
}
/// A declared `archive` rides the artifact record as a SECOND artifact beside
/// the image identity — and an undeclared one adds nothing. RED-when-broken:
/// if `build_container` went back to `vec![b.to_artifact()]`, the archive
/// would be produced on disk yet absent from the record, exactly the silent
/// truncation `to_artifacts` exists to prevent.
#[test]
fn a_declared_archive_is_a_recorded_artifact() {
let known = BuiltImage {
image_ref: "tunnr-appliance:0.1".into(),
digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222"
.into(),
bytes: 4096,
archive: Some(Artifact {
path: ".container-out/tunnr.tar".into(),
bytes: 12_345,
sha256: "3".repeat(64),
}),
};
let builder = FakeImageBuilder { built: known };
let mut img = appliance();
img.archive = ".container-out/tunnr.tar".into();
let out = build_container(&builder, &img, "tunnr", Path::new(".")).unwrap();
assert_eq!(out.artifacts.len(), 2, "image identity + the exported archive");
assert_eq!(out.artifacts[1].path, ".container-out/tunnr.tar");
assert_eq!(out.artifacts[1].bytes, 12_345);
assert_eq!(out.artifacts[1].sha256, "3".repeat(64));
}
/// `archive` must stay inside the repo: absolute paths and `..` escapes are
/// refused at validate time, before any engine runs.
#[test]
fn an_escaping_archive_path_is_refused() {
let mut img = appliance();
img.archive = "/tmp/evil.tar".into();
assert!(img.validate().is_err(), "absolute archive path must be refused");
img.archive = "../outside.tar".into();
assert!(img.validate().is_err(), "`..` escape must be refused");
img.archive = ".container-out/ok.tar".into();
assert!(img.validate().is_ok(), "a sane relative path passes");
}
/// A builder that remembers what it was asked: the tags each build ran under, every
/// `tag` and `untag` call — so a test can say which names moved.
struct TagLedger {
answers_to: Vec<String>,
built_under: std::cell::RefCell<Vec<Vec<String>>>,
tagged: std::cell::RefCell<Vec<String>>,
untagged: std::cell::RefCell<Vec<String>>,
}
impl TagLedger {
fn answering(ids: &[&str]) -> Self {
TagLedger {
answers_to: ids.iter().map(|s| s.to_string()).collect(),
built_under: Default::default(),
tagged: Default::default(),
untagged: Default::default(),
}
}
/// Every name that ends up pointing at the built image: a build's own tags, plus
/// every `tag`, minus every `untag`.
fn names_left(&self) -> Vec<String> {
let mut all: Vec<String> = self.built_under.borrow().iter().flatten().cloned().collect();
all.extend(self.tagged.borrow().iter().cloned());
all.retain(|n| !self.untagged.borrow().contains(n));
all.sort();
all.dedup();
all
}
}
impl ImageBuilder for TagLedger {
fn build(&self, image: &ContainerBuild, _root: &Path) -> Result<BuiltImage> {
self.built_under.borrow_mut().push(image.tags.clone());
Ok(BuiltImage {
image_ref: image.primary_tag().unwrap_or("none").to_string(),
digest: format!("sha256:{}", self.answers_to[0]),
bytes: 1,
archive: None,
})
}
fn identities(&self, _built: &BuiltImage) -> Result<Vec<String>> {
Ok(self.answers_to.clone())
}
fn tag(&self, _built: &BuiltImage, tags: &[String]) -> Result<()> {
self.tagged.borrow_mut().extend(tags.iter().cloned());
Ok(())
}
fn untag(&self, reference: &str) -> Result<()> {
self.untagged.borrow_mut().push(reference.to_string());
Ok(())
}
}
/// An engine with a store (`holds`: what the declared tag answers to), a builder (`mints`:
/// what a mint of the recipe answers to), and its own word on whether it can reproduce.
struct Engine {
holds: Vec<String>,
mints: Vec<String>,
lame: Option<String>,
builds: std::cell::Cell<u32>,
held_asked: std::cell::Cell<u32>,
}
impl Engine {
fn new(holds: &[&str], mints: &[&str], lame: Option<&str>) -> Self {
Engine {
holds: holds.iter().map(|s| s.to_string()).collect(),
mints: mints.iter().map(|s| s.to_string()).collect(),
lame: lame.map(str::to_string),
builds: Default::default(),
held_asked: Default::default(),
}
}
}
impl ImageBuilder for Engine {
fn build(&self, image: &ContainerBuild, _root: &Path) -> Result<BuiltImage> {
self.builds.set(self.builds.get() + 1);
Ok(BuiltImage { image_ref: image.primary_tag().unwrap_or("none").into(), digest: format!("sha256:{}", self.mints[0]), bytes: 1, archive: None })
}
fn identities(&self, _built: &BuiltImage) -> Result<Vec<String>> {
Ok(self.mints.clone())
}
fn held(&self, image: &ContainerBuild, pin: &str) -> Result<Option<BuiltImage>> {
self.held_asked.set(self.held_asked.get() + 1);
Ok(built_matches_pin(&self.holds, pin).then(|| BuiltImage {
image_ref: image.primary_tag().unwrap_or("none").into(),
digest: format!("sha256:{pin}"),
bytes: 1,
archive: None,
}))
}
fn cannot_mint_reproducibly(&self) -> Option<String> {
self.lame.clone()
}
}
const PIN: &str = "78eaa55a651d5bd76aae36db0f3307e267a92d2a547ee338a17822a103e0320e";
const DRIFTED: &str = "ea2c07ca42bc0000000000000000000000000000000000000000000000000000";
/// **An engine that cannot reproduce takes the pinned image it already HOLDS, and the record
/// says so** (2026-10-13). MEASURED on a podman 4.9.3 builder: the pinned `78eaa55a` was in
/// the store as `nordisk-kernel-toolchain:latest` (couriered), the step minted anyway, the
/// older buildah answered `ea2c07ca`, and the compose stopped at its first step.
///
/// **Fails before the fix**: `build_pinned` always builds, so the outcome is the
/// "NOT the pinned one" failure and `builds` is 1.
#[test]
fn an_engine_that_cannot_reproduce_takes_the_held_pin_and_the_record_says_held() {
let engine = Engine::new(&["3f632569f198", PIN], &[DRIFTED], Some("podman 4.9.3"));
let out = build_container(&engine, &kernel_toolchain(PIN), "tunnr", Path::new(".")).unwrap();
assert!(out.success, "{:?}", out.error);
assert_eq!(engine.builds.get(), 0, "nothing was minted");
assert_eq!(out.artifacts[0].path, "nordisk-kernel-toolchain:latest");
assert_eq!(out.artifacts[0].sha256, PIN);
assert_eq!(
out.attestation.record.engine, "bollard — held, not minted on this engine (podman 4.9.3)",
"a release tells a couriered pin from a minted one by the record"
);
// A declared archive is exported by a mint, and this engine's mint is never the pin.
let mut c = kernel_toolchain(PIN);
c.archive = ".container-out/t.tar".into();
let out = build_container(&engine, &c, "tunnr", Path::new(".")).unwrap();
assert!(!out.success && engine.builds.get() == 0);
assert!(out.error.unwrap_or_default().contains("podman 5 builder"));
}
/// **(a) An engine that CAN reproduce is never excused by what its store holds**: the store
/// holds the pin under the declared tag, the recipe changed and nobody re-pinned — the
/// step still mints, and FAILS by name. This is the recipe-drift gate; the first cut of
/// `held` (edda 158038d) switched it off on every warm box.
///
/// **Fails at 158038d**: `held` answers first, `builds` is 0 and the outcome is a success
/// over the OLD image.
#[test]
fn a_reproducing_engine_that_holds_the_pin_still_mints_and_a_changed_recipe_fails_by_name() {
let engine = Engine::new(&["3f632569f198", PIN], &[DRIFTED], None);
let out = build_container(&engine, &kernel_toolchain(PIN), "tunnr", Path::new(".")).unwrap();
assert_eq!(engine.builds.get(), 1, "it minted");
assert_eq!(engine.held_asked.get(), 0, "the store is not even asked");
assert!(!out.success, "a recipe that no longer mints its pin is a FAILED build");
let err = out.error.unwrap_or_default();
assert!(err.contains("NOT the pinned one") && err.contains("ea2c07ca42bc") && err.contains("78eaa55a651d"), "{err}");
assert_eq!(out.attestation.record.engine, "bollard", "and nothing is recorded as held");
// The same engine, recipe unchanged: minted, compared, recorded as an ordinary mint.
let engine = Engine::new(&[PIN], &["3f632569f198", PIN], None);
let out = build_container(&engine, &kernel_toolchain(PIN), "tunnr", Path::new(".")).unwrap();
assert!(out.success && engine.builds.get() == 1, "{:?}", out.error);
assert_eq!(out.attestation.record.engine, "bollard");
}
/// **(b) An engine that cannot reproduce and does not hold the pin fails with the fix in the
/// sentence** — courier the image, or use a podman 5 builder — never the bare "NOT the
/// pinned one", which on such a box sends the reader to a recipe that did not change.
/// Holding something ELSE under the tag is not holding the pin.
#[test]
fn an_engine_that_cannot_reproduce_and_does_not_hold_the_pin_fails_naming_the_fix() {
for holds in [vec![], vec!["311bc06f5637"]] {
let engine = Engine::new(&holds, &[DRIFTED], Some("podman 4.9.3"));
let out = build_container(&engine, &kernel_toolchain(PIN), "tunnr", Path::new(".")).unwrap();
assert!(!out.success);
assert_eq!(engine.builds.get(), 0, "a mint that can only fail is not started");
let err = out.error.unwrap_or_default();
assert!(!err.contains("NOT the pinned one"), "{err}");
for want in ["78eaa55a651d", "podman 4.9.3", "courier the pinned image", "nordisk-kernel-toolchain:latest", "podman save --format oci-archive", "podman 5 builder"] {
assert!(err.contains(want), "`{want}` is missing from: {err}");
}
}
}
/// The engine's own `/version` decides, and only a recognisable podman below 5 is excused.
#[test]
fn only_a_podman_below_the_reproducible_libpod_is_an_engine_that_cannot_reproduce() {
let v = |n: &str, v: &str| vec![("Conmon".to_string(), "conmon version 2.1.10".to_string()), (n.to_string(), v.to_string())];
assert_eq!(engine_that_cannot_reproduce(&v("Podman Engine", "4.9.3")).as_deref(), Some("podman 4.9.3"));
assert_eq!(engine_that_cannot_reproduce(&v("Podman Engine", "5.0.0")), None);
assert_eq!(engine_that_cannot_reproduce(&v("Podman Engine", "5.7.0")), None);
assert_eq!(engine_that_cannot_reproduce(&v("Engine", "24.0.7")), None, "not podman: not excused, it mints and compares");
assert_eq!(engine_that_cannot_reproduce(&v("Podman Engine", "unknown")), None, "unreadable: not excused");
assert_eq!(engine_that_cannot_reproduce(&[]), None);
assert!(reproducible_build_uri("http://d/build", 1).unwrap().contains(&format!("/v{REPRODUCIBLE_LIBPOD_MAJOR}.0.0/libpod/build")), "one number, two readers");
}
fn kernel_toolchain(pin: &str) -> ContainerBuild {
let mut c = crate::kernel::kernel_toolchain_container("");
c.containerfile = String::new();
c.base = "debian:bookworm-slim".into();
c.expected_sha256 = pin.into();
c
}
/// **A pinned image that the build does not reproduce moves NO tag, and the build fails by
/// name** (2026-09-22). MEASURED by T16 on t14s: the `--build kernel` compose's first step
/// built `nordisk-kernel-toolchain:latest`, the layer cache answered with the 2026-09-15
/// rebuild `311bc06f`, and `:latest` moved to it while the kernel row pins `78eaa55a`.
///
/// **Fails before the fix**: at edda `8a70bfe` `build_container` hands the declaration's own
/// tags to the engine — the image is built UNDER `:latest` — and nothing compares it with
/// any pin, so the outcome is a success and `:latest` names `311bc06f`.
#[test]
fn a_pinned_image_the_build_does_not_reproduce_moves_no_tag_and_fails_by_name() {
const PIN: &str = "78eaa55a651d5bd76aae36db0f3307e267a92d2a547ee338a17822a103e0320e";
const CACHED: &str = "311bc06f56379229e2f037cee44dea50a8d7b07bdffdfa51963abe37aa5872f8";
let engine = TagLedger::answering(&[CACHED, "4c90eea4aaaa"]);
let out = build_container(&engine, &kernel_toolchain(PIN), "tunnr", Path::new(".")).unwrap();
assert!(!out.success, "an image that is not the pinned one is a FAILED build");
let err = out.error.unwrap_or_default();
assert!(err.contains("NOT the pinned one") && err.contains("311bc06f5637") && err.contains("78eaa55a651d"), "{err}");
assert!(
engine.names_left().is_empty(),
"no name points at the unpinned image: {:?} (built under {:?})",
engine.names_left(),
engine.built_under.borrow()
);
assert!(!engine.built_under.borrow().iter().flatten().any(|t| t.ends_with(":latest")), "never built UNDER :latest");
}
/// The other half: an image that IS the pin (by manifest digest, or by image id) gets its
/// declared tags, and the staging tag comes off again.
#[test]
fn a_pinned_image_that_matches_gets_its_tags_and_loses_the_staging_one() {
const PIN: &str = "78eaa55a651d5bd76aae36db0f3307e267a92d2a547ee338a17822a103e0320e";
for ids in [vec!["3f632569f198", PIN], vec![PIN]] {
let engine = TagLedger::answering(&ids);
let out = build_container(&engine, &kernel_toolchain(&format!("sha256:{PIN}")), "tunnr", Path::new(".")).unwrap();
assert!(out.success, "{:?}", out.error);
assert_eq!(engine.names_left(), vec!["nordisk-kernel-toolchain:latest".to_string()]);
assert_eq!(out.artifacts[0].path, "nordisk-kernel-toolchain:latest", "the record names the declared tag");
}
// Unpinned: the old path exactly — built under its own tags, nothing else asked.
let engine = TagLedger::answering(&["abc"]);
let out = build_container(&engine, &kernel_toolchain(""), "tunnr", Path::new(".")).unwrap();
assert!(out.success);
assert_eq!(*engine.built_under.borrow(), vec![vec!["nordisk-kernel-toolchain:latest".to_string()]]);
assert!(engine.tagged.borrow().is_empty() && engine.untagged.borrow().is_empty());
}
#[test]
fn build_container_seals_over_the_produced_image_digest() {
let known = BuiltImage {
image_ref: "tunnr-appliance:0.1".into(),
digest: "sha256:1111111111111111111111111111111111111111111111111111111111111111".into(),
bytes: 4096,
archive: None,
};
let builder = FakeImageBuilder { built: known.clone() };
let img = appliance();
let out = build_container(&builder, &img, "tunnr", Path::new(".")).unwrap();
// The provenance seals over the PRODUCED image digest, not the descriptor.
assert_eq!(out.kind, TargetKind::Container);
assert_eq!(out.artifacts.len(), 1);
assert_eq!(out.artifacts[0].path, "tunnr-appliance:0.1");
assert_eq!(out.artifacts[0].sha256, "1".repeat(64), "sha256: prefix stripped, lowercased");
assert_eq!(out.artifacts[0].bytes, 4096);
// The descriptor's own content_id is retained AND is distinct from the
// produced-artifact hash — spec hash vs artifact hash.
assert_eq!(out.attestation.record.descriptor_id, img.content_id());
assert_ne!(out.attestation.record.descriptor_id, out.artifacts[0].sha256);
assert_eq!(out.attestation.record.schema, TARGET_PROVENANCE_SCHEMA);
assert!(out.success);
}
#[test]
fn target_attestation_round_trips_and_tamper_is_detected() {
let known = BuiltImage {
image_ref: "app:1".into(),
digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222".into(),
bytes: 10,
archive: None,
};
let out = build_container(
&FakeImageBuilder { built: known },
&ContainerBuild { name: "app".into(), base: "scratch".into(), tags: vec!["app:1".into()], ..Default::default() },
"repo",
Path::new("."),
)
.unwrap();
// Round-trip through JSON keeps the digest + verifies.
let att = out.attestation;
assert!(!att.is_sealed(), "fresh attestation is unsealed — no faked seal");
assert!(att.digest_matches_record().unwrap());
let back = TargetAttestation::from_json(&att.to_json().unwrap()).unwrap();
assert_eq!(back.digest, att.digest);
assert!(back.digest_matches_record().unwrap());
// TAMPER the produced-artifact hash → the digest no longer matches.
let mut tampered = att;
tampered.record.artifacts[0].sha256 = "0".repeat(64);
assert!(!tampered.digest_matches_record().unwrap(), "tampered artifact breaks the seal digest");
}
#[test]
fn build_vm_seals_over_the_produced_kernel_and_rootfs_hashes() {
// Stage real kernel+rootfs files, then prove the provenance binds their sha256.
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("artifacts")).unwrap();
std::fs::write(root.join("artifacts/bzImage"), b"KERNEL bytes").unwrap();
std::fs::write(root.join("artifacts/rootfs.cpio.gz"), b"ROOTFS bytes").unwrap();
let vm = VmBuild {
name: "holger-appliance".into(),
kernel: "artifacts/bzImage".into(),
rootfs: "artifacts/rootfs.cpio.gz".into(),
mem_mb: 512,
cores: 2,
..Default::default()
};
let out = build_vm(&PrebuiltVmBuilder::new(), &vm, "tunnr", root).unwrap();
assert_eq!(out.kind, TargetKind::Vm);
assert_eq!(out.artifacts.len(), 2, "kernel + rootfs sealed");
let kernel = out.artifacts.iter().find(|a| a.path == "artifacts/bzImage").unwrap();
assert_eq!(kernel.sha256, provenance::hex(&Sha256::digest(b"KERNEL bytes")));
assert!(out.attestation.digest_matches_record().unwrap());
// Distinct from the descriptor's spec hash.
assert_eq!(out.attestation.record.descriptor_id, vm.content_id());
}
#[test]
fn prebuilt_vm_builder_errors_when_no_artifact_present() {
let dir = tempfile::tempdir().unwrap();
let vm = VmBuild {
name: "ghost".into(),
kernel: "missing/bzImage".into(),
rootfs: "missing/rootfs".into(),
..Default::default()
};
let msg = failed_reason(build_vm(&PrebuiltVmBuilder::new(), &vm, "repo", dir.path()));
assert!(msg.contains("no produced"), "honest reason, carried on the record: {msg}");
}
#[test]
fn prebuilt_vm_builder_stages_and_seals_the_from_variant_artifact() {
// The "build the app, then boot it" chain: a VM whose `from_variant` names a
// cargo build seals THAT produced binary (the PID-1 that becomes /init) as a
// sealed artifact, ahead of the kernel/rootfs it rides in.
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("artifacts")).unwrap();
std::fs::write(root.join("artifacts/bzImage"), b"KERNEL bytes").unwrap();
std::fs::write(root.join("artifacts/rootfs.cpio.gz"), b"ROOTFS bytes").unwrap();
// the cargo variant's produced PID-1 binary, staged from target/.
std::fs::create_dir_all(root.join("target/release")).unwrap();
std::fs::write(root.join("target/release/wg-init"), b"PID1 APP BINARY").unwrap();
let vm = VmBuild {
name: "wg-appliance".into(),
kernel: "artifacts/bzImage".into(),
rootfs: "artifacts/rootfs.cpio.gz".into(),
from_variant: Some("wg-pid1".into()),
..Default::default()
};
let staged: BTreeMap<String, PathBuf> =
[("wg-pid1".to_string(), root.join("target/release/wg-init"))].into();
let builder = PrebuiltVmBuilder::new().with_variant_artifacts(staged);
let out = build_vm(&builder, &vm, "tunnr", root).unwrap();
// kernel + rootfs + the staged from_variant PID-1 binary — all sealed.
assert_eq!(out.artifacts.len(), 3, "kernel + rootfs + staged pid1");
let pid1 = out
.artifacts
.iter()
.find(|a| a.path.ends_with("wg-init"))
.expect("the from_variant PID-1 binary is a sealed produced-artifact");
assert_eq!(pid1.sha256, provenance::hex(&Sha256::digest(b"PID1 APP BINARY")));
assert!(out.attestation.digest_matches_record().unwrap());
// RED when broken: a `from_variant` with NOTHING staged is a hard error, never
// a silently-skipped chain.
let unstaged = PrebuiltVmBuilder::new();
let msg = failed_reason(build_vm(&unstaged, &vm, "tunnr", root));
assert!(
msg.contains("no built artifact was staged"),
"honest unsatisfied-chain reason: {msg}"
);
}
#[test]
fn build_container_seal_with_dev_sealer_is_verifiable() {
// The seal binds the produced-artifact digest through the SAME ProvenanceSealer
// seam build_variant uses. A tiny in-test sealer proves the wiring.
struct FixedSealer;
impl ProvenanceSealer for FixedSealer {
fn seal(&self, digest: &[u8; 32]) -> Result<Seal> {
Ok(Seal { alg: "test".into(), signer_cn: "t".into(), cms_der: digest.to_vec() })
}
}
let known = BuiltImage { image_ref: "x:1".into(), digest: "sha256:abcd".into(), bytes: 1, archive: None };
let out = build_container(
&FakeImageBuilder { built: known },
&ContainerBuild { name: "x".into(), base: "scratch".into(), tags: vec!["x:1".into()], ..Default::default() },
"repo",
Path::new("."),
)
.unwrap();
let sealed = out.attestation.seal_with(&FixedSealer).unwrap();
assert!(sealed.is_sealed());
// The seal's bytes are exactly the digest that covers the artifact hash.
assert_eq!(sealed.seal.as_ref().unwrap().cms_der, sealed.digest.to_vec());
}
/// The provenance engine label comes from the seam impl, not a hard-coded
/// literal — so a real `tunnr` VM build is recorded as `tunnr`, not `prebuilt`.
#[test]
fn build_vm_uses_the_builders_engine_label() {
struct FakeVmBuilder;
impl VmImageBuilder for FakeVmBuilder {
fn build(&self, _vm: &VmBuild, _root: &Path) -> Result<Vec<Artifact>> {
Ok(vec![Artifact { path: "artifacts/bzImage".into(), bytes: 6, sha256: "ab".repeat(32) }])
}
fn engine(&self) -> &str {
"tunnr"
}
}
let vm = VmBuild {
name: "appliance".into(),
kernel: "artifacts/bzImage".into(),
rootfs: "artifacts/rootfs.cpio.gz".into(),
..Default::default()
};
let out = build_vm(&FakeVmBuilder, &vm, "tunnr", Path::new(".")).unwrap();
assert_eq!(out.attestation.record.engine, "tunnr", "engine label comes from the seam impl");
assert_eq!(out.artifacts.len(), 1);
}
struct FailingImageBuilder;
impl ImageBuilder for FailingImageBuilder {
fn build(&self, _image: &ContainerBuild, _root: &Path) -> Result<BuiltImage> {
anyhow::bail!("engine refused: no such base image")
}
}
/// **Assert a target build reached a FAILED outcome, and hand back its reason.**
///
/// Four tests used to do this with `.unwrap_err()` on the call, because a builder
/// error was propagated. Each existed to prove that a missing artifact is never a
/// false green, and under the record-keeping contract that proof gets *stronger*,
/// not weaker: an `Err` carried no record at all, so "it failed honestly" could
/// only ever be read off an error string. It is now read off the sealed record —
/// `Outcome::Failed`, no artifacts — which is the thing the ledger actually
/// stores. One helper rather than four copies, so the four cannot drift.
fn failed_reason(out: Result<TargetBuildOutcome>) -> String {
let out = out.expect("a build that reached an outcome has a record to file");
assert!(!out.success, "a build that produced nothing must not report success");
assert_eq!(
out.attestation.record.outcome,
Outcome::Failed,
"and the SEALED record must say so — this is the field the ledger stores"
);
assert!(out.artifacts.is_empty(), "and must claim no artifact it did not produce");
out.error.expect("a failed build carries its reason")
}
fn broken_container() -> ContainerBuild {
ContainerBuild {
name: "broken".into(),
base: "scratch".into(),
tags: vec!["broken:1".into()],
..Default::default()
}
}
/// **The FAILED-outcome branch KEEPS its record.** This used to assert the
/// opposite — that `build_container` propagates the builder's error — and the
/// error it propagated took the already-captured `Outcome::Failed` attestation
/// with it, so a red image build filed nothing at all while a red cargo build
/// filed normally.
///
/// RED against the old body: `built?` returned `Err` here, so `unwrap()` panics
/// with the builder's message instead of yielding an outcome.
#[test]
fn a_failed_container_build_still_produces_a_record() {
let out = build_container(&FailingImageBuilder, &broken_container(), "repo", Path::new("."))
.expect("a build that reached a terminal outcome has a record to file");
assert!(!out.success, "the outcome must say it failed");
assert_eq!(
out.attestation.record.outcome,
Outcome::Failed,
"and so must the sealed record — this is the field the ledger stores"
);
assert!(
out.artifacts.is_empty(),
"a failed build produced nothing; artifacts must not be invented"
);
// Without this the record would say a build failed and never say why, which
// is the half of the account that makes it worth keeping.
assert!(
out.error.as_deref().unwrap_or_default().contains("no such base image"),
"the builder's reason is carried, not dropped: {:?}",
out.error
);
// The other side. A `capture_target_build` that answered `Failed` to
// everything would pass every assertion above.
let ok_builder = FakeImageBuilder {
built: BuiltImage {
image_ref: "tunnr-appliance:0.1".into(),
digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222"
.into(),
bytes: 4096,
archive: None,
},
};
let good = build_container(&ok_builder, &appliance(), "tunnr", Path::new("."))
.expect("a green build");
assert!(good.success && good.error.is_none(), "a green build carries no error");
assert_eq!(good.attestation.record.outcome, Outcome::Success);
assert_eq!(good.artifacts.len(), 1, "and it does carry its artifact");
}
/// A descriptor that never reaches a builder is still an `Err`, and must be:
/// there is no outcome and nothing to file. Without this, a
/// `capture_target_build` that swallowed *everything* into a `Failed` record
/// would look identical from outside, and `Ok` would stop meaning anything.
#[test]
fn a_descriptor_that_never_builds_is_still_a_hard_error() {
let invalid = ContainerBuild { name: String::new(), ..broken_container() };
let err = build_container(&FailingImageBuilder, &invalid, "repo", Path::new("."))
.expect_err("an invalid descriptor never reached a builder");
assert!(
format!("{err:#}").contains("validate container descriptor"),
"and it fails for THAT reason, not the builder's: {err:#}"
);
}
/// The ISO peer of `build_vm_seals_over_the_produced_...`: a mastered `.iso`
/// on disk is sealed as the produced artifact, and a chained `from_variant`
/// PID-1 binary is sealed FIRST. RED-when-broken: an unstaged `from_variant`
/// (or a missing ISO) is a hard error, never a false green.
#[test]
fn build_iso_seals_over_the_produced_iso_and_from_variant_payload() {
use crate::iso::{IsoBuild, IsoFormat};
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join(".iso-out")).unwrap();
std::fs::write(root.join(".iso-out/korp-installer.iso"), b"ISO9660 bytes").unwrap();
std::fs::create_dir_all(root.join("target/release")).unwrap();
std::fs::write(root.join("target/release/korp-init"), b"KORP PID1 BINARY").unwrap();
let iso = IsoBuild {
name: "korp-installer".into(),
kernel: "artifacts/bzImage".into(),
rootfs: "artifacts/rootfs.cpio.gz".into(),
volume_id: "KORP_DEMO".into(),
format: IsoFormat::Hybrid,
from_variant: Some("korp-pid1".into()),
..Default::default()
};
let staged: BTreeMap<String, PathBuf> =
[("korp-pid1".to_string(), root.join("target/release/korp-init"))].into();
let builder = crate::image_build::PrebuiltIsoBuilder::new().with_variant_artifacts(staged);
let out = build_iso(&builder, &iso, "korp", root).unwrap();
assert_eq!(out.kind, TargetKind::Iso);
assert_eq!(out.artifacts.len(), 2, "the from_variant PID-1 + the mastered .iso");
let pid1 = out.artifacts.iter().find(|a| a.path.ends_with("korp-init")).unwrap();
assert_eq!(pid1.sha256, provenance::hex(&Sha256::digest(b"KORP PID1 BINARY")));
let iso_art = out.artifacts.iter().find(|a| a.path.ends_with(".iso")).unwrap();
assert_eq!(iso_art.sha256, provenance::hex(&Sha256::digest(b"ISO9660 bytes")));
assert!(out.attestation.digest_matches_record().unwrap());
assert_eq!(out.attestation.record.descriptor_id, iso.content_id());
// RED: nothing staged for the from_variant chain → honest hard error.
let unstaged = crate::image_build::PrebuiltIsoBuilder::new();
let msg = failed_reason(build_iso(&unstaged, &iso, "korp", root));
assert!(msg.contains("no built artifact was staged"), "{msg}");
}
/// **End-to-end REAL mastering in PURE RUST** — no external tool required, so
/// this always runs (the whole point: the ISO path shells out to nothing).
/// Masters a bootable UEFI ISO from a tiny staging tree carrying an
/// `efiboot.img` ESP, seals it, and asserts the produced file carries the
/// ISO-9660 `CD001` PVD magic and an El-Torito boot record — a real, on-disk,
/// bootable-structure ISO, not a fabricated blob.
///
/// # What these assertions used to be worth
///
/// Five of the six boot-structure assertions below could not fail. The
/// writer's catalog builder had a `(no BIOS, no EFI) => (lba 0, 4 sectors)`
/// arm and then unconditionally stamped `0x88` bootable / `0x00`
/// no-emulation, so the Boot Record VD, the El-Torito id, the validation
/// header and the `0x55AA` key were emitted for ANY image — including one
/// with no boot image at all. Only `catalog[65] == 0xEF` discriminated.
///
/// Two things fixed that. The writer no longer emits El-Torito structures
/// for an image with nothing bootable (see
/// `an_iso_with_no_boot_image_is_not_presented_as_bootable`), so the five
/// structural assertions now depend on the ESP having really resolved; and
/// the assertions below now read the catalog's **recorded extent and sector
/// count** and go and check the ESP bytes are actually there, rather than
/// checking constants the writer types out either way.
#[test]
fn pure_rust_builder_masters_a_real_bootable_iso() {
use crate::iso::{IsoBuild, IsoFormat};
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
// A minimal UEFI staging tree: the ESP the El-Torito EFI entry boots.
// 8192 B is a deliberate 16 El-Torito virtual sectors — a size the
// catalog must state exactly, and one no default can be mistaken for.
std::fs::create_dir_all(root.join("iso-root")).unwrap();
std::fs::write(root.join("iso-root/efiboot.img"), vec![0xEFu8; 8192]).unwrap();
std::fs::write(root.join("iso-root/hello.txt"), b"korp basemap placeholder").unwrap();
let iso = IsoBuild {
name: "korp-data".into(),
staging: "iso-root".into(),
volume_id: "KORP_DATA".into(),
format: IsoFormat::Uefi,
..Default::default()
};
let builder = PureRustIsoBuilder::new();
let out = build_iso(&builder, &iso, "korp", root)
.unwrap_or_else(|e| panic!("pure-rust master failed: {e:#}"));
assert_eq!(out.kind, TargetKind::Iso);
assert_eq!(out.attestation.record.engine, "pure-rust");
let iso_art = out.artifacts.iter().find(|a| a.path.ends_with(".iso")).expect("mastered .iso");
assert!(iso_art.bytes > 0, "the produced ISO is non-empty");
let produced = root.join(".iso-out/korp-data.iso");
assert!(produced.is_file(), "a REAL .iso was mastered at {}", produced.display());
let bytes = std::fs::read(&produced).unwrap();
// PVD "CD001" magic at sector 16.
const S: usize = crate::iso_writer::SECTOR;
assert_eq!(&bytes[16 * S + 1..16 * S + 6], b"CD001", "ISO-9660 PVD magic present");
// El-Torito boot record VD at sector 17.
assert_eq!(bytes[17 * S], 0, "El-Torito boot record VD type");
assert!(bytes[17 * S + 7..17 * S + 30].starts_with(b"EL TORITO SPECIFICATION"), "El-Torito id");
// Boot catalog validation entry key 0x55AA at its pointer.
let cat_lba = u32::from_le_bytes([bytes[17 * S + 71], bytes[17 * S + 72], bytes[17 * S + 73], bytes[17 * S + 74]]) as usize;
assert_eq!(bytes[cat_lba * S], 0x01, "El-Torito validation entry header");
assert_eq!(bytes[cat_lba * S + 30], 0x55);
assert_eq!(bytes[cat_lba * S + 31], 0xAA);
assert_eq!(bytes[cat_lba * S + 65], 0xEF, "El-Torito EFI (0xEF) section present");
// ── the catalog must describe the ESP THAT WAS STAGED ──────────────
// Applied output: read the section entry's recorded sector count and
// load RBA, then go and look at what is there. A catalog that named a
// plausible LBA and a default sector count satisfied every assertion
// above while pointing firmware at nothing.
let cat = cat_lba * S;
let efi_sectors = u16::from_le_bytes([bytes[cat + 102], bytes[cat + 103]]);
assert_eq!(
efi_sectors, 16,
"the 8192 B ESP is exactly 16 El-Torito 512 B virtual sectors; {efi_sectors} means \
the catalog is describing something other than the image that was staged"
);
let efi_lba = u32::from_le_bytes([
bytes[cat + 104], bytes[cat + 105], bytes[cat + 106], bytes[cat + 107],
]) as usize;
assert!(efi_lba >= 22, "the ESP cannot live inside the fixed descriptors (LBA {efi_lba})");
assert_eq!(
&bytes[efi_lba * S..efi_lba * S + 8192],
&vec![0xEFu8; 8192][..],
"the ESP bytes must be readable back at the extent the boot catalog points firmware \
at — this is the assertion the old `(None, None) => (0, 4)` arm could never fail"
);
// The UEFI-only default/initial (80x86) entry names something real AND
// honest: NOT the ESP (until 2026-08-28 it did, and SeaBIOS executed FAT
// bytes as code — gunnar's "colour confetti", UpCloud's silence) and not
// LBA 0 — the writer's one-sector BIOS refusal stub, which prints a
// sentence and halts. See `iso_writer::bios_refusal_sector`.
assert_eq!(bytes[cat + 32], 0x88, "default entry bootable");
let def_lba =
u32::from_le_bytes([bytes[cat + 40], bytes[cat + 41], bytes[cat + 42], bytes[cat + 43]]) as usize;
assert_ne!(def_lba, efi_lba, "the default entry must not hand a BIOS the FAT ESP to execute");
assert_ne!(def_lba, 0, "with no BIOS image the default entry must not fall back to LBA 0");
assert_eq!(
u16::from_le_bytes([bytes[cat + 38], bytes[cat + 39]]),
1,
"the refusal stub is one 512 B virtual sector"
);
assert_eq!(
&bytes[def_lba * S..def_lba * S + 512],
&crate::iso_writer::bios_refusal_sector()[..],
"the default entry loads the UEFI-only refusal stub"
);
eprintln!(
"MASTERED a real {}-byte bootable ISO in PURE RUST at {} (no xorriso, no subprocess)",
iso_art.bytes, produced.display()
);
}
/// **A declared `staging` tree that is not on disk is a hard, named error.**
///
/// It used to be skipped in silence: `if staging.is_dir() { … }` with no
/// `else`. Measured through the real forge on 2026-08-07 — `staging =
/// "no-such-staging-dir"` mastered a valid 47 104-byte ISO with nothing in
/// it and reported `BUILT … 1 artifact(s)`, exit 0, against 49 152 bytes for
/// the correct build. The whole difference between "the ISO carries the
/// payload" and "the ISO is empty" was 2 048 bytes and no verdict change at
/// all, which is the worst shape a build failure can have: it is discovered
/// by whoever boots the media.
///
/// A declaration is a claim that the tree exists. Asserted on the applied
/// output — the error text AND the absence of the file — because an ISO that
/// was written and then reported as failed is still an ISO somebody can pick
/// up.
#[test]
fn a_declared_staging_tree_that_is_missing_is_refused_not_silently_skipped() {
use crate::iso::{IsoBuild, IsoFormat};
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
// A real payload EXISTS at the conventional place, so nothing else in
// this build is broken — only the declared staging tree is absent.
std::fs::create_dir_all(root.join("artifacts")).unwrap();
std::fs::write(root.join("artifacts/bzImage"), b"BZIMAGE").unwrap();
let iso = IsoBuild {
name: "korp-ghost".into(),
kernel: "artifacts/bzImage".into(),
staging: "no-such-staging-dir".into(),
volume_id: "KORP_GHOST".into(),
format: IsoFormat::Bios,
..Default::default()
};
let msg = failed_reason(build_iso(&PureRustIsoBuilder::new(), &iso, "korp", root));
assert!(
msg.contains("no-such-staging-dir"),
"the refusal must name the tree that was declared and is not there: {msg}"
);
assert!(
msg.contains("staging"),
"…and say it is the staging declaration that is unsatisfied: {msg}"
);
assert!(
!root.join(".iso-out/korp-ghost.iso").exists(),
"an ISO that could not carry its declared tree must not be on disk at all"
);
// GREEN control: the same descriptor with the tree actually present
// masters, so this is a check on the missing tree and not a refusal of
// every staging declaration.
std::fs::create_dir_all(root.join("no-such-staging-dir")).unwrap();
std::fs::write(root.join("no-such-staging-dir/data.bin"), b"REAL STAGED DATA").unwrap();
build_iso(&PureRustIsoBuilder::new(), &iso, "korp", root)
.unwrap_or_else(|e| panic!("a staging tree that IS there must master: {e:#}"));
let bytes = std::fs::read(root.join(".iso-out/korp-ghost.iso")).unwrap();
assert!(
bytes.windows(16).any(|w| w == b"REAL STAGED DATA"),
"the staged bytes must actually be inside the mastered ISO — an ISO that merely \
exists is exactly the false green this test was written for"
);
}
/// **Two descriptors that master different bytes must not share one
/// `content_id`.**
///
/// `IsoBuild::canonical_bytes` was taken over `master_args`, and
/// `master_args` renders no argument for `rootfs` — under `xorriso` the
/// rootfs rode in on the staging tree. The pure-Rust masterer places it
/// itself (`w.add_file(&iso.rootfs, …)`), so `rootfs` changes the mastered
/// image and did not change the id: two different inputs, one id, and a
/// content-addressed cache that hands back the wrong ISO.
///
/// Asserted the only way that means anything — MASTER both, prove the bytes
/// differ, and only then require the ids to differ. An id test that compares
/// two descriptors without ever building them cannot tell a field that
/// matters from a field that does not.
#[test]
fn content_id_covers_every_declared_input_that_changes_the_mastered_bytes() {
use crate::iso::{IsoBuild, IsoFormat};
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("artifacts")).unwrap();
std::fs::write(root.join("artifacts/bzImage"), b"BZIMAGE-BYTES").unwrap();
std::fs::write(root.join("artifacts/rootfs-a.cpio.gz"), b"ROOTFS-A-PAYLOAD").unwrap();
std::fs::write(root.join("artifacts/rootfs-b.cpio.gz"), b"ROOTFS-B-PAYLOAD-DIFFERENT").unwrap();
let base = IsoBuild {
name: "korp-rootfs".into(),
kernel: "artifacts/bzImage".into(),
volume_id: "KORP_ROOTFS".into(),
format: IsoFormat::Bios,
..Default::default()
};
let a = IsoBuild { rootfs: "artifacts/rootfs-a.cpio.gz".into(), ..base.clone() };
let b = IsoBuild { rootfs: "artifacts/rootfs-b.cpio.gz".into(), ..base.clone() };
let master = |iso: &IsoBuild, out: &str| -> Vec<u8> {
let out = root.join(out);
PureRustIsoBuilder::new()
.with_output(out.clone())
.build(iso, root)
.unwrap_or_else(|e| panic!("master {}: {e:#}", out.display()));
std::fs::read(&out).unwrap()
};
let iso_a = master(&a, "a.iso");
let iso_b = master(&b, "b.iso");
// The premise: `rootfs` really is an input to the produced bytes.
assert_ne!(
iso_a, iso_b,
"the two rootfs payloads must reach the ISO, or this test is asserting nothing"
);
assert!(iso_a.windows(16).any(|w| w == b"ROOTFS-A-PAYLOAD"), "rootfs A is on its ISO");
assert!(!iso_b.windows(16).any(|w| w == b"ROOTFS-A-PAYLOAD"), "…and only on its own");
// …therefore the content ids must differ.
assert_ne!(
a.content_id(),
b.content_id(),
"two descriptors that master DIFFERENT bytes shared one content_id — a \
content-addressed build keyed on this hands back the wrong ISO"
);
// The per-ISO customer boot-logo is an input too: it is exported as
// KORP_BOOT_LOGO to the chained cargo build, so it changes the binary
// that rides the ISO.
let plain = IsoBuild { from_variant: Some("korp-pid1".into()), ..base.clone() };
let marked = IsoBuild { boot_logo: Some("assets/customer.webp".into()), ..plain.clone() };
assert_ne!(
plain.content_id(),
marked.content_id(),
"a customer boot-logo bakes a different payload binary, so it cannot leave the id \
unchanged — two customers' ISOs would be one cache entry"
);
// …and the id is still stable for a descriptor that did not change, so
// this is a widening and not a randomization.
assert_eq!(a.content_id(), a.clone().content_id());
}
/// A UEFI ISO whose staging tree lacks the `efiboot.img` ESP is an honest hard
/// error — the writer refuses to fabricate a boot image, mirroring the old
/// "no masterer ⇒ loud error, never a faked ISO" honesty contract.
#[test]
fn pure_rust_builder_errors_when_efi_boot_image_missing() {
use crate::iso::{IsoBuild, IsoFormat};
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("iso-root")).unwrap();
std::fs::write(root.join("iso-root/hello.txt"), b"no ESP here").unwrap();
let iso = IsoBuild {
name: "korp-noefi".into(),
staging: "iso-root".into(),
volume_id: "KORP".into(),
format: IsoFormat::Uefi,
..Default::default()
};
let msg = failed_reason(build_iso(&PureRustIsoBuilder::new(), &iso, "korp", root));
assert!(msg.contains("efiboot.img") || msg.contains("boot image"), "honest ESP reason: {msg}");
assert!(!root.join(".iso-out/korp-noefi.iso").exists(), "no faked ISO written");
}
/// ★ RED-FIRST — **the rootless socket is the RUNNING uid's, never uid
/// 1000's.** kit-defects (3): with no `DOCKER_HOST` and no
/// `XDG_RUNTIME_DIR` (a systemd system unit with `User=`, an `ssh host cmd`
/// without a login session) the builder looked in `/run/user/1000` — another
/// account's runtime directory on any box whose build user is not the
/// desktop's first user. The ladder is DOCKER_HOST, then XDG_RUNTIME_DIR, then
/// `/run/user/<real uid>`; with none of the three it refuses by name.
#[test]
fn the_socket_follows_the_running_uid_not_uid_1000() {
// DOCKER_HOST wins outright.
assert_eq!(
podman_socket_url_from(Some("unix:///srv/p.sock"), Some("/run/user/7"), Some(7)).unwrap(),
"unix:///srv/p.sock"
);
// XDG_RUNTIME_DIR is the session's own answer.
assert_eq!(
podman_socket_url_from(None, Some("/run/user/4242"), Some(4242)).unwrap(),
"unix:///run/user/4242/podman/podman.sock"
);
// No XDG_RUNTIME_DIR: the REAL uid's runtime dir — 111 is korp on oden.
assert_eq!(
podman_socket_url_from(None, None, Some(111)).unwrap(),
"unix:///run/user/111/podman/podman.sock",
"a uid-111 build must not look in uid 1000's runtime dir"
);
// An empty variable is unset, not the filesystem root.
assert_eq!(
podman_socket_url_from(Some(""), Some(""), Some(111)).unwrap(),
"unix:///run/user/111/podman/podman.sock"
);
// Nothing to go on: refused by name, never a guessed uid.
let e = podman_socket_url_from(None, None, None).unwrap_err().to_string();
assert!(e.contains("XDG_RUNTIME_DIR") && e.contains("DOCKER_HOST"), "named refusal: {e}");
assert!(!e.contains("/run/user/1000"), "no guessed uid in the refusal: {e}");
}
/// The real uid is the first field of `/proc/self/status`'s `Uid:` line.
#[test]
fn the_real_uid_is_read_off_the_status_text() {
let status = "Name:\tx\nUmask:\t0002\nUid:\t111\t222\t333\t444\nGid:\t1\t1\t1\t1\n";
assert_eq!(real_uid_from_status(status), Some(111));
assert_eq!(real_uid_from_status("Name:\tx\n"), None);
assert_eq!(real_uid_from_status("Uid:\tnope\n"), None);
}
/// The default container engine label stays `bollard`.
#[test]
fn build_container_default_engine_label_is_bollard() {
let known = BuiltImage { image_ref: "x:1".into(), digest: "sha256:aa".into(), bytes: 1, archive: None };
let out = build_container(
&FakeImageBuilder { built: known },
&ContainerBuild { name: "x".into(), base: "scratch".into(), tags: vec!["x:1".into()], ..Default::default() },
"repo",
Path::new("."),
)
.unwrap();
assert_eq!(out.attestation.record.engine, "bollard");
}
/// ★ RED-FIRST — **the digest a pin is READ from, and the refusal when
/// there is none.** [`manifest_digest_of`] is the whole of what
/// [`BollardImageBuilder::manifest_digest`] answers with, lifted out of the
/// socket so the behaviour that becomes a `[build.toolchain] image_sha256`
/// line is provable without an engine.
///
/// The case that matters is `None`. An image the store holds but never
/// pushed or pulled carries NO `RepoDigests` — it has an id and nothing
/// else. The build path is allowed to shrug and record the id, because a
/// provenance record must say something about what it built; the pin path
/// is NOT, because an id pasted where a manifest digest belongs is refused
/// later by `CargoToolchain::validate` ("must be 64 lowercase hex … the
/// MANIFEST digest"), and answering one here would only move the refusal to
/// a place with less context. So `manifest_digest_of` returns `None` and the
/// caller refuses BY NAME, naming the fix — it never falls back.
#[test]
fn a_missing_repo_digest_yields_no_pin_rather_than_a_plausible_one() {
// The ordinary case: `repo@sha256:…` → the digest half.
let d = manifest_digest_of(Some(&[
"localhost/nordisk-cargo-toolchain@sha256:2cf9b012c9d2ffa42b447e5b0d127184e5a3f998dd2574d771477e9c8e366659".to_string(),
]));
assert_eq!(
d.as_deref(),
Some("sha256:2cf9b012c9d2ffa42b447e5b0d127184e5a3f998dd2574d771477e9c8e366659")
);
// …and the bare hex a declaration carries is that, minus the prefix —
// the exact shape `CargoToolchain::validate` demands.
let hex = d.unwrap().trim_start_matches("sha256:").to_string();
assert_eq!(hex.len(), 64, "{hex}");
assert!(hex.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)), "{hex}");
// A locally built image: the engine reports no repo digests at all.
assert_eq!(manifest_digest_of(None), None, "no pin may be invented from nothing");
assert_eq!(manifest_digest_of(Some(&[])), None, "an empty list is not a digest");
// An id where a repo digest belongs has no `@`, so it is not silently
// read as a digest either — this is the shape that actually happened.
assert_eq!(
manifest_digest_of(Some(&[
"72abf41e32e24d1d0423598f4040b72bddaee5c5a62213edb188a513f19be669".to_string()
])),
None,
"an image id is not a manifest digest"
);
}
/// #4 — a `from_variant` layer is staged into the build context under the
/// variant name (matching the Containerfile's `COPY ./<variant>`), so a
/// variant-chained image build actually finds its payload.
#[cfg(feature = "image-build")]
#[test]
fn tar_context_stages_from_variant_artifacts() {
use std::io::Read;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let art = root.join("target/release/tunnr");
std::fs::create_dir_all(art.parent().unwrap()).unwrap();
std::fs::write(&art, b"BINARY").unwrap();
let img = appliance(); // layer: from_variant("full", "/usr/local/bin/tunnr")
let mut staged = BTreeMap::new();
staged.insert("full".to_string(), art.clone());
let tar_bytes = BollardImageBuilder::tar_context(&img, root, &staged).unwrap();
let mut ar = tar::Archive::new(&tar_bytes[..]);
let mut names = Vec::new();
let mut full_bytes = Vec::new();
for entry in ar.entries().unwrap() {
let mut e = entry.unwrap();
let name = e.path().unwrap().to_string_lossy().to_string();
if name == "full" {
e.read_to_end(&mut full_bytes).unwrap();
}
names.push(name);
}
assert!(names.contains(&"Dockerfile".to_string()));
assert!(names.contains(&"full".to_string()), "from_variant staged under variant name: {names:?}");
assert_eq!(full_bytes, b"BINARY", "the staged bytes are the produced artifact");
}
/// #4 — an unstaged `from_variant` layer is an honest error, not a silently
/// broken build context.
#[cfg(feature = "image-build")]
#[test]
fn tar_context_errors_when_variant_not_staged() {
let dir = tempfile::tempdir().unwrap();
let img = appliance();
let empty = BTreeMap::new();
let err = BollardImageBuilder::tar_context(&img, dir.path(), &empty).unwrap_err();
assert!(format!("{err:#}").contains("no built"), "honest error: {err:#}");
}
}