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

nordisk/gunnar-ui

crates/gunnar-ui-recipe/src/lib.rs

//! # gunnar-ui-recipe — gunnar-ui's build space, declared in Rust
//!
//! THE LAW: the code is the source. This crate's [`GunnarUiBuildThing`] is the
//! ONE declaration of what gunnar-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 gunnar-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.
//!
//! `gunnar-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
//!
//! One console in two halves that build nothing alike: `gunnar-ui-server`, a
//! native static-musl daemon skidbladnir installs, and `gunnar-ui-web`, a
//! `wasm32-unknown-unknown` bundle a browser loads. Each is built twice,
//! community and `metered`, which is the whole of the feature universe. And the
//! product, `gunnar-ui` (`cargo install gunnar-ui`): a thin bin over the daemon's
//! library with the console bundle built in.
//!
//! ```
//! let set = gunnar_ui_recipe::declared();
//! assert_eq!(set.variants.len(), 5);
//! 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 = "gunnar-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 = "gunnar-ui-build-thing";

/// gunnar-ui's build thing.
#[derive(Debug, Clone, Copy, Default)]
pub struct GunnarUiBuildThing;

impl BuildThing for GunnarUiBuildThing {
    fn manifest(&self) -> Manifest {
        Manifest {
            abi: ABI_VERSION,
            repo: REPO.to_string(),
            source_crate: SOURCE_CRATE.to_string(),
            summary: "the gunnar console — the static-musl BFF service and the \
                      wasm32 browser bundle, each in a community and a metered \
                      build, declared in Rust"
                .to_string(),
        }
    }

    fn variants(&self) -> BuildVariantSet {
        BuildVariantSet {
            offers: offers(),
            allow_unbuilt_bins: allow_unbuilt_bins(),
            variants: variants::variants(),
            // Both console bundles (`dist`, `dist-metered`) as Build Thing
            // web-app rows — see `variants::web_apps`.
            web_apps: variants::web_apps(),
            // The environment every cargo row here — and each web app's cargo
            // build — compiles in: the pinned image, not this box.
            toolchain: Some(toolchain()),
            // ONE delivery, named MVP on every row (see `variants.rs`).
            // This used to read "No suite axis" — the workspace SUITE-MODEL
            // overrules that: the tag is what lets one command shape work in
            // every repo in the house.
            //
            // No `exclusive` groups: `metered` is the only feature offered, and
            // a one-member universe has nothing to be exclusive with.
            //
            // No `required`: neither `[[bin]]` declares `required-features`.
            //
            // No `downstream`: nothing here embeds a sibling — both halves are
            // thin and reach their owners — so the asymmetry rule has nothing
            // to hold.
            ..BuildVariantSet::default()
        }
    }
}

/// **The feature universe** gunnar-ui offers; a variant feature outside it is
/// rejected up front.
///
/// Exactly one word, and it is deliberately gunnar's own rather than a spelling
/// invented here: `metered` is declared by `gunnar-ui-server`, by
/// `gunnar-ui-web` and by `gunnar-server` under the same name, so an operator
/// cannot build a console that talks about money against a gunnar that cannot.
fn offers() -> BTreeSet<String> {
    [variants::METERED]
        .into_iter()
        .map(str::to_string)
        .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`), 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 gunnar-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.",
        ),
        // `uitest` is NOT named here and does not need to be: it is a detached
        // crate with its own `[workspace]`, so it is not a member of this one
        // and `cargo metadata` on this workspace has never seen its bin.
    ]
    .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 = "\
# gunnar-ui — .nornir/build-variants.toml — EMITTED, NOT SOURCE.
#
# Generated by `cargo run -p gunnar-ui-build-thing -- emit` from gunnar-ui-recipe
# (the Rust `impl BuildThing`: crates/gunnar-ui-recipe/src/variants.rs for the
# build points, lib.rs for the feature universe and the set rules;
# gunnar-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. gunnar-ui is ONE
# console in two halves that build nothing alike — a native static-musl daemon
# and a wasm32 browser bundle — and each of them is built twice, community and
# `metered`, which is why there are four rows and not two. The fifth is the product,
# `gunnar-ui` (`cargo install gunnar-ui`): the thin bin over the daemon's library with
# the console bundle built in.
#
# 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 {
    GunnarUiBuildThing.variants()
}

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

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

/// gunnar-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/gunnar-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 gunnar-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 its one cargo-driving loop
/// (`build_variant`), and reaching them through this crate is what keeps
/// `xtask/Cargo.toml` at ONE dependency — the 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;

/// **`[build.toolchain]`** — the pinned build image every cargo row of this
/// repository compiles in (workspace law: a build thing builds in a container
/// or a KVM, never on the host). Until 2026-09-28 gunnar-ui declared none, so
/// the console bundle had one road — host `trunk` — and a box without trunk
/// could not build the paid console gunnar's metered front embeds.
///
/// The digest and the target list are the same ones holger-ui, holger and
/// gunnar pin: `nordisk-cargo-toolchain` minted reproducibly from the
/// Containerfile embedded in `nornir-build-thing` (`ARG RUST_VERSION=1.97.1`,
/// `ARG RUST_TARGETS`). A digest names the recipe, not a minting event.
///
/// `siblings` is `monetize` (2026-10-04): `gunnar-ui-server` links the
/// suite's one estate renderer, `nordisk-estate-svg`, as a path with its
/// version kept until it is on crates.io (`../monetize/crates/estate-svg` — see
/// that manifest line for why it is not a `[patch.crates-io]`). Every other
/// `path =` dependency stays inside the checkout (`nornir-build-thing` and
/// Skidbladnir come from the registry — see `crates/gunnar-ui-recipe/Cargo.toml`).
/// The day the renderer is published, the `path =` goes and so does this entry.
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(),
        image_sha256: "86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762".to_string(),
        rust_version: "1.97.1".to_string(),
        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(),
        ],
        timeout_secs: 3600,
        mem_limit_mb: 8192,
        cpus: 8,
        network: false,
        remap_paths: true,
        siblings: vec!["monetize".to_string()],
    }
}