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

nordisk/edda

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()
        }
    }
}