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

nordisk/holger

holger-recipe/src/lib.rs

//! # holger-recipe — holger's build space, declared in Rust
//!
//! THE LAW: the code is the source. This crate's [`HolgerBuildThing`] is the ONE
//! declaration of what holger can be built into — every variant (`variants.rs`),
//! the appliance container row and the per-site installer row (`packaging.rs`),
//! and the set-level rules below. `.nornir/build-variants.toml` is EMITTED from
//! it (`cargo run -p holger-build-thing -- emit`) to DESCRIBE the build for the
//! tools that discover a repo by that path; it is never read back as authority.
//! `tests/toml_parity.rs` holds the committed file to the emission byte for byte.
//! `holger-build-thing` is the wasm32 cdylib shim over this crate (the name
//! dwarves looks for); this crate is a plain rlib so its consumers never share a
//! cdylib's suffix-less output file (cargo#6313, see Cargo.toml).
//!
//! Until 2026-09-15 the direction was reversed: the toml was hand-written and
//! `holger-build-space` parsed it (`BuildVariantSet::from_toml_str`), with two
//! byte-copies (build-space's crate-local one and edda's
//! `nornir-buildspec-holger`) held to it by drift tests. build-space now answers
//! from [`declared`].
//!
//! ```
//! let set = holger_recipe::declared();
//! // One product list (HOL-PRODUCTS, 2026-10-30): the four products, the estate's server
//! // builds, the media pointers, the front, the downstream medium, the DEV rows and the
//! // three signed wasm guests; a downstream's two payloads and media (one-way, fetching).
//! assert_eq!(set.variants.len(), 24);
//! assert_eq!(set.installers.len(), 7);
//! assert_eq!(set.isos.len(), 1);
//! assert!(set.check_defaults().is_empty());
//! ```

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

use anyhow::Result;
use nornir_build_thing::emit;
use nornir_build_thing::plugin::{
    ArtifactFacts, BuildPlan, BuildRequest, BuildThing, Manifest, RebuildRecipe, Verdict,
    ABI_VERSION,
};
use nornir_build_thing::variant::BuildVariantSet;

/// A downstream's declaration (lane OX-WGDS3): what its medium's `holger_ron` carries.
pub mod downstream_ron;
pub mod estate_ron;
/// **The GATE's rules** — feature honesty computed over holger's own manifests,
/// offline. Behind the non-default `gate` feature so the wasm32 shim links no
/// `toml` and no `std::fs`. See [`honesty`] for why the house needed them.
#[cfg(feature = "gate")]
pub mod honesty;
/// The open load of the declaration — the commercial monetize-impl edge lazy.
#[cfg(not(target_arch = "wasm32"))]
pub mod load;
pub mod packaging;
/// The rules the declaration cannot carry — the airgap and thin-client invariants.
pub mod rules;
/// The four products and their `cargo install` twins.
pub mod twins;
pub mod variants;

/// The repo / suite member this declares.
pub const REPO: &str = "holger";
/// This crate's name, recorded in the manifest so a reader knows where the source is.
pub const SOURCE_CRATE: &str = "holger-build-thing";

/// holger's build thing.
#[derive(Debug, Clone, Copy, Default)]
pub struct HolgerBuildThing;

impl BuildThing for HolgerBuildThing {
    fn manifest(&self) -> Manifest {
        Manifest {
            abi: ABI_VERSION,
            repo: REPO.to_string(),
            source_crate: SOURCE_CRATE.to_string(),
            summary: "artifact security gateway — the four products (holger, \
                      holger-downstream, holger-ui, holger-cli) and the estate's own \
                      builds (the appliance and metered servers, the front, the signed \
                      wasm plugins, the installer media), declared in Rust"
                .to_string(),
        }
    }

    fn variants(&self) -> BuildVariantSet {
        BuildVariantSet {
            // One suite (MVP), so there is no base to carve it out of.
            base_suite: None,
            independent_suites: true,
            offers: offers(),
            exclusive: Vec::new(),
            required: required(),
            downstream: BTreeMap::from([(
                "holger".to_string(),
                BTreeSet::from(["skade".to_string()]),
            )]),
            variants: variants::variants(),
            containers: vec![packaging::container()],
            installers: vec![
                packaging::installer(),
                packaging::metered_installer(),
                packaging::front_installer_metered(),
                packaging::keep_installer(),
                packaging::front_keep_installer(),
                packaging::downstream_installer(),
                packaging::downstream_oneway_installer(),
            ],
            toolchain: Some(toolchain()),
            // The LIVE appliance medium — the `kind = "iso"` peer of the installer
            // row, sharing its payload and its name with the pointer variant that
            // tags it into the MVP delivery. See `packaging::appliance_iso`.
            isos: vec![packaging::appliance_iso()],
            ..BuildVariantSet::default()
        }
    }

    // ── The rules that are NOT data, delegated to [`rules`] ──────────────────
    //
    // They must be answered by THIS type, because this type is what the wasm shim
    // exports: the trait's own defaults would tell a forge that holger "declares no
    // artifact-level invariant", which is an OK verdict for an airgap tier linking
    // reqwest. Bodies live in `rules.rs`; this is the seam, deliberately thin.

    fn plan(&self, variant: &str) -> Option<BuildPlan> {
        rules::plan(variant)
    }

    fn validate(&self, req: &BuildRequest) -> Verdict {
        rules::validate(req)
    }

    fn judge_artifact(&self, facts: &ArtifactFacts) -> Verdict {
        rules::judge_artifact(facts)
    }

    fn rebuild_recipe(&self, variant: &str) -> Option<RebuildRecipe> {
        rules::rebuild_recipe(variant)
    }
}

/// **The pinned build image every cargo variant compiles in** — `[build.toolchain]`.
///
/// # Why this table exists, and why it did not until 2026-09-17
///
/// holger declared none, which meant `cargo xtask build` ran host cargo and the
/// verdict of a build belonged to whichever box typed the command. The exposure
/// is not hypothetical and it is not even holger-specific: `mvp-server-musl`
/// targets `x86_64-unknown-linux-musl`, and MEASURED on loki 2026-09-17 the
/// SAME shape killed gunnar's host build outright — `aws-lc-sys`' build script
/// died on a missing `x86_64-linux-musl-gcc`, which loki does not have and is
/// not going to be made to have. The law is written in the image's own opening
/// lines (`nornir-build-thing/src/containerfiles/cargo-toolchain.Containerfile:3`):
///
/// > The host contributes a container engine socket and a workdir. NOTHING
/// > else… A build that only works on one box is a bug, not a build.
///
/// # The pin is MEASURED, not copied
///
/// `nordisk-cargo-toolchain` is the constellation's ONE cargo build image and it
/// has no registry behind it: every box MINTS it, reproducibly, from the
/// Containerfile embedded in `nornir-build-thing` (pinned base digest, a
/// snapshot.debian.org instant, a fixed `SOURCE_DATE_EPOCH`, logs scrubbed
/// in-layer). Minted on loki 2026-09-17 through
/// `DraupnirCargoRunner::build_toolchain_image` — a box that had never held the
/// image — `podman images --digests` reported
/// `sha256:86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762`,
/// which is the value below and is byte-for-byte the digest gunnar's own
/// `[build.toolchain]` has pinned since 2026-09-15. Two repos, two boxes, one
/// recipe, one digest: that is what makes this a pin rather than a minting
/// event, and it is why this line could be written without inventing anything.
///
/// When the Containerfile in `nornir-build-thing` changes, this line moves WITH
/// it, in the same commit, and never before.
///
/// **2026-10-23: re-pinned to edda's `CARGO_TOOLCHAIN_PIN`, `86d575e8…`**
/// (recipe-53fec13a…, + libgraphblas-dev). The old `3d5115ba…` manifest was held
/// only by oden's image store, so every Build Thing off oden refused; the CI lock
/// check now reds any repo whose pin is not edda's.
fn toolchain() -> nornir_build_thing::cargo_container::CargoToolchain {
    nornir_build_thing::cargo_container::CargoToolchain {
        image: "nordisk-cargo-toolchain".to_string(),
        image_sha256: "86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762"
            .to_string(),
        // What the image was built with; the runner compares it against the
        // container's own `rustc --version` and a disagreement is a refusal.
        rust_version: "1.97.1".to_string(),
        // What the image carries std FOR — read off the Containerfile's own
        // `ARG RUST_TARGETS`, never wished for. `check_target` compares a
        // variant's `--target` against this list, so `mvp-server-musl` is
        // admitted here rather than discovered an hour in with an `E0463`.
        targets: vec![
            "x86_64-unknown-linux-gnu".to_string(),
            "x86_64-unknown-linux-musl".to_string(),
            "wasm32-unknown-unknown".to_string(),
            "x86_64-unknown-uefi".to_string(),
        ],
        // The bounds — a build container is a GUEST, never a host process.
        timeout_secs: 3600,
        mem_limit_mb: 8192,
        cpus: 8,
        // The image is self-contained, so the compile runs `--network none` and
        // the sources come from the host registry bound read-only, every one of
        // them checksum-pinned by `Cargo.lock` and held there by `--locked`.
        network: false,
        // rustc bakes absolute directories into debug info; remapping them is
        // what lets a host build and a container build of one variant be
        // byte-identical.
        remap_paths: true,
        // **The sibling checkouts this repo's escaping `path =` deps reach
        // into.** A container holding only holger cannot see one of them, and a
        // missing entry is refused BY NAME before the container starts.
        // MEASURED by running the verb, which prints the escape list — not
        // guessed from the manifests by eye. The first attempt listed three
        // (edda, znippy, draupnir) and `cargo xtask build --suite MVP --variant
        // mvp-server-musl` refused before the container started, naming the
        // four it had missed and the manifests that reach them:
        //
        //     [build.toolchain]: path dependencies escape this repo into sibling
        //     repo(s) the declaration does not list … Add `siblings =
        //     ["Skidbladnir", "facett", "modgunn", "znippy-zoomies"]`. Escaping:
        //     Skidbladnir (2 dep(s), e.g. server/cli/Cargo.toml → skidbladnir);
        //     facett (6 dep(s), e.g. holger-embed/Cargo.toml → facett-core);
        //     modgunn (1 dep(s), e.g. server/lib/Cargo.toml → modgunn);
        //     znippy-zoomies (2 dep(s), e.g. agent/lib/Cargo.toml → znippy-zoomies)
        //
        // The declaration follows the manifests and not the other way round:
        // this is what the tree ACTUALLY reaches for, and a container that is
        // not told fails inside itself, where the error says least. Shrink it
        // the day those become registry versions — and measure it, do not
        // assume it: the same verb prints the list.
        // ⚠ AND THE LIST IS NOT ONLY ABOUT BUILDING HOLGER (2026-09-21, lane H2).
        // `cargo xtask installer-iso` runs a SECOND container build through this
        // same declaration — `korp-boot`, for `x86_64-unknown-uefi`, rooted at
        // EDDA, not at holger — and edda's crates escape into two siblings holger
        // itself never touches. Measured by running the verb, which refused
        // before the container started:
        //
        //     ✗ installer-iso failed: build korp-boot (needs the
        //     x86_64-unknown-uefi rust-std): [build.toolchain]: path dependencies
        //     escape this repo into sibling repo(s) the declaration does not list
        //     … Add `siblings = ["funnel-core", "skade"]`. Escaping: funnel-core
        //     (1 dep(s), e.g. edda/crates/dwarves-runa/Cargo.toml → funnel-core);
        //     skade (3 dep(s), e.g. edda/crates/nornir-release-ledger/Cargo.toml
        //     → iceberg)
        //
        // So **the installer ISO could not be composed at all** from a checkout
        // whose siblings were exactly what holger's own build needs. The two are
        // named here rather than in a second declaration, because there is one
        // `[build.toolchain]` and both builds ride it.
        //
        // 2026-09-26: `tunnr` joins the list. korp-installer moved from edda to
        // tunnr (owner ruling), and xtask path-depends on it there, so holger's own
        // build now escapes into tunnr. The korp-boot build is rooted at TUNNR now;
        // its mounts are computed by `sibling_closure` from that root.
        siblings: vec![
            "Skidbladnir".to_string(),
            "draupnir".to_string(),
            "edda".to_string(),
            "facett".to_string(),
            "funnel-core".to_string(),
            // `holger-ui` is NOT here (2026-10-18, L23): only vm1's program
            // `holger-estate-front` (its OWN detached workspace, built by
            // `manifest_dir`) reaches it, so that row names it in its own
            // `siblings` ([`front_siblings`]) and every open row's container build
            // mounts — and needs — no holger-ui checkout.
            "modgunn".to_string(),
            "monetize".to_string(),
            "skade".to_string(),
            "tunnr".to_string(),
            "znippy".to_string(),
            "znippy-zoomies".to_string(),
        ],
    }
}

/// **The sibling checkouts the front's row mounts** — the toolchain's list plus
/// `holger-ui`: vm1's program `holger-estate-front` path-depends on holger-ui
/// (holger's console, `holger-ui-server`) and on monetize (`monetize-front`). Only
/// that row reaches holger-ui, so only that row names it.
#[must_use]
pub fn front_siblings() -> Vec<String> {
    let mut s = toolchain().siblings;
    s.push("holger-ui".to_string());
    s.sort_unstable();
    s
}

/// **The feature universe** holger offers across the workspace; a variant feature outside
/// it is rejected up front. What each one is: the product crates' manifests
/// (`server/cli`, `holger-downstream`, `holger-cli`) say it beside the feature.
///
/// Wider than the packages it describes, recorded not hidden: `sign` and `testmatrix`
/// exist on server/cli but no row names `testmatrix`; `served` is
/// `holger-bench-scenarios`', named by no row.
fn offers() -> BTreeSet<String> {
    [
        // The named extras of every product.
        "all-formats",
        "online",
        "federation",
        "full",
        // The formats (the twelve MVP formats are every server product's default).
        "fmt-all",
        "fmt-raw",
        "fmt-rust",
        "fmt-npm",
        "fmt-maven",
        "fmt-python",
        "fmt-docker",
        "fmt-go",
        "fmt-helm",
        "fmt-deb",
        "fmt-rpm",
        "fmt-znippy",
        "fmt-build-thing",
        // The downstream gateway and its WireGuard tunnel.
        "downstream",
        "downstream-wg",
        "sync",
        "fetch",
        "export-push",
        // The one-way appliance's init without pairing (lane HOL-CDS-5).
        "pid1",
        // The server's planes.
        "secure_update",
        "supply-chain",
        "mirror",
        "admission",
        "migration",
        "jobs",
        "sign",
        // Heimdall's protected runner for the scans and the bundle verifies (lane HOL-HEIMDALL-03).
        "heimdall",
        "front",
        "auth-passkey",
        // The estate's builds: the appliance marker, the ceremony, pairing, the paid
        // state and its admission, the plugin host pair and the open built-in meter, the
        // box's own advisory fetch and the DEV media's boot log.
        "mvp-server",
        "appliance",
        "paired",
        "metered",
        "admit-share",
        "wasm-plugins",
        "meter-guests",
        "allow-meter",
        "intel-fetch",
        "dev-narrate",
        // holger-estate-front's console and holger.rs's public face (the front's rows).
        "console",
        "public-site",
        // Declared, named by no row.
        "testmatrix",
        "served",
    ]
    .into_iter()
    .map(str::to_string)
    .collect()
}

/// Cargo `[[bin]] required-features`, mirrored so a bad feature combo fails fast with a
/// clear message instead of a raw cargo error.
fn required() -> BTreeMap<String, Vec<String>> {
    [
        ("holger-mvp", &["mvp-server"][..]),
        ("holger-mvp-metered", &["mvp-server", "metered"][..]),
    ]
    .into_iter()
    .map(|(bin, features)| {
        (
            bin.to_string(),
            features.iter().map(|f| f.to_string()).collect(),
        )
    })
    .collect()
}

/// The header every emission starts with — what the file is, and what it is not.
pub const EMITTED_HEADER: &str = "\
# holger — .nornir/build-variants.toml — EMITTED, NOT SOURCE.
#
# Generated by `cargo run -p holger-build-thing -- emit` from holger-recipe (the
# Rust `impl BuildThing`: holger-recipe/src/variants.rs for the build variants,
# packaging.rs for the container and installer rows, lib.rs for the feature
# universe and set rules; holger-build-thing is its wasm32 cdylib shim). The code is the one source; this file
# DESCRIBES the build for readers and for the tools that discover a repo by this
# path (the dwarves forge, the build-thing pane, xtask). Editing it changes
# NOTHING: re-emit after changing the code. Do not edit by hand.
#
# Model: variant -> features -> settings/sub-settings -> typ.
#
# The reasoning that used to sit between these rows is in git history
# (`git show ed4e988:.nornir/build-variants.toml`) and, condensed, in the doc
# comments beside each row. New reasoning goes there, never here: a comment
# written in this file is deleted by the next `emit`.";

/// The set this crate declares — the one source.
#[must_use]
pub fn declared() -> BuildVariantSet {
    HolgerBuildThing.variants()
}

/// The emission — what `.nornir/build-variants.toml` must contain.
pub fn emitted_toml() -> Result<String> {
    emit::emitted_toml(EMITTED_HEADER, &HolgerBuildThing)
}

/// Regenerate `<repo_root>/.nornir/build-variants.toml` from the code.
pub fn write_emitted(repo_root: &Path) -> Result<PathBuf> {
    emit::write_emitted(EMITTED_HEADER, &HolgerBuildThing, repo_root)
}

/// holger's repo root, from this crate's manifest directory.
#[must_use]
pub fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("holger-recipe has a parent (the workspace root)")
        .to_path_buf()
}

/// Refuse when the committed `.nornir/build-variants.toml` under `repo_root` is
/// not the code's emission — for doors that read the file (the dwarves-shared
/// installer door discovers it by path) and must not act on a stale description.
pub fn require_committed_is_emitted(repo_root: &Path) -> Result<()> {
    let path = BuildVariantSet::conventional_path(repo_root);
    let committed = std::fs::read_to_string(&path)
        .map_err(|e| anyhow::anyhow!("read {}: {e}", path.display()))?;
    if let Some(diff) = emit::first_difference(&committed, &emitted_toml()?) {
        anyhow::bail!(
            "{} is not the code's emission ({diff}) — run `cargo run -p holger-build-thing -- emit`",
            path.display()
        );
    }
    Ok(())
}