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

nordisk/edda

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

//! # nornir-build-thing
//!
//! Declarative **cargo build variants** + **build-provenance** attestation
//! (task #36) — the reusable, dependency-light core of nornir's build-thing,
//! extracted so a leaf repo's `xtask` can pull it without nornir's iceberg /
//! arrow / eframe stack (mirrors how `nornir-testmatrix` is a leaf crate).
//!
//! # The vocabulary
//!
//! **Ruled 2026-08-15. There is ONE word for "a declared buildable", and it is
//! `variant`.** Read this before adding a noun.
//!
//! * **`variant`** — the NOUN. One declared buildable; one row in the build list.
//!   It has a **name**, and the name is the handle a human types
//!   (`gunnar-installer`, `holger-appliance`).
//! * **`kind`** — a **FIELD on that variant**, naming the shape of artifact it
//!   produces. Not a noun, not a section, not a second identity.
//!
//! The seven kinds:
//!
//! | `kind` | what comes out |
//! |---|---|
//! | `exe` | a native executable (the default) |
//! | `service` | an executable that ALSO wraps as a skidbladnir `ServiceSpec` |
//! | `container` | an OCI / appliance image |
//! | `vm` | a bootable VM / appliance |
//! | `iso` | a bootable ISO-9660 image — the ISO *is* the appliance |
//! | `installer` | an installer ISO — it *produces* an appliance on a disk |
//! | `kernel` | a built Linux kernel (`bzImage` + `.config` + sha256 pin file), compiled from a pinned tarball in a toolchain container under a named hardening profile |
//!
//! (`lib` and `wasm` also exist in [`ArtifactKind`] as cargo output shapes; the seven
//! above are the *deliverable* kinds the ruling enumerates.)
//!
//! Worked example — gunnar declares five variants, five names, four kinds:
//! `gunnar-server` (service), `gunnar-cli` (exe), `gunnar-image` (container),
//! `gunnar-appliance-iso` (iso), `gunnar-installer` (installer).
//!
//! ## `[[build.container]]` & friends are PERMANENT SUGAR — do not "clean them up"
//!
//! A declaration may spell its kind either way, and both are correct FOREVER:
//!
//! ```toml
//! [[build.installer]]           # the section spelling …
//! name = "site-installer"
//!
//! [[build.variant]]             # … and the field spelling. Same thing.
//! name = "site-installer"
//! kind = "installer"
//! ```
//!
//! `[[build.container]]`, `[[build.vm]]`, `[[build.iso]]`,
//! `[[build.installer]]` and `[[build.kernel]]` are **sugar for `kind = "…"`**. They are kept because
//! every existing `.nornir/build-variants.toml` in the suite is written that way
//! and a repo that never edits its file must behave EXACTLY as before. They are
//! also where the rich per-kind payload lives ([`ContainerBuild`], [`VmBuild`],
//! [`IsoBuild`], [`InstallerBuild`]) — an ISO's boot payload and an image's layer
//! list are genuinely different data, and they are NOT to be flattened into one
//! god-struct. Nobody may retire these sections.
//!
//! ## Retired synonyms: "atom", "cell"
//!
//! **"atom" and "cell" are RETIRED.** They used to compete with `variant` for the
//! same noun — "the `dwarves-server` atom", "an `[[build.installer]]` cell" — and
//! having three words for one thing is what let `BuildVariantSet::get()` search one
//! of five collections for years while every caller believed it searched the build
//! list. They survive only in historical comments, in kept API field names
//! (`BuildArgs.atom`), and as DEPRECATED CLI aliases: since 2026-08-20 the flag is
//! `--variant`, with `--atom` accepted forever because the written laws cite it
//! (`cargo xtask build --suite ORIGINAL --atom dwarves-server`) and muscle memory
//! is not to be broken. Do not introduce either word in new
//! prose, new APIs, or new diagnostics.
//!
//! ## Asking the set anything
//!
//! [`BuildVariantSet`] stores its rows in six collections (`variants`,
//! `containers`, `vms`, `isos`, `installers`, `kernels`) because the payload types differ.
//! That is STORAGE. Every unified question goes through the single enumeration
//! [`BuildVariantSet::declarations`] and its derivatives —
//! [`declared`](BuildVariantSet::declared),
//! [`declared_names`](BuildVariantSet::declared_names),
//! [`declared_count`](BuildVariantSet::declared_count),
//! [`kind_of`](BuildVariantSet::kind_of), [`artifacts`](BuildVariantSet::artifacts)
//! — which yield [`Declared`] rows. Five parallel searches at a call site is the
//! twin this crate forbids: it is exactly how a declared installer became
//! unfindable by name.
//!
//! ## What a repo declares
//!
//! A product like holger ships **three split executables from one tree** —
//! `embedded`, `minimal`, `full` — each a distinct `(package × bin × features ×
//! profile × target)` point. It declares them as data ([`BuildVariantSet`], a
//! `[build]` / `[[build.variant]]` TOML table). nornir validates a chosen
//! variant, materializes the exact `cargo build` argv, and runs it.
//!
//! ## The headline call
//!
//! ```no_run
//! use nornir_build_thing::{BuildVariantSet, build_variant};
//! # fn main() -> anyhow::Result<()> {
//! let set = BuildVariantSet::discover(std::path::Path::new("."))?.unwrap_or_default();
//! let outcome = build_variant(&set, "full", std::path::Path::new("."))?;
//! // `outcome.attestation` is a content-addressed build-provenance record
//! // (variant, inputs, toolchain, host cores+memory, artifact hashes).
//! println!("{}", outcome.attestation.digest_hex());
//! # Ok(()) }
//! ```
//!
//! ## Modules
//!
//! * [`variant`] — the cargo-build descriptor, validator, and cargo materializer.
//! * [`container`] — the **image / appliance** build target kind
//!   ([`ContainerBuild`]): a declarative OCI/appliance image (base, layers,
//!   entrypoint) that materializes a Containerfile + builder argv. A tunnr
//!   appliance image and a desktop-app image both express here; a layer can
//!   chain a cargo [`variant`]'s artifact via [`ImageLayer::from_variant`].
//! * [`provenance`] — the [`BuildProvenance`] record, [`Attestation`], the
//!   [`ProvenanceSealer`] seam, and [`build_variant`] → [`BuildOutcome`].
//! * [`image_build`] — the IMAGE-target peer of [`build_variant`]:
//!   [`build_container`] / [`build_vm`] BUILD a [`ContainerBuild`] / [`VmBuild`]
//!   through a builder seam and seal a [`TargetAttestation`] over the PRODUCED
//!   artifact (image digest / kernel+rootfs sha256), not the descriptor hash. The
//!   heavy real container builder (bollard) is opt-in behind the `image-build`
//!   feature; the default crate ships only the seam + the dep-light VM path.
//! * [`linkage`] — the `linkage = "thin"` declaration CHECKED against the built
//!   binary ([`LinkageAudit`]): a thin artifact must carry no server entry point
//!   and no storage engine, an unmeasured audit never passes, and a fat one is
//!   never failed for being fat (only for having stopped being fat).
//! * [`surface`] — the data-driven UI surface a viz tab / CLI `--json` renders.
//! * [`sink`] — the [`BuildProvenanceSink`] warehouse seam + dep-light built-ins.
//! * [`elm`] — the build-thing UI component (facett Elm pure core:
//!   Model/Msg/update).

pub mod analyze;
pub mod assemble;
/// **`--build <X>` and the edge DAG** — the grammar of a chained build (the
/// path prefix IS the brace: `--build kernel { --opt unix=off }` ≡ `--opt
/// kernel.unix=off`) and the ordered steps a variant's typed consumption edges
/// imply ([`build_dag::BuildDag`]), donors first; each step's `Display` is the
/// exact CLI line the runner runs. Data only — nothing here builds. See
/// [`build_dag`].
pub mod build_dag;
pub mod build_info;
/// **The build gate** — compile everything the repo declares and go RED *with the
/// numbers* when fewer things came out than went in. See [`buildgate`].
pub mod buildgate;
pub mod capability_probe;
/// **The cargo kind builds in a CONTAINER** — the leg it did not have. Every
/// other build entry point in this crate spawns `cargo` in the host's
/// environment, so the rustc version, the installed target set, the C compiler
/// and `~/.cargo/config.toml`'s linker choice decided the outcome (MEASURED
/// 2026-08-25: `error[E0463]: can't find crate for core` on a box without musl
/// std, for a variant that builds fine elsewhere). This is the pinned-image
/// seam that removes the host from the answer, shaped like [`kernel`]'s so the
/// constellation has one container pattern and not two. See [`cargo_container`].
pub mod cargo_container;
/// **The container road, whole**: enter the pinned image once, build a declared
/// cargo row in it, and run its `[[build.wasm]]` stages (wasm-bindgen, the
/// component manifest) in the SAME image — never the host's tools. Lifted from
/// facett's xtask when nornir and dwarves (`nornir_pages::demo_wasm`) wanted it.
#[cfg(all(feature = "cargo-container", feature = "wasm-stage"))]
pub mod container_road;
/// **The bridge between the binary that was audited and the one that ships** —
/// a SHA-256 over the allocated sections alone, so a stripped artifact and the
/// unstripped binary it came from measurably carry the same program. See
/// [`code_digest`].
pub mod code_digest;
pub mod container;
pub mod consequence;
pub mod contract;
pub mod data;
pub mod elm;
/// `code → toml`: the ONE emitter every repo's `<repo>-build-thing emit` calls.
pub mod emit;
pub mod feature_axis;
pub mod flags;
pub mod gating;
/// **The constellation inventory** — every declared build thing as one crisp
/// 3-col row, rendered from the declarations (and the settings tree's own
/// flags) so a committed table is a RENDER of this and never a source.
pub mod inventory;
/// **The build history — `znippy-build-thing`.** The attestation package dwarves
/// writes AFTER a build and BEFORE the release: *recipe R with parameters P
/// produced artifact hash H*. Derived, never a source of truth — its only
/// constructor is the regeneration path. See [`history`].
pub mod history;
/// **The legacy-BIOS stage patch table** — the contract between the 16-bit
/// El-Torito loader (`korp-boot-bios`) and [`iso_writer::IsoWriter::set_bios_stage`],
/// which fills it with the kernel's and initramfs' placements at mastering time.
pub mod bios_patch;
pub mod image_build;
pub mod installer;
pub mod iso;
pub mod iso_writer;
/// **The cloud MEDIUM writers** — the peer of [`iso_writer`] for every platform
/// that does NOT take an attached ISO. An already-installed raw disk becomes a
/// fixed-size Azure VHD, an AWS raw import, GCE's `disk.raw` in a gzipped tar,
/// or a sparse qcow2 v3 for OVH/Scaleway/OpenStack — in pure Rust, no
/// `qemu-img`, no `tar(1)`, no `gzip(1)`, no subprocess at all. This is what
/// makes `MediumBring::{ImportRaw, ImportQcow2, ImportVhd, Several}` reachable:
/// they were declared in [`target::presets`] and built by nothing. See
/// [`image_writer`].
///
/// Host-only: a build-thing plugin compiled to wasm declares and validates, it
/// converts no disk images.
#[cfg(not(target_arch = "wasm32"))]
pub mod image_writer;
/// **The built-kernel build target kind** — a `[[build.kernel]]` variant compiles
/// a PINNED vanilla Linux tarball inside a toolchain container under a named
/// hardening profile (`vanilla` / `hardened` / `k8s-hardened`, committed kconfig fragments in this
/// crate), producing `bzImage` + `.config` + a sha256 pin file. See [`kernel`].
pub mod kernel;
pub mod kernel_features;
pub mod kconfig_graph;
pub mod kernel_requires;
/// **The measured kernel trim, static half** (tunnr 1.1): what-if a row drops
/// `CONFIG_X`, and the ranked trim candidates of a composed row kernel.
pub mod kernel_trim;
/// **The measure step — system calls** (Heimdall tooling plan phase 1): every
/// program of a row probed by the EXTERNAL `heimdall` tool while the row's
/// runs execute; seccomp profiles written beside the row; `--check` = drift.
/// A process boundary: this crate never links the AGPL heimdall crates.
pub mod measure;
pub mod requires;
/// **The join: measured system calls ↔ kernel options** (Heimdall tooling
/// plan step 3): a syscall → `CONFIG_*` map read off the composed kernel
/// tree, `kernel needs <row>`, and the veto `kernel what-if`/`candidates`
/// apply (a drop that would ENOSYS a measured call is refused by name).
pub mod syscall_join;
/// The TOOLCHAIN-IMAGE surface — the second sub build-thing a consumer may
/// place requirements on (lane AF, 2026-08-30). Same closure, same union, same
/// refusal, its own inverted index.
pub mod toolchain_requires;
/// The OFFLINE toolchain courier (`#14`, lane AO 2026-08-30) — a staged
/// `docker-archive` of the kernel toolchain image, content-addressed in the same
/// cache as the kernel tarball, verified against the declared pin BEFORE the
/// build container is created, and refused by name when it is absent or holds
/// something else. What turns "a kernel build needs a registry" into "a kernel
/// build needs a file somebody put there on purpose".
pub mod toolchain_archive;
/// **THE BIJECTION LAWS** — the conformance gate every [`plugin::BuildThing`]
/// implementation's own test suite must call ([`laws::assert_bijective`]):
/// declaration ↔ struct, menu ↔ declaration, option row ↔ CLI flag,
/// decision ↔ fragment, receipt → recipe. One declaration, many surfaces, all
/// mutually recoverable — so no surface can drift. See [`laws`].
pub mod laws;
pub mod lfs;
pub mod linkage;
/// **The media gate's shared words** (lane B4): a medium's kind baked onto it and read
/// back, and the clean pinned trees it is composed from. See [`media_gate`].
pub mod media_gate;
pub mod options;
pub mod output;
/// **A produced FILE SET as a build target** — the `kind = "package"` cell: the
/// declaration, the member set that makes a release checkable, and the ONE
/// `SHA256SUMS` writer every door routes through. See [`package`].
pub mod package;
/// **The wasm cell and its TYPED post-build stages** — wasm-bindgen and the
/// component manifest, declared rather than remembered. See [`wasm`].
pub mod wasm;
pub mod web_app;
pub mod params;
pub mod plugin;
#[cfg(feature = "plugin-host")]
pub mod plugin_host;
pub mod progress;
pub mod provenance;
/// **The receipt made real** — the `build.rs` writer that puts it in
/// [`contract::RECEIPT_SECTION`], and the object-parser reader that lifts it back
/// out without running the artifact. See [`receipt`].
pub mod receipt;
/// **A declared pattern, PROVEN** — the receipt's claims measured against the
/// built binary's symbol table, so a `gpu = false` that links wgpu is failed
/// rather than believed. See [`receipt_proof`].
pub mod receipt_proof;
pub mod selftest;
pub mod service;
pub mod sign;
/// **The recipe format's compatibility rule** — one file, five readers, four
/// pin generations: how an additive key stops being either an outage or a
/// silence.
pub mod schema;
pub mod sink;
pub mod spec;
/// The ONE `cargo xtask build --suite/--variant` verb every member repo delegates to.
pub mod suite_cli;
pub mod surface;
pub mod target;
pub mod tree;
pub mod variant;
pub mod verbs;
pub mod vm;

pub use capability_probe::{
    Capability, DEPLOYABLES, HollowBinary, Presence, ProbeReport, capabilities_for, gate_binary,
    probe,
};
pub use container::{BaseRecipe, ContainerBuild, ImageFormat, ImageLayer};
pub use feature_axis::{ConstellationRot, FeatureAxis, FeatureRot, gate_all, manifest_features};
pub use image_build::{
    BuiltImage, ImageBuilder, IsoImageBuilder, PrebuiltIsoBuilder, PrebuiltVmBuilder,
    PureRustIsoBuilder, TARGET_PROVENANCE_SCHEMA, TargetAttestation, TargetBuildOutcome,
    TargetKind, TargetProvenance, VmImageBuilder, build_container, build_iso, build_iso_as, build_vm,
    APPLIANCE_SEED_FIXTURE_ENV, FIXTURE_ISO_SUFFIX, appliance_seed_fixture_requested,
    iso_out_stem, iso_output_path,
};
pub use installer::{ContainerBackend, InstallerBuild, PayloadKind};
pub use iso::{IsoBuild, IsoFormat};
// `cargo_container::build_script` is deliberately NOT re-exported here: the
// kernel kind already owns that name at this level, and two `build_script`s in
// one namespace is exactly the "second spelling" the kinds are supposed to avoid.
// Call it path-qualified.
pub use cargo_container::{
    ByteIdentity, CARGO_TOOLCHAIN_CONTAINERFILE, CARGO_TOOLCHAIN_IMAGE, CargoContainerRunner,
    CargoContainerRun, CargoInvocation, CargoRunOutcome, CargoRunRequest, CargoToolchain, HostPaths,
    ParsedRun, TOOLCHAIN_IMAGE_OVERRIDE_ENV, apply_image_override, build_variant_in_container, build_variant_in_container_diagnosed, ContainerDiagnostics, CARGO_TOOLCHAIN_PIN, ToolchainPin,
    byte_identity_verdict, parse_container_stdout, run_cargo_in_container, sibling_closure, sibling_clone_urls, metadata_sibling_closure, variant_manifest_root,
    toolchain_with_override,
};
pub use kernel::{
    ContainerKernelBuilder, HardeningProfile, KernelBuild, KernelBuilder, KernelContainerRunner,
    KernelPin, KernelResolution, KernelRunOutcome, KernelRunRequest, TarballFetcher,
    build_kernel_with, build_script, chosen_kernel_for, kernel_build_of_step, kernel_build_step,
    kernel_output_dir, kernel_step_pin, merge_kconfig, merged_fragment, resolve_kernel,
    set_for_kernel_step,
};
pub use toolchain_archive::{
    ARCHIVE_MISMATCH, StagedImage, TOOLCHAIN_MISSING, ToolchainImageStore, ToolchainResolution,
    ToolchainSource, image_id_ref, resolve_toolchain, stage_toolchain_archive,
    toolchain_archive_entry,
};
pub use build_dag::{
    BUILD_VERB, BesideCheckouts, BuildDag, BuildLine, BuildStep, Declarations,
    InMemoryDeclarations, StepPin, Via, door_path, resolve_build_target,
};
pub use options::{
    AppliedOption, OptionAudit, OptionBake, OptionSpec, ResolvedOptions, parse_cli_options,
    resolve_options, validate_options,
};
pub use provenance::{
    Artifact, Attestation, BUILD_TYPE_VARIANT_V1, BuildOutcome, BuildProvenance, DSSE_PAYLOAD_TYPE_INTOTO,
    DsseSeal, DsseSealer, HostInfo, Inputs, Outcome, PROVENANCE_SCHEMA, ProvenanceSealer, Seal,
    SourceRev, Toolchain, build_variant, build_variant_observed,
    build_variant_observed_with_info, build_variant_with_info,
};
pub use vm::{VmBuild, VmFormat};
// The `linkage` declaration CHECKED against the produced binary: a thin claim that
// carries a server entry point or the storage engine is a lie, and an audit that
// could not read the artifact is never a pass. Promoted out of `dwarves` so holger's
// UI tier guard can consume it without dev-depending on the whole forge.
pub use linkage::{LinkageAudit, LinkageVerdict, THIN_FORBIDDEN_SYMBOLS, human_bytes};
// NB `plugin::BuildThing` (the app-implements-its-own-build-thing TRAIT) is
// deliberately NOT re-exported at the crate root: `elm::BuildThing` (the UI
// component) already holds that name here, and two different things called
// `BuildThing` one `use` apart is precisely the kind of collision that wastes an
// afternoon. Reach the trait as `nornir_build_thing::plugin::BuildThing`.
pub use analyze::ArtifactAnalyzer;
pub use assemble::{
    Assemble, AssembleCache, AssembleCtx, Assembled, AssembledProof, Builders, InMemAssembleCache,
    Precheck, assemble, precheck,
};
pub use build_info::{
    BUILD_INFO_SCHEMA, BuildInfoKind, BuildInfoRow, BuildInfoSink, InMemBuildInfoSink,
    NullBuildInfoSink, build_feed_rows, manifest_rows, now_micros, target_manifest_rows,
};
#[cfg(feature = "build-info-arrow")]
pub use build_info::{build_info_batch, build_info_schema};
pub use output::{ArtifactOutput, BinaryArtifact, ImageRef, PagesBundle, output_of};
pub use flags::{Flag, VALUE_PLACEHOLDER, flags_of};
pub use laws::{assert_bijective, check_bijective};
pub use plugin::{
    ABI_VERSION, ArtifactFacts, BuildRequest, Manifest, OptionsQuery, RebuildRecipe,
    SECTION as BUILD_THING_SECTION, Verdict,
};
#[cfg(feature = "plugin-host")]
pub use plugin_host::{ContainerFetchRunner, ContainerFetcher, Fetcher, HostPathFetcher, Limits, PluginHandle};
pub use progress::{BuildEvent, BuildObserver, NullObserver, RecordingObserver};
pub use service::{AgentMode, Scope, ServiceSpec, service_spec};
pub use sink::{BuildProvenanceSink, JsonFileSink, NullSink};
pub use spec::{BuildSpec, BuildSpecSource, JsonSpecSource, SPEC_FILE, SpecDocument, SpecOrigin};
pub use surface::{
    BuildAction, BuildSurface, ContainerView, IsoView, LaunchAction, LaunchDecl, LaunchOutput,
    VariantView, VmView,
};
pub use tree::{
    Edge, FeatureNode, NodeKind, Rows, SettingNode, SettingsIndex, SettingsNode, SettingsTree, ToggleDecision,
    ToggleRequest, ValNode,
};
pub use verbs::{Action, VERBS, actions_for};
pub use variant::{
    // `AllowEntry` is re-exported because it is the VALUE TYPE of two public
    // fields — a consumer matching on `set.allow_unbuilt_bins` cannot name the
    // arms without it, and a public field whose type is unreachable is the same
    // "declared but not usable" shape this module spent the day removing.
    AllowEntry, ArtifactKind, BuildConfig, BuildVariant, BuildVariantSet, CargoFeatures, Declared,
    DeclaredPayload, Profile, ServiceShape,
};

pub use elm::{BuildThing, BuildThingEffect, BuildThingModel, BuildThingMsg};

// The cloud MEDIUM writers. Re-exported at the crate root beside the ISO's
// `PureRustIsoBuilder` so a consumer reaching for "how does this platform take a
// medium" finds BOTH answers in one namespace: `iso_writer` for a cdrom attach,
// `image_writer` for every platform that imports a disk instead. `CloudMedium::
// for_bring` is the join back to the roster's declared `MediumBring`.
#[cfg(not(target_arch = "wasm32"))]
pub use image_writer::{
    CloudMedium, DiskProbe, MediumProduct, Qcow2Header, Qcow2Reader, VhdFooter, VhdOptions,
    GCE_MEMBER_NAME, probe_disk, read_vhd_footer, tar_gz_member_names, unpack_gce_tar_gz,
    validate_source, vhd_checksum, vhd_chs, write_all_media, write_gce_tar_gz, write_medium,
    write_qcow2, write_raw, write_vhd, write_vhd_with,
};