crates/holger-ui-recipe/src/lib.rs
//! # holger-ui-recipe — holger-ui's build space, declared in Rust
//!
//! THE LAW: the code is the source. This crate's [`HolgerUiBuildThing`] is the
//! ONE declaration of what holger-ui can be built into — every variant
//! ([`variants`]) and the set-level rules below. `.nornir/build-variants.toml`
//! is EMITTED from it (`cargo run -p holger-ui-build-thing -- emit`) to
//! DESCRIBE the build for the tools that discover a repository 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-ui-build-thing` is the wasm32 cdylib shim over this crate (the name
//! dwarves looks for); this crate is a plain rlib so its consumers — `xtask` —
//! never share a cdylib's suffix-less output file (cargo#6313, see Cargo.toml).
//!
//! ## What this repository is
//!
//! TWO halves of one console:
//!
//! * **`holger-ui-web`** — a `wasm32-unknown-unknown` bundle a browser loads,
//! bundled by trunk and served as static files. It speaks gRPC-web straight to
//! holger's tonic-web door for everything it reads.
//! * **`holger-ui-server`** — the backend-for-frontend, added 2026-09-17. It
//! holds the passkey ceremony, terminates the session cookie, and speaks
//! NATIVE gRPC to holger holding the console's own bearer. A browser can do
//! none of those three.
//!
//! Until the BFF existed this declaration carried one row and a long note saying
//! why the second was absent; [`variants`] quotes that note and says what
//! replaced it.
//!
//! ```
//! let set = holger_ui_recipe::declared();
//! assert_eq!(set.variants.len(), 4); // + the open `ui` row (H5)
//! assert!(set.check_defaults().is_empty());
//! set.validate_all().unwrap();
//! ```
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use anyhow::Result;
use nornir_build_thing::emit;
use nornir_build_thing::plugin::{BuildThing, Manifest, ABI_VERSION};
use nornir_build_thing::variant::{AllowEntry, BuildVariantSet};
pub mod variants;
/// The repository / suite member this declares.
pub const REPO: &str = "holger-ui";
/// The crate the forge loads, recorded in the manifest so a reader knows where
/// the source is. It is the shim; the declaration is this crate.
pub const SOURCE_CRATE: &str = "holger-ui-build-thing";
/// holger-ui's build thing.
#[derive(Debug, Clone, Copy, Default)]
pub struct HolgerUiBuildThing;
impl BuildThing for HolgerUiBuildThing {
fn manifest(&self) -> Manifest {
Manifest {
abi: ABI_VERSION,
repo: REPO.to_string(),
source_crate: SOURCE_CRATE.to_string(),
summary: "the holger console in two halves — a wasm32 browser bundle bundled \
by trunk and served as static files, and the backend-for-frontend \
that holds its passkey ceremony and speaks gRPC to holger; \
declared in Rust"
.to_string(),
}
}
fn variants(&self) -> BuildVariantSet {
BuildVariantSet {
offers: offers(),
allow_unbuilt_bins: allow_unbuilt_bins(),
variants: variants::variants(),
// No `exclusive` and no `required`: the feature universe is one
// word, `metered` (see `offers()`), so there is nothing for either
// rule to hold. Neither `[[bin]]` in the workspace declares
// `required-features`.
//
// No `downstream`: nothing here embeds a sibling — the bundle is
// thin and reaches holger — so the asymmetry rule has nothing to
// hold.
//
// No `base_suite`: holger's own declaration sets `base_suite =
// "ORIGINAL"` because it ships in both deliveries. This repository
// ships in MVP only (see `variants::MVP`), and naming a base suite
// it declares no row for would answer a question nobody asked.
// **The environment every CARGO row here compiles in** — the pinned
// image, not this box. See `toolchain()` below.
toolchain: Some(toolchain()),
// The console bundles `dist/` and `dist-metered/` (see `variants::web_apps`).
web_apps: variants::web_apps(),
..BuildVariantSet::default()
}
}
}
/// **The pinned build image every cargo row in this repository compiles in** —
/// `[build.toolchain]`.
///
/// # Why it is here, and what it replaces
///
/// The estate law (`workspace_nordisk/nornir-workspace.toml`) is one sentence:
/// *a build thing builds in a container or a KVM, never on the host — the host
/// contributes a container engine socket and a workdir, NOTHING else, because
/// then the BOX decides the verdict instead of the DECLARATION.*
///
/// Until this table existed, `cargo xtask build --variant mvp-ui` could only
/// REFUSE, and it did, by name. It refused correctly: the alternative on offer
/// was `Command::new("cargo")` on whatever box typed the command, and that road
/// is a coin toss per box — MEASURED twice in the house on the same shape
/// (oden 2026-08-25 `error[E0463]: can't find crate for core`; loki 2026-09-17
/// `aws-lc-sys` on a missing `x86_64-linux-musl-gcc`), and MEASURED GREEN on
/// t14s 2026-09-20 (`variants::MUSL_WALL`). Three boxes, three different
/// verdicts, one row: that is the defect, and a green is as much a symptom of
/// it as a red.
///
/// # The pin is READ, not wished for
///
/// `nordisk-cargo-toolchain` has no registry behind it: every box MINTS it from
/// the Containerfile embedded in `nornir-build-thing`
/// (`src/containerfiles/cargo-toolchain.Containerfile` — pinned Debian base
/// digest, `ARG RUST_VERSION=1.97.1`, a fixed `SOURCE_DATE_EPOCH` through
/// podman's `timestamp`), which is why a digest names a RECIPE and not a
/// minting event. The value below is what `podman images --digests` reports for
/// `localhost/nordisk-cargo-toolchain` on t14s 2026-09-20, and it is byte-for-
/// byte the digest holger's and gunnar's own `[build.toolchain]` tables have
/// pinned since 2026-09-15/17. Three repos, three boxes, one recipe, one
/// digest.
///
/// `targets` is read off that same Containerfile's `ARG RUST_TARGETS`, never
/// guessed: `check_target` compares a row's triple against this list and
/// refuses AT VALIDATION, so `mvp-ui`'s musl and `mvp-ui-web`'s wasm32 are both
/// admitted here rather than discovered an hour later with an `E0463`.
///
/// When the Containerfile in `nornir-build-thing` changes, this line moves WITH
/// it, in the same commit, and never before.
///
/// # `siblings` is EMPTY, and that is a measurement
///
/// `grep 'path = "' **/Cargo.toml` in this repository finds exactly two path
/// dependencies and both stay inside the checkout (`xtask` → `crates/holger-ui-recipe`,
/// `crates/holger-ui-build-thing` → the same). Nothing escapes into a sibling
/// repo, which is what `nornir-build-thing = { version = "0.1.24" }` FROM THE
/// REGISTRY buys this repository (see `Cargo.toml`) and what lets a container
/// holding only this tree build it. holger declares seven siblings; this one
/// declares none, and the empty list is the honest answer rather than an
/// omission. An escaping dep added later is refused BY NAME before the
/// container starts (`check_escaping_path_deps`).
fn toolchain() -> nornir_build_thing::cargo_container::CargoToolchain {
nornir_build_thing::cargo_container::CargoToolchain {
image: nornir_build_thing::cargo_container::CARGO_TOOLCHAIN_IMAGE.to_string(),
// RE-PINNED 2026-10-25 (gate holger, loki), `668468e7…` → `86d575e8…`: the manifest digest
// nornir-build-thing 0.2.6's cargo-toolchain.Containerfile mints (gunnar-ui, dwarves and
// edda pin the same). Before: 2026-10-01 (lane R), `3d5115ba…` → `668468e7…`.
image_sha256: "86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762"
.to_string(),
// What the image was built with. The runner compares it against the
// container's own `rustc --version`; a disagreement is a refusal, not a
// shrug.
rust_version: "1.97.1".to_string(),
// `ARG RUST_TARGETS` of the embedded Containerfile, in its order.
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. This
// repository's one cargo row is a single axum binary, so the house
// defaults are ample; they are spelled rather than defaulted because a
// bound nobody wrote down is a bound nobody can audit.
timeout_secs: 3600,
mem_limit_mb: 8192,
cpus: 8,
// The image is self-contained ⇒ `--network none`, `--offline --locked`,
// and every source comes from the host registry bound read-only and
// checksum-pinned by `Cargo.lock`.
network: false,
// rustc bakes absolute directories into debug info; remapping them on
// both sides is what makes two builds of one row comparable at all.
remap_paths: true,
// edda, for the estate sibling patch (`.cargo/estate-siblings.toml`): the container's cargo
// resolves the whole workspace, xtask too, and xtask's nornir-xtask-support 0.1.4 is
// edda's (crates.io holds 0.1.3). Gate holger, loki 2026-10-25.
// monetize, for the same template's `monetize-estate-side` patch (5ff33dd, not on
// crates.io yet): without it the container cannot read
// `../monetize/crates/monetize-estate-side` and `mvp-ui` dies at resolve (ODEN-CI
// round 1, 2026-10-25). `tests/estate_siblings.rs` holds the two lists together.
siblings: vec!["edda".to_string(), "monetize".to_string()],
}
}
/// **The feature universe** holger-ui offers; a variant feature outside it is
/// rejected up front.
///
/// ONE word, `metered` (A28 M13): `holger-ui-web`'s paid half, which the
/// `mvp-ui-web-metered` row turns on. It used to be EMPTY, as a measurement:
/// `holger-ui-web` carried no `[features]` table. The console's other
/// build-time knobs are `option_env!` variables — `HOLGER_GRPC_WEB_URL`,
/// `HOLGER_OIDC_ISSUER`, `HOLGER_OIDC_CLIENT_ID` (`.nornir/ai-context.toml`,
/// README §Dev loop) — which are environment at compile time and NOT features;
/// `offers` would be the wrong place to write them down.
fn offers() -> BTreeSet<String> {
[variants::METERED.to_string()].into_iter().collect()
}
/// The `[[bin]]`s in this workspace that NO variant builds, each with the
/// reason — so the absence is a declaration a reader can check rather than an
/// omission nobody noticed.
fn allow_unbuilt_bins() -> BTreeMap<String, AllowEntry> {
[
(
"xtask",
"repository tasks (`cargo xtask publish | verify-chain | build`), invoked as a \
tool and never shipped; built by every `cargo xtask` run, not by a delivery.",
),
(
SOURCE_CRATE,
"the emission door (`cargo run -p holger-ui-build-thing -- emit`) and the wasm32 \
plugin the dwarves forge loads. Repository tooling on the tool axis, like xtask; \
it owns no build knowledge and ships nowhere.",
),
]
.into_iter()
.map(|(bin, why)| (bin.to_string(), AllowEntry::Reason(why.to_string())))
.collect()
}
/// The header every emission starts with — what the file is, and what it is not.
pub const EMITTED_HEADER: &str = "\
# holger-ui — .nornir/build-variants.toml — EMITTED, NOT SOURCE.
#
# Generated by `cargo run -p holger-ui-build-thing -- emit` from holger-ui-recipe
# (the Rust `impl BuildThing`: crates/holger-ui-recipe/src/variants.rs for the
# build points, lib.rs for the feature universe and the set rules;
# holger-ui-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
# repository by this path (the dwarves forge, the build-thing pane, xtask,
# installtest, the bench harness). Editing it changes NOTHING: re-emit after
# changing the code. Do not edit by hand.
#
# Model: variant -> features -> settings/sub-settings -> typ. holger-ui is a
# console in TWO halves, and each row is one of them:
#
# mvp-ui-web — the wasm32 browser bundle, bundled by trunk, served as static
# files by the nginx quadlet in deploy/, speaking gRPC-web straight to
# holger's tonic-web door for everything it reads. It renders no word
# about money.
# mvp-ui-web-metered — the same bundle with `metered`, into dist-metered/:
# the paid console, with the sign-up, welcome and order pages. holger's
# paid front embeds it.
# mvp-ui — the backend-for-frontend, added 2026-09-17: it holds the passkey
# ceremony, terminates the session cookie, and speaks NATIVE gRPC to holger
# holding the console's own bearer. A browser can do none of those three.
#
# Until that crate existed this file carried ONE row and a note saying the second
# was owed but not declared, because owing a crate is not the same act as
# declaring its build. The crate exists, so the row does.
#
# The reasoning that belongs beside these rows is in the doc comments of the
# code above, and never here: a comment written into this file is deleted by the
# next `emit` without anybody noticing.";
/// The set this crate declares — the one source.
#[must_use]
pub fn declared() -> BuildVariantSet {
HolgerUiBuildThing.variants()
}
/// The emission — what `.nornir/build-variants.toml` must contain.
pub fn emitted_toml() -> Result<String> {
emit::emitted_toml(EMITTED_HEADER, &HolgerUiBuildThing)
}
/// Regenerate `<repo_root>/.nornir/build-variants.toml` from the code.
pub fn write_emitted(repo_root: &Path) -> Result<PathBuf> {
emit::write_emitted(EMITTED_HEADER, &HolgerUiBuildThing, repo_root)
}
/// holger-ui's repository root, from this crate's manifest directory.
///
/// Baked in at compile time rather than discovered by walking up from the
/// current directory, so the answer does not depend on where a binary was
/// invoked from.
#[must_use]
pub fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/holger-ui-recipe has a grandparent (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 (every tool that
/// discovers this repository by that 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-ui-build-thing -- emit`",
path.display()
);
}
Ok(())
}
/// **The shared seam, re-exported.**
///
/// `xtask` is written against `nornir_build_thing`'s resolver
/// (`suite_cli::{parse, resolve}`), and reaching it through this crate is what
/// keeps the build verb's dependency on the build vocabulary at ONE entry — this
/// repository's own declaration — rather than two. It also makes it impossible
/// for the xtask and the recipe to resolve to different versions of the
/// declaration types.
pub use nornir_build_thing;