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