crates/edda-build-thing/src/recipe.rs
//! **The recipe** — edda's `impl BuildThing`, the ONE declaration of what this
//! repository builds. `.nornir/build-variants.toml` is emitted from
//! [`EddaBuildThing::variants`] (see [`crate::emit`]); nothing here reads it.
//!
//! # What edda actually ships
//!
//! edda is the nordisk shared-lib SAMLING — ~66 crates, almost all of them
//! libraries somebody else links. A LIBRARY is not a variant: it is delivered by
//! `dwarves crate-release` to the registry, not by a build point. What a variant
//! claims is an ARTIFACT this repo produces, and by that measure edda ships
//! **two** executables:
//!
//! | variant | role | package · bin | what it is |
//! |---|---|---|---|
//! | `autonom` ⭐ | tool | `autonom` · `autonom` | the "code lacking tests" worklist CLI (`scan`) |
//! | `robotui` ⭐ | client | `nornir-robotui` · `robotui` | the shipped accessible robot UI (`watch`) |
//!
//! # `korp-installer` and `korp-start` MOVED TO TUNNR on 2026-09-26
//!
//! Owner ruling: the install medium's OS-less PID-1 boots tunnr's kernel
//! (`tunnr/tunnr-kernel`) and belongs next to it. The crate (with its leaves
//! `korp-boot-bios` and `korp-mkfs-xfs`) and its two rows moved to
//! `tunnr/crates/tunnr-build-thing`, unchanged: the coordinates are now
//! `tunnr/korp-installer` and `tunnr/korp-start`. The section below is kept as
//! the history of how those rows came to be declared at all.
//!
//! # The two rows the hand-written file excused, and why the excuse was false
//!
//! `.nornir/build-variants.toml` said, in prose, that the ISO boot inputs
//! *"are NOT plain cargo artifacts — they are staged into bootable ISOs by
//! tunnr's `[[build.iso]]` / `[[build.installer]]` variants (an engine target,
//! out of the `--all` cargo scope), so they stay declared in tunnr … and are
//! intentionally absent here."*
//!
//! Three things are wrong with that, and all three were checkable:
//!
//! 1. **They ARE plain cargo artifacts.** `korp_installer::pid1build::build_pid1`
//! runs, verbatim, `cargo build --release --bin <bin> --target
//! x86_64-unknown-linux-musl --manifest-path …`. That is a cargo variant with
//! a target triple on it — precisely the shape [`BuildVariant`] exists for.
//! 2. **tunnr does not declare them.** Measured 2026-09-20: `korp-installer` and
//! `korp-start` appear in tunnr's recipe only inside doc-comment prose, and
//! nowhere in its emitted toml. The rows were not moved somewhere else; they
//! were nowhere.
//! 3. **An ISO consuming an artifact is not the same claim as the artifact.**
//! korp declares an installer MEDIUM named `korp-installer` (`kind =
//! "installer"`); that medium's inputs are these two binaries. Variant names
//! are repo-scoped, so `edda/korp-installer` (the musl exe) and
//! `korp/korp-installer` (the medium that carries it) are two coordinates, and
//! conflating them is what let the exe go undeclared while everybody pointed
//! at the medium.
//!
//! These two binaries are shipped further than anything else in edda: they are
//! what a gunnar appliance boots, pinned by version in two gunnar manifests. A
//! repo whose most widely shipped artifact is absent from its own build matrix is
//! under-declared, and now it is not.
//!
//! # `linkage` was silently `fat` while the prose said it did not apply
//!
//! The hand-written file's header said *"neither binary embeds a suite sibling
//! nor is itself a suite service, so the fat/thin linkage axis does not apply —
//! plain `exe` targets"* — and then declared no `linkage` key, which
//! [`ServiceShape`]'s default reads as **fat**. The prose and the data disagreed,
//! and only the data is read. Every row here says
//! [`ServiceShape::NotApplicable`] out loud.
//!
//! # No `member`, and therefore no `downstream`
//!
//! `member` names a node in the SUITE dependency graph. edda is a LIBRARY
//! SAMLING and no product — the law is explicit that znippy, facett, Skidbladnir,
//! draupnir, edda and knut *"have build-things and they have NO product"* — so no
//! row claims one, and an `embeds` allowlist keyed by member would have no
//! subject to key on. `embeds` and `reaches` are empty on every row, and the
//! row tests assert they stay that way.
use std::collections::BTreeMap;
use nornir_build_thing::cargo_container::CargoToolchain;
use nornir_build_thing::contract::Coordinate;
use nornir_build_thing::plugin::{BuildThing, Manifest, Role, ABI_VERSION};
use nornir_build_thing::variant::{
ArtifactKind, BuildVariant, BuildVariantSet, Profile, ServiceShape,
};
/// The crate a receipt names as its recipe.
pub const KRATE: &str = "edda-build-thing";
/// The content hash over this crate's sources, computed by `build.rs`.
pub const CONTENT_HASH: &str = env!("EDDA_BUILD_THING_CONTENT_HASH");
/// **The delivery this repo ships in.** `suites` is a LIST — one row may ship in
/// more than one flavour — but edda has exactly one, and writing it down is the
/// whole point: an EMPTY list is not a third flavour, it is a row that
/// `cargo xtask build --suite ORIGINAL` cannot select. Both of edda's old rows
/// were exactly that, and they are two of the 27 untagged rows the 2026-09-17
/// census counted. The only other flavour anywhere today is holger's MVP; every
/// other product declares ORIGINAL alone and TAGS it, so the build verb has ONE
/// shape everywhere.
pub const SUITE: &str = "ORIGINAL";
/// edda's build thing.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EddaBuildThing;
impl EddaBuildThing {
/// The coordinate a receipt names this recipe by: `krate:version:hash`.
#[must_use]
pub fn coordinate() -> Coordinate {
Coordinate {
krate: KRATE.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
hash: Some(CONTENT_HASH.to_string()),
}
}
}
fn s(x: &str) -> String {
x.to_string()
}
/// The two rows.
#[must_use]
pub fn variants() -> Vec<BuildVariant> {
vec![
BuildVariant {
// crates/autonom, bin `autonom` — the runnable "code lacking tests"
// worklist CLI. Only builds with the `scan` feeder compiled in (cargo
// `required-features = ["scan"]`, mirrored in `required()` so a bad
// combo fails with a message instead of a raw cargo error).
name: s("autonom"),
suites: vec![s(SUITE)],
role: Some(Role::Tool),
is_default: true,
package: Some(s("autonom")),
bin: Some(s("autonom")),
kind: ArtifactKind::Exe,
shape: ServiceShape::NotApplicable,
features: vec![s("scan")],
profile: Profile::Release,
..BuildVariant::default()
},
BuildVariant {
// crates/nornir-robotui, bin `robotui` — the shipped accessible robot
// UI. Needs `watch` (eframe + the wire surface); an egui desktop app,
// not a suite service, which is why it is `client` and not `server`.
name: s("robotui"),
suites: vec![s(SUITE)],
role: Some(Role::Client),
is_default: true,
package: Some(s("nornir-robotui")),
bin: Some(s("robotui")),
kind: ArtifactKind::Exe,
shape: ServiceShape::NotApplicable,
features: vec![s("watch")],
profile: Profile::Release,
..BuildVariant::default()
},
]
}
/// The feature space edda offers — the two its rows build and the three it
/// offers and nothing builds. The six korp-installer features (`znippy`,
/// `splash`, `forge`, `secure-boot`, `testmatrix`, `bench-halt`) left with the
/// crate for tunnr on 2026-09-26, and their excuses with them.
fn offers() -> Vec<&'static str> {
vec![
"scan",
"watch",
"watch-wgpu",
"wire",
"bench",
]
}
/// **The features no row builds, each with a written reason.**
///
/// The hand-written file offered `watch-wgpu`, `wire` and `bench` and excused
/// none of them — an offered-but-unbuilt feature with no reason is rot, and it is
/// the exact shape that let korp's `spark41` accumulate three invisible compile
/// breaks. The excuses are here now, and a STALE one (a feature a row starts
/// building) fails the gate too, so the allowlist cannot rot either.
pub fn allow_unbuilt() -> BTreeMap<String, String> {
[
("watch-wgpu", "nornir-robotui's wgpu-backed alias of `watch` — the SAME window through a different eframe renderer. It is the renderer choice of whoever runs the UI, not a second artifact: shipping both would put two identically-behaving `robotui` binaries in the delivery matrix, and `tests/watch_wgpu_is_an_alias.rs` in that crate is the proof they are one program."),
("wire", "nornir-robotui's wire surface as a STANDALONE feature — the transport types a robot driver links when it wants to speak to the UI without compiling the UI. `watch` already implies it, so the shipped binary carries it; enabling it alone builds a library configuration, and a library configuration is not an artifact."),
("bench", "autonom's nornir-bench harness arm: an `[[example]]` (`required-features = [\"bench\"]`) that prints one JSON BenchRun on stdout. A bench harness entrypoint is a measurement tool, never a delivery, and it is compiled by `cargo test`/`cargo bench` rather than by a build point."),
]
.into_iter()
.map(|(k, v)| (s(k), s(v)))
.collect()
}
/// Cargo `[[bin]] required-features`, mirrored so a bad combo fails fast with a
/// clear message instead of a raw cargo error.
fn required() -> BTreeMap<String, Vec<String>> {
[(s("autonom"), vec![s("scan")]), (s("robotui"), vec![s("watch")])]
.into_iter()
.collect()
}
/// **Bins that exist, are not shipped, and must not read as an unbuilt hole.**
///
/// Both became visible on 2026-08-25 — they were not added. `cargo_bin_targets`
/// hand-parsed `[[bin]]` stanzas and was blind to cargo's `src/bin/*.rs`
/// auto-discovery, so neither was in the oracle every gate asks. Neither is a
/// shipped artifact: both are test-harness subprocesses that exist so an
/// assertion can be made about a real PROCESS rather than an in-process value.
fn allow_unbuilt_bins() -> BTreeMap<String, nornir_build_thing::variant::AllowEntry> {
use nornir_build_thing::variant::AllowEntry;
[
("nornir-bench-redprobe", "crates/nornir-bench/src/bin/nornir-bench-redprobe.rs — a two-arm bench binary that exists to be watched FAILING. nornir-bench's claim after 2026-08-06 is that a red arm exits NON-ZERO, and that is a process exit status no in-process unit test can assert: calling `verdict` and checking the `Err` proves the value, not the status the shell reads, and the gap between those two is exactly where the old defect lived (`\"passed\": false` was in the JSON the whole time and the process still exited 0). `tests/failed_arm_exits_nonzero.rs` builds and runs it as a real subprocess both ways, so it IS compiled by `cargo test -p nornir-bench` — just never by a delivery row, and it must never ship."),
// 2026-10-16 (build-thing sweep on loki): three more the gate counted and no
// row or reason covered.
("bootx64", "crates/nornir-build-thing/tests/fixtures/korp-efistub — a no_std UEFI test payload (its own [workspace], not a member), built on demand with `--target x86_64-unknown-uefi` by the ignored ISO boot test (`tests/iso_boot.rs`) so a serial capture proves a pure-Rust ISO boots. A fixture, never shipped."),
("edda-build-thing", "this recipe crate's own emit door (`cargo run -p edda-build-thing -- emit` regenerates .nornir/build-variants.toml); the forge loads the crate as wasm, the bin is repository tooling, never shipped."),
("estate-clock", "crates/estate-clock's standalone daemon form (`required-features = [\"bin\"]`). Its consumers link the LIBRARY today (holger-estate-front with `fetch`+`adjust`), and gunnar still ships its own gunnar-clock; no estate ships this bin yet, so no edda row builds it."),
("xtask", "the `cargo xtask` door (`cargo xtask ci`, `cargo xtask build`): repository tooling that runs the CI plan and the declared rows; a member nothing depends on, never shipped."),
("fleet_mock_server", "crates/nornir-fleet/src/bin/fleet_mock_server.rs — the trivial mounted app `LocalProcessSpinner`'s test launches instead of the five real apps, which are too heavy and flaky to spawn in a unit test (the `run_local_fleet` example is the real-apps entry). It mounts a `CmdJsonServer` over a bare click-counter `CmdHost` on a Unix socket, mirroring dwarves-gui's `--robot-remote <socket>` so the spinner drives it exactly like a real app. Compiled by the nornir-fleet spinner test; a fixture, not an artifact."),
]
.into_iter()
.map(|(k, v)| (s(k), AllowEntry::Reason(s(v))))
.collect()
}
/// **The pinned build image every cargo row compiles in** — `[build.toolchain]`.
///
/// edda declared none, and the consequence was NOT that edda built on the host:
/// `CargoToolchain::validate` refuses an absent or empty `image_sha256` BY NAME
/// (*"a named tag is a convenience, NOT a pin"*), which closes the code road and
/// makes the forge fall back to reading the emitted toml as if it were the
/// source — the ruling out of force for want of one table.
///
/// The digest is the **MANIFEST** digest of `nordisk-cargo-toolchain:live`, read
/// off the engine rather than typed from memory (`cargo run -p nornir-build-thing
/// --features image-build --example image_digest`). It is the `live-uefi` recipe
/// (nornir-build-thing 0.1.17's `cargo-toolchain.Containerfile`), which is the
/// one that carries `x86_64-unknown-uefi` (korp-installer, which needed it, moved to
/// tunnr on 2026-09-26; the image and its target list are facts about the image
/// and did not change). It moves only when the image RECIPE moves, in the same commit.
///
/// **RE-PINNED 2026-10-16 `668468e7…` → `86d575e8…`** with the recipe (libgraphblas-dev);
/// the pin is now `nornir_build_thing::CARGO_TOOLCHAIN_PIN`, moved with the recipe.
///
/// **RE-PINNED 2026-09-30 (lane P, dwarf), `3d5115ba…` → `668468e7…`**, in the commit
/// that adds `clang llvm` to `cargo-toolchain.Containerfile` (C for wasm32: zstd-sys).
/// Minted twice through `mint_cargo_toolchain`, the second after `podman rmi` +
/// prune: the same manifest digest both times (id `88c4902987c2`).
fn toolchain() -> Option<CargoToolchain> {
Some(CargoToolchain {
image: s("nordisk-cargo-toolchain"),
// THE pin of the recipe this edda carries — moved with it, never typed here.
image_sha256: s(nornir_build_thing::CARGO_TOOLCHAIN_PIN.digest),
rust_version: s("1.97.1"),
// What the pinned image CARRIES std for, not what edda delivers. No row
// names a `target` since the two musl PID-1 rows moved to tunnr
// (2026-09-26); a row naming a triple the image does not carry is refused
// at validation instead of an hour later with E0463.
// wasm32 is this crate's own plugin module's target.
targets: vec![
s("x86_64-unknown-linux-gnu"),
s("x86_64-unknown-linux-musl"),
s("wasm32-unknown-unknown"),
s("x86_64-unknown-uefi"),
],
timeout_secs: 3600,
mem_limit_mb: 8192,
cpus: 8,
network: false,
remap_paths: true,
// **The sibling checkouts the container must have mounted beside edda.**
// MEASURED 2026-09-20 by reading every `path = "../../../…"` in
// `crates/*/Cargo.toml` — the deps that ESCAPE this repo — not guessed:
// jera -> draupnir, storage-git-gix -> znippy/git-storage-trait,
// nornir-depgraph / nornir-docbook / storage-git-gix ->
// znippy-zoomies/gatling, nornir-knowledge-view -> znippy-zoomies,
// nornir-release-ledger / nornir-warehouse-trait -> skade, step-board ->
// facett/facett-jobview, dwarves-runa -> funnel-core,
// nornir-build-thing -> draupnir. NOT tunnr (2026-09-29): the cloud-media boot
// proof takes korp-installer by REGISTRY version from its own test crate
// (crates/cloud-media-bootproof), because a path into tunnr closed an edda ->
// tunnr -> edda path cycle (korp-installer path-deps back into edda).
// cargo parses EVERY workspace member's
// manifest even for a `-p` build, so a row that touches none of these
// still cannot resolve without them. If a sibling path dep is added or
// removed, this list moves in the SAME commit.
siblings: vec![
s("draupnir"),
s("facett"),
s("funnel-core"),
s("skade"),
s("znippy"),
s("znippy-zoomies"),
],
})
}
impl BuildThing for EddaBuildThing {
fn manifest(&self) -> Manifest {
Manifest {
abi: ABI_VERSION,
repo: s("edda"),
source_crate: s(KRATE),
summary: "edda — the nordisk shared-lib SAMLING: ~66 reusable library crates the \
rest of the suite links to avoid circular deps, plus the TWO \
executables it actually ships — the autonom coverage CLI and the robotui \
accessible robot UI. The appliance PID-1s (korp-installer, korp-start) \
moved to tunnr on 2026-09-26. A library samling, never a product."
.to_string(),
}
}
fn variants(&self) -> BuildVariantSet {
BuildVariantSet {
// ONE delivery, named rather than left implicit, so `--suite ORIGINAL`
// resolves here instead of falling through to UNSUITED.
base_suite: Some(s(SUITE)),
offers: offers().into_iter().map(s).collect(),
required: required(),
allow_unbuilt: allow_unbuilt(),
allow_unbuilt_bins: allow_unbuilt_bins(),
variants: variants(),
toolchain: toolchain(),
..BuildVariantSet::default()
}
}
}