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

nordisk/holger-ui

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;