facett-build-thing/src/lib.rs
//! # facett-build-thing — facett's recipe, as code
//!
//! ## What facett declares, and what it deliberately does not
//!
//! facett is a library constellation — seventy-odd crates a consumer links. It has
//! no exhaustive library catalogue. Falsett declares a focused feature matrix
//! because its embedders unify optional rendering features in the same build.
//!
//! What it does have is what is PRODUCED rather than linked: the two PyPI releases
//! (owner decision 2026-10-02, version [`PYPI_VERSION`]) — `facett`, the components
//! wheel (the wasm bundles + loader, Streamlit glue behind the `[streamlit]` extra),
//! and `facett-demo`, the Streamlit demo app that depends on `facett[streamlit]`.
//! Each is a wheel, the `SHA256SUMS` a downloader runs `sha256sum -c` against, and a
//! provenance record over the produced bytes; `facett-demo` also has an sdist (it is
//! pure Python, so its source distribution can rebuild it). `facett` has NONE: its
//! wheel carries prebuilt wasm, and a source distribution that cannot rebuild that
//! wasm from what it contains would be a binary in a source costume.
//!
//! That release was real and shipped before this crate existed, and reachable only
//! through `cargo ops publish-pip`. `facett-ops/tests/release_manifest.rs` wrote the
//! reason into its own module docs rather than working around it:
//!
//! > The brief's door is `dwarves buildthing facett --variant facet-wrapped-web`.
//! > That needs the forge to have an artifact kind for a **package**, and it does
//! > not: `ArtifactKind` is `Lib | Exe | Wasm | Kernel | Container | Service | Vm |
//! > Iso | Installer`. A wheel is none of those, and `Wasm` would make the forge
//! > build a bare `.wasm` — not run wasm-bindgen, not build the wheel, not verify
//! > it.
//!
//! edda 0.1.13 added the kind ([`ArtifactKind::Package`]). This crate is what gives
//! the forge something to route to.
//!
//! ## THE MODEL
//!
//! The 2026-08-27 law: **the CODE is the source.** [`FacettBuildThing`] is the
//! declaration; `.nornir/build-variants.toml` is EMITTED from it by
//! [`write_emitted`] to DESCRIBE the build for readers and for the doors that
//! discover a repo by that path. The file is never read back as authority, and
//! editing it changes nothing. `tests/toml_parity.rs` holds the committed bytes
//! equal to this code's emission — not two mirrors kept in step by hand, but a
//! writer and a check that it ran.
//!
//! ## The two rows, and why the wasm one is not a `from_variant`
//!
//! The wheel carries a wasm-bindgen bundle: `facet_wrapped_bg.wasm`,
//! `facet_wrapped.js`, `manifest.json`, produced by `cargo xtask wrap-wasm` from
//! the `facet-wrapped` cdylib. Those are declared as the package cell's `inputs`,
//! so a forge run before `wrap-wasm` refuses BY NAME on a missing or 0-byte input
//! instead of building a wheel that loads nothing — the same question
//! `pip::check_wasm_bundle` asks at the xtask door, now said in the declaration so
//! both doors ask it.
//!
//! They are **inputs and not `from_variant`**, and the distinction is honest rather
//! than pedantic. `from_variant` means "the forge builds this cargo variant first",
//! and a cargo build of the cdylib does NOT produce those three files —
//! wasm-bindgen does, in a step that is `cargo xtask wrap-wasm` and not a variant.
//! Declaring the chain would promise the forge produces an input it cannot, and the
//! failure would arrive as a missing file after a build that reported success.
//!
//! ## What this crate does NOT decide
//!
//! The no-JavaScript-graphics rule (`xtask/src/jsguard.rs`) is not declared here,
//! and the forge therefore does not enforce it — see `PackageStager` in edda, which
//! states the difference between the two doors and why closing it means moving that
//! rule into this crate's `validate`. It needs to know that `getContext` inside
//! `facet_canvas_api` is the probe proving the renderer is WebGPU and not a painter
//! acquiring a surface, and no declaration can say that.
use std::path::{Path, PathBuf};
use anyhow::Result;
use nornir_build_thing::cargo_container::{CargoToolchain, CARGO_TOOLCHAIN_IMAGE};
use nornir_build_thing::emit;
use nornir_build_thing::container::ContainerBuild;
use nornir_build_thing::package::{PackageBuild, PackageFormat};
use nornir_build_thing::wasm::{BindgenStage, BindgenTarget, ComponentManifestStage, WasmBuild};
use nornir_build_thing::plugin::{Manifest, ABI_VERSION};
/// Re-exported so a consumer of this crate (the `[[bin]]`, the tests) can call
/// [`FacettBuildThing::variants`] without also naming edda — the trait has to be in
/// scope for its methods to exist, and having to import two crates to ask one
/// declaration a question is friction with no upside.
pub use nornir_build_thing::plugin::BuildThing;
/// **The engine's error type, re-exported for the same reason the engine is**: the
/// seams xtask implements (`wasm::WasmStageRunner`, `cargo_container::
/// CargoContainerRunner`) return `anyhow::Result`, and a second `anyhow` edge in
/// xtask would be a second way to spell one type.
pub use anyhow;
use nornir_build_thing::variant::{AllowEntry, ArtifactKind, BuildVariant, BuildVariantSet, Profile};
/// **The engine, re-exported** — so facett's xtask names ONE dependency (this
/// crate) and reaches every row type, the argv writer (`build_variant`) and the
/// wasm stage runner through it. Two `nornir-build-thing` edges, one here and one
/// in xtask, could resolve two different `BuildVariant` types; one edge cannot.
pub use nornir_build_thing;
/// **The declaration, as the set every door resolves.** `FacettBuildThing.variants()`
/// by its plain name, so a door asks the CODE (never the emitted toml) with one call.
#[must_use]
pub fn declared() -> BuildVariantSet {
FacettBuildThing.variants()
}
/// **The door that DOES build a row cargo cannot**, named — `None` for a cargo kind.
///
/// `cargo xtask build` materializes cargo argv and nothing else, so a `package` or
/// `container` row asked of it is REFUSED; refusing it with "use the image builder"
/// sends the reader hunting. Which door builds a row is a fact about the
/// declaration, so it lives here beside the rows rather than as a list of names in
/// xtask. Keyed on the KIND with the row's own name carried in, so a fourth image
/// needs no edit here.
#[must_use]
pub fn door_for(v: &BuildVariant) -> Option<String> {
if v.kind.is_cargo() {
return None;
}
let forge = format!("`dwarves buildthing facett --variant {}`", v.name);
Some(match v.kind {
ArtifactKind::Package => format!(
"built by `cargo ops publish-pip` (the facett + facett-demo wheels, the demo's sdist, \
SHA256SUMS + provenance, inside the pinned `{PIP_TOOLCHAIN_IMAGE}` container, after \
`cargo xtask wrap-wasm` has built every bundle group), or by the forge: {forge}"
),
ArtifactKind::Container => format!(
"built by the forge: {forge} (file-mode [[build.container]] over {CONTAINERFILE_DIR}/); \
the facett-ops verbs that run it (publish-pip, verify-install, streamlit, test-demo) \
mint it themselves"
),
other => format!("kind '{}' — built by the forge: {forge}", other.as_str()),
})
}
/// This crate, as the receipt names it.
pub const KRATE: &str = "facett-build-thing";
/// **The delivery facett's rows ship in.** `suites` is a LIST — one row may
/// ship in more than one flavour — but facett has exactly one, and writing it
/// down is the whole point: an EMPTY list is not a third flavour, it is a row
/// that `cargo xtask build --suite ORIGINAL` cannot select. All SEVEN rows here
/// were exactly that until 2026-09-20. The only other flavour anywhere today is
/// holger's MVP; every other product declares ORIGINAL alone and TAGS it, so
/// the build verb has ONE shape everywhere.
pub const SUITE: &str = "ORIGINAL";
/// The declared PACKAGE variant of the COMPONENTS wheel (PyPI `facett`) — the name
/// `dwarves buildthing facett --variant …` takes for it. Until 0.2.0 this row was
/// `facet-wrapped-web`, after the distribution it built.
pub const VARIANT: &str = "facett-wheel";
/// The declared PACKAGE variant of the DEMO wheel (PyPI `facett-demo`). Not
/// `facett-demo`: that name is the native demo binary's row ([`DEMO_VARIANT`]).
pub const DEMO_PIP_VARIANT: &str = "facett-demo-wheel";
/// **The ONE version both PyPI packages carry** (owner decision 2026-10-02: 0.2.0;
/// 0.2.1 on 2026-10-03 — the re-release with no personal name in any packaged file,
/// held by `cargo ops publish-pypi`'s name gate). Separate from the cargo workspace
/// version (the crates move on their own line, even when the numbers coincide); `pkg-facet-wrapped/pyproject.toml`, `py/pyproject.toml` and the demo's
/// `facett[streamlit]` pin are held to it by
/// `xtask/tests/python_versions_track_the_workspace.rs`.
pub const PYPI_VERSION: &str = "0.2.1";
/// The PyPI project names, as a user types them (`pip install facett-demo`).
pub const PYPI_PROJECT: &str = "facett";
/// See [`PYPI_PROJECT`].
pub const DEMO_PYPI_PROJECT: &str = "facett-demo";
/// The declared WASM variant — the bundle the wheel carries.
///
/// It is on the roster in its own right, and that is half the point: a variant the
/// roster does not have answers `no such variant`, which is how tunnr ended up with
/// seven ISO cells nobody can dispatch.
pub const WASM_VARIANT: &str = "facet-wrapped-wasm";
/// The cdylib crate the bundle is built from.
pub const WASM_PACKAGE: &str = "facet-wrapped";
/// The components wheel's distribution name as its filename spells it — which is
/// also its IMPORT package (`import facett`), the same constant
/// `facett_ops::pip::IMPORT_PACKAGE` carries.
pub const DISTRIBUTION: &str = "facett";
/// The pre-0.2.0 import name, shipped in the same wheel as a thin alias of
/// [`DISTRIBUTION`] for ONE release (removed in 0.3.0).
pub const COMPAT_IMPORT: &str = "facet_wrapped_web";
/// The demo wheel's distribution name as its filename spells it
/// (`facett_demo-0.2.0-py3-none-any.whl`), which is also its import package.
pub const DEMO_DISTRIBUTION: &str = "facett_demo";
/// The demo's package source (pure Python) — the app, its payload table, the
/// error-code catalog.
pub const DEMO_SOURCE: &str = "py";
/// Where the demo's release set lands.
pub const DEMO_OUT_DIR: &str = "py/dist";
/// The console script the demo wheel installs (`[project.scripts]`), and what it runs.
pub const DEMO_ENTRY_POINT: (&str, &str) = ("facett-demo", "facett_demo.cli:main");
/// The pinned toolchain image `python -m build` runs inside. **No python runs on
/// the host**, ever: the image carries the exact PEP 517 backend, which is why the
/// build can run with the network off and get the same backend every time.
pub const PIP_TOOLCHAIN_IMAGE: &str = "nordisk-pip-toolchain";
/// Where `cargo xtask wrap-wasm` writes the bundle this package carries.
pub const WASM_DIST: &str = "facet-wrapped/dist";
/// Where the produced release set lands — the directory `publish-pip` already
/// writes and `publish-holger` already serves.
pub const OUT_DIR: &str = "pkg-facet-wrapped/dist";
/// The package SOURCE the format's tool is run over.
pub const SOURCE: &str = "pkg-facet-wrapped";
/// The three files `cargo xtask wrap-wasm` produces and the wheel must carry.
///
/// `index.html` is NOT here and must not be: it is package SOURCE, the loader page
/// that lives in `pkg-facet-wrapped/src/…/assets/`. Staging it as a build output
/// would mean the page that ships came from somewhere other than the committed
/// package — and that page is the one file facett's js guard scans.
pub const WASM_OUTPUTS: [&str; 3] =
["facet_wrapped_bg.wasm", "facet_wrapped.js", "manifest.json"];
// ── THE BUNDLE GROUPS — facet-wrapped's facets, split into loadable bundles ─────
//
// Owner, 2026-09-29: docview + manual + model3d grow the one bundle from 12.64 to
// ~16.44 MB, so they ship in bundles of their own, and the grouping goes THROUGH the
// build thing (variant -> features -> settings -> typ). Each group becomes, by
// construction and nowhere else:
// * one `kind = "wasm"` [[build.variant]] row (suites ORIGINAL),
// * one [[build.wasm]] cell (bindgen + the component manifest, into its dist dir),
// * its files' place in the wheel (`assets/` for core, `assets/<group>/` else),
// * one line of the ROUTING TABLE the loader reads ([`routing_json`]).
//
// WHICH PACKAGE (measured by lane A, 2026-09-29): `facet-wrapped` can carry NO cargo
// features — even an empty `[features]` table changes 14 517 bytes of its release
// wasm — so the core group is `facet-wrapped` exactly as it always was, and every
// other group is `facet-wrapped-group` (same crate root, `[lib] path` =
// facet-wrapped's `src/lib.rs`, lib name `facet_wrapped_group`) built with ONE group
// feature. That package has no default features, so a group row needs no
// `--no-default-features`; its `group-only` feature (pulled by each group feature)
// drops the core registrations.
//
// ENTRY -> GROUP has ONE source, [`BundleGroup::entries`] here. facet-wrapped's
// `groups::GROUP_ENTRIES` (the refusal the group wasm makes) is held to it by
// `facet-wrapped-group/tests/groups.rs::group_entries_are_the_recipes`, and the
// loader reads it from the emitted `bundles.json` — never from a hand list.
/// **One bundle group.** Every field is a fact the rows, the cells, the wheel and
/// the routing table are DERIVED from; nothing downstream re-states any of them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BundleGroup {
/// The group's name — what a registry entry is tagged with and what the
/// routing table keys on. `core` is the default bundle.
pub name: &'static str,
/// The `[[build.variant]]` / `[[build.wasm]]` name of this group's row.
pub variant: &'static str,
/// The cargo package the row builds. Its lib stem (`-` → `_`) names the
/// bundle's files: `<stem>_bg.wasm` and `<stem>.js` (edda's `wasm::crate_stem`).
pub package: &'static str,
/// The package's cargo features this group compiles with.
pub features: &'static [&'static str],
/// Build WITHOUT the package's default features. `false` for every group today
/// (`facet-wrapped-group` declares no defaults); a field so the row says it.
pub no_default_features: bool,
/// Repo-relative dist dir the cell writes (rebuilt every run).
pub dist_dir: &'static str,
/// Where the group's files sit under the wheel's `assets/`: `""` = `assets/`
/// itself (core, unchanged), else `assets/<wheel_dir>/`.
pub wheel_dir: &'static str,
/// The registry entries this group holds. EMPTY for core, which holds every
/// entry no other group names (the routing table's `default`).
pub entries: &'static [&'static str],
}
impl BundleGroup {
/// This group's files under `src/<DISTRIBUTION>/assets/`, as the wheel holds them.
#[must_use]
pub fn wheel_path(&self, file: &str) -> String {
if self.wheel_dir.is_empty() {
file.to_string()
} else {
format!("{}/{file}", self.wheel_dir)
}
}
/// The three files the group's cell writes, in [`WASM_OUTPUTS`] order: the
/// bindgen'd wasm, its JS glue, the component manifest. Derived from the package
/// the way edda derives them (`tests/thin_door.rs` holds the two equal).
#[must_use]
pub fn outputs(&self) -> [String; 3] {
let stem = self.package.replace('-', "_");
[format!("{stem}_bg.wasm"), format!("{stem}.js"), WASM_OUTPUTS[2].to_string()]
}
}
/// The default group: today's bundle, byte-identical (no features, defaults on).
pub const CORE_GROUP: &str = "core";
/// The package every NON-core group builds (see the section comment).
pub const GROUP_PACKAGE: &str = "facet-wrapped-group";
/// **THE groups, in load order.** `core` first and unchanged — its row keeps the
/// name `facet-wrapped-wasm`, its package `facet-wrapped` and its dir
/// `facet-wrapped/dist`, so every door that already names the default bundle keeps
/// working and its bytes do not move.
pub const BUNDLE_GROUPS: &[BundleGroup] = &[
BundleGroup {
name: CORE_GROUP,
variant: WASM_VARIANT,
package: WASM_PACKAGE,
features: &[],
no_default_features: false,
dist_dir: WASM_DIST,
wheel_dir: "",
entries: &[],
},
BundleGroup {
name: "docs",
variant: "facet-wrapped-docs-wasm",
package: GROUP_PACKAGE,
features: &["docs"],
no_default_features: false,
dist_dir: "facet-wrapped-group/dist-docs",
wheel_dir: "docs",
entries: &["docview", "manual"],
},
BundleGroup {
name: "model3d",
variant: "facet-wrapped-model3d-wasm",
package: GROUP_PACKAGE,
features: &["model3d"],
no_default_features: false,
dist_dir: "facet-wrapped-group/dist-model3d",
wheel_dir: "model3d",
entries: &["model3d"],
},
BundleGroup {
name: "sim",
variant: "facet-wrapped-sim-wasm",
package: GROUP_PACKAGE,
features: &["sim"],
no_default_features: false,
dist_dir: "facet-wrapped-group/dist-sim",
wheel_dir: "sim",
entries: &["simview"],
},
];
/// A group by name.
#[must_use]
pub fn bundle_group(name: &str) -> Option<&'static BundleGroup> {
BUNDLE_GROUPS.iter().find(|g| g.name == name)
}
/// The group an ENTRY is in: the group that names it, else core.
#[must_use]
pub fn group_of_entry(entry: &str) -> &'static BundleGroup {
BUNDLE_GROUPS
.iter()
.find(|g| g.entries.contains(&entry))
.unwrap_or(&BUNDLE_GROUPS[0])
}
/// The routing table's file name, in the wheel's `assets/`.
pub const ROUTING_FILE: &str = "bundles.json";
/// Where the emitted routing table is written — package SOURCE, beside the loader
/// page that reads it, so the wheel carries it without a build step.
pub const ROUTING_PATH: &str = "pkg-facet-wrapped/src/facett/assets/bundles.json";
/// **The ROUTING TABLE, emitted from [`BUNDLE_GROUPS`]** — one direction, like the
/// toml: `{"schema":2,"default":"core","groups":[{"name","dir","wasm","js",
/// "manifest","features","entries"}]}`, paths relative to the wheel's `assets/`.
/// The loader routes an entry to the group whose `entries` name it, else to
/// `default`. Hand-formatted: this crate carries no serde, and the shape is flat.
#[must_use]
pub fn routing_json() -> String {
let q = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
let list = |xs: &[&str]| xs.iter().map(|x| q(x)).collect::<Vec<_>>().join(", ");
let groups: Vec<String> = BUNDLE_GROUPS
.iter()
.map(|g| {
let [wasm, js, manifest] = g.outputs();
format!(
" {{\"name\": {}, \"dir\": {}, \"wasm\": {}, \"js\": {}, \"manifest\": {}, \"features\": [{}], \"entries\": [{}]}}",
q(g.name),
q(g.wheel_dir),
q(&g.wheel_path(&wasm)),
q(&g.wheel_path(&js)),
q(&g.wheel_path(&manifest)),
list(g.features),
list(g.entries)
)
})
.collect();
format!(
"{{\n \"_\": \"EMITTED by facett-build-thing (BUNDLE_GROUPS) — do not edit; re-run `cargo run -p facett-build-thing -- emit`\",\n \"schema\": 2,\n \"default\": {},\n \"groups\": [\n{}\n ]\n}}\n",
q(CORE_GROUP),
groups.join(",\n")
)
}
// ── THE DEMO, which is a PRODUCT and not a library ───────────────────────────
//
// Added 2026-09-18. Until this pass the declaration held the wheel and the bundle
// it carries, and the argument for stopping there — "facett's seventy crates are
// LINKED by consumers, not built" — is right about the LIBRARIES and says nothing
// about `facett-demo`. The demo is not linked by anybody: it is a binary a human
// runs and a wasm bundle a browser loads, and both were reachable only by typing
// `cargo xtask demo-wasm` from memory. That is the same shape Road B closed for
// `wrap-wasm` one row up — a produced artifact with no declared door.
/// The declared EXE variant — the native demo binary.
pub const DEMO_VARIANT: &str = "facett-demo";
/// The declared WASM variant — the gh-pages bundle `deploy-pages` publishes.
///
/// A row of its own rather than a flag on [`DEMO_VARIANT`]: the two are built from
/// one crate but they are different artifacts with different toolchains, and a
/// variant that meant "the native one, unless…" could not be dispatched by name.
pub const DEMO_WASM_VARIANT: &str = "facett-demo-wasm";
/// The crate both demo rows build.
pub const DEMO_PACKAGE: &str = "facett-demo";
/// Where the `facett-demo-wasm` cell writes the gh-pages bundle (`xtask demo-wasm`
/// builds the row in the pinned image; edda `nornir_pages::stage_dist` adds the page).
pub const DEMO_WASM_DIST: &str = "dist";
/// The demo HOST's `panel` fixture row (a cargo EXAMPLE, see the row).
pub const PANEL_WASM_VARIANT: &str = "facett-wasm-demo-panel";
/// The reusable demo HOST every `<repo>-demo` mounts on.
pub const HARNESS_PACKAGE: &str = "facett-wasm-demo";
/// The example the fixture row builds.
pub const PANEL_EXAMPLE: &str = "panel";
/// Where the fixture's bundle lands (`panel.js`, `panel_bg.wasm`, + the page).
pub const PANEL_DIST: &str = "facett-wasm-demo/dist";
/// **The demo's wasm feature axis**, and it is not decoration: the native-only
/// `region-clip`/osm-katana lanes do not build for wasm32, and the GPU lanes are
/// what give the browser build the wgpu (WebGPU) backend and the real 3D OSM
/// render instead of the old glow/CPU painters; `gpu` (the instanced graph cloud) and
/// `graph-layout-gpu` (the compute-shader force layout, lent the host's device) joined
/// on 2026-09-19 — the same two lanes the wheel already selects and installs. `graph-wgpu`
/// (the 2-D GraphView on the same cloud) joined on 2026-10-01, the lane the wheel
/// already carries as `facett-graph/wgpu`. `flow-wgpu` (Falsett's resident curve and
/// packet renderer, `facett-flow/wgpu`) joined on 2026-10-04, so the pages demo draws
/// the full flow graph instead of the 2 000-point CPU preview. The same strings
/// `pages_cfg().extra_build_args` passes — one axis, declared where the forge can
/// read it rather than living only inside an xtask literal.
pub const DEMO_WASM_FEATURES: [&str; 6] =
["map-wgpu", "map3d-wgpu", "gpu", "graph-layout-gpu", "graph-wgpu", "flow-wgpu"];
/// The same axis as a comma-joined cargo argument.
///
/// Held EQUAL to [`DEMO_WASM_FEATURES`] by `tests/declared.rs` — two spellings of
/// one fact inside one crate is still two writers, and the test is what makes the
/// pair safe rather than the comment.
pub const DEMO_WASM_FEATURE_LIST: &str = "map-wgpu,map3d-wgpu,gpu,graph-layout-gpu,graph-wgpu,flow-wgpu";
/// **The demo's wasm build arguments, verbatim** — what `nornir_pages` passes and
/// what the `facett-demo-wasm` row declares, from ONE place.
///
/// This constant exists because the axis had two writers: a string literal in
/// `xtask::pages_cfg` and (since the row landed) the declaration. They agreed on
/// the day the row was written, which is the only day drift is impossible. The
/// xtask now reads this, so `cargo xtask deploy-pages` and
/// `cargo xtask build --variant facett-demo-wasm` cannot compile different code and
/// call it the same demo.
pub const DEMO_WASM_CARGO_ARGS: [&str; 3] =
["--no-default-features", "--features", DEMO_WASM_FEATURE_LIST];
// ── THE IMAGES THIS REPO MASTERS ─────────────────────────────────────────────
//
// `..BuildVariantSet::default()` used to carry the comment "no containers/vms/isos
// /installers/kernels (facett masters no media)". Half of that was true and the
// container half was not: three Containerfiles live in `facett-ops/src/containerfiles/`,
// this repo is the only writer of them, and `facett-ops` mints all three. They are
// declared in FILE MODE — the bytes on disk are the build input, so `RUN`, `ARG`
// and the digest-pinned `FROM` in each recipe are the declaration, not a synthesis
// this crate would have to re-say and drift from.
/// The image the Streamlit demo RUNS in.
pub const STREAMLIT_IMAGE: &str = "nordisk-streamlit";
/// The clean-consumer image `verify-install` proves the published wheel in.
pub const PIP_CONSUMER_IMAGE: &str = "nordisk-pip-consumer";
/// Where this repo's Containerfiles live, repo-root-relative. The context dir a
/// file-mode cell derives from each path is this directory, which is what
/// `facett_ops::holger`/`facett_ops::pip` stage as the build context.
pub const CONTAINERFILE_DIR: &str = "facett-ops/src/containerfiles";
/// **The MANIFEST digest of the one cargo toolchain image this constellation
/// compiles in** — the digest the committed edda recipe mints (see the re-pin note
/// below; it used to say "gunnar's value, to the character", and gunnar's value
/// was not what the recipe mints), because a second spelling would be a second
/// image nobody minted.
///
/// It is a digest and not a tag on purpose, and the sentence the forge refuses with
/// is the reason: *a named tag is a convenience, NOT a pin, and an unpinned build
/// image can be replaced under you between two builds that report the same
/// provenance*.
///
/// **What this digest identifies, said exactly, because the answer changed.** Until
/// edda `ac3852e` (2026-09-05) `image_sha256` was a PER-BOX fact: the cargo
/// Containerfile left its base, its archive and its clock unpinned, so every mint
/// landed a new digest and a pin named a minting EVENT rather than a recipe —
/// gunnar moved this line twice in one day for that reason, and
/// `edda/crates/nornir-build-thing/tests/toolchain_images_live.rs:67` still carries
/// the old finding ("a pinned digest needs an image PULLED from a shared
/// registry"). `ac3852e` made the cargo file make the four moves its kernel sibling
/// already made — base by digest, a `snapshot.debian.org` instant, `ENV
/// SOURCE_DATE_EPOCH=1783900800`, the clock's logs deleted in the layer that writes
/// them — and `DraupnirCargoRunner::build_toolchain_image` now mints through
/// `build_image_reproducible`. So the digest identifies the RECIPE
/// (`edda/crates/nornir-build-thing/src/containerfiles/cargo-toolchain.Containerfile`),
/// not one box's copy of it, and no registry is behind the name: there is nothing
/// to pull `nordisk-cargo-toolchain` FROM (`short-name did not resolve`, `403
/// denied`). A box that has never minted it mints it —
/// `cargo run -p nornir-build-thing --features cargo-container --example
/// mint_cargo_toolchain` — and lands these 64 characters.
///
/// MEASURED on t14s 2026-09-06 by this row: the box's own
/// `nordisk-cargo-toolchain:1.97.1` is `f29e517c1ba34b920c…`, minted 2026-08-25 —
/// eleven days BEFORE the recipe became reproducible, which is why it is a
/// different number and not a contradiction. That stale digest is also the constant
/// `dwarves/crates/dwarves/src/build_thing/code.rs:768` still hands its own test
/// scaffolding.
///
/// **RE-PINNED 2026-09-29 (lane C, container road), `2cf9b012…` → `3d5115ba…`.**
/// The first `cargo xtask build` that actually ENTERED this image refused by name:
/// the committed recipe (edda `nornir-build-thing` at `114fabb`) re-minted on this
/// box as a layer-cache hit — id `ab4b8dc004e8…`, `created` = 2026-07-13T00:00:00Z,
/// which is `SOURCE_DATE_EPOCH=1783900800`, i.e. the recipe's instant, not the
/// minute of the mint — and its MANIFEST digest is `3d5115ba…`. Nothing on this box
/// answered to `2cf9b012…`. The new value is not this box's opinion: it is the same
/// 64 characters edda's own recipe pins (`edda-build-thing/src/recipe.rs:237`) and
/// holger measured on loki on 2026-09-17 (`holger/xtask/src/main.rs`, "building in
/// nordisk-cargo-toolchain@sha256:3d5115ba…"). Two boxes, one recipe, one digest.
/// `2cf9b012…` was gunnar's value on the day this row was written and nothing had
/// ever built in it here — a pin nobody entered is a pin nobody checked.
///
/// **RE-PINNED 2026-09-30 (lane P, dwarf), `3d5115ba…` → `668468e7…`**: edda
/// `6b5f905` adds `clang llvm` to the recipe (C for wasm32 — zstd-sys under parquet
/// `zstd`, which `facett-demo-wasm` needs). Minted twice on dwarf, same digest.
pub const CARGO_TOOLCHAIN_SHA256: &str =
"668468e775ad80f32d700cbc9709d41cac726f40d00649461a0932f30d1b2067";
/// The rustc release baked into the image above, and compared against what the
/// container reports — a mismatch is a NAMED refusal, not a shrug.
pub const RUST_VERSION: &str = "1.97.1";
/// **The sibling CHECKOUTS facett's path dependencies escape into**, by directory
/// name under the repo's own parent. Not discovered — DECLARED, because each name is
/// one more tree that ends up inside the build container.
///
/// MEASURED 2026-09-06 by walking every `Cargo.toml` in the repo (the same walk
/// `escaping_path_deps` does): `facett-core`, `facett-cfd`, `facett-docview`,
/// `facett-graph3d`, `facett-graphnav`, `facett-map3d` and `facett-demo` reach into
/// `znippy-zoomies`; `xtask`, `facett-about`, `facett-claude`, `facett-deploykey`,
/// `facett-zoneui`, `facett-pipeline`, `facett-demo`, `facett-buildthing` and THIS
/// crate reach into `edda`; `facett-pipeline` and `facett-demo` into `knut`;
/// `facett-demo` into `korp-collectors`; `xtask` into `draupnir`; `bench` into
/// `nornir`. Undeclared, the container holds facett alone and cargo dies on
/// `failed to load manifest for workspace member … failed to read
/// ../znippy-zoomies/Cargo.toml` — an error naming a path instead of a
/// declaration.
///
/// **`funnel-core` is on this list and it is NOT one of facett's own escapes**, so
/// it is worth saying why rather than letting a reader hunt for the dependency that
/// is not there. `check_escaping_path_deps` walks THIS repo's manifests only, and by
/// that walk the list was complete — and the container still refused, MEASURED at
/// the door on 2026-09-07 with the six above declared:
///
/// ```text
/// error: failed to get `funnel-core` as a dependency of package
/// `dwarves-runa v0.2.2 (/work/edda/crates/dwarves-runa)`
/// Caused by: failed to read `/work/funnel-core/Cargo.toml`
/// ```
///
/// A mounted sibling has escapes of its own. `facett-pipeline` and `facett-demo`
/// reach into `edda/crates/dwarves-runa`, whose manifest reaches on into
/// `../../../funnel-core` — a second hop the checker never looks at, because it
/// starts and stops inside the repo it is given. So this list is the TRANSITIVE
/// closure and the check is only its first level; the closure was read off `cargo
/// metadata` (`draupnir`, `edda`, `funnel-core`, `knut`, `znippy-zoomies` resolve;
/// `korp-collectors` and `nornir` are declared-but-unresolved — optional, and
/// `bench/` is a detached workspace — and stay listed because the first-level check
/// demands every name it can see).
pub const SIBLINGS: [&str; 7] =
["draupnir", "edda", "funnel-core", "knut", "korp-collectors", "nornir", "znippy-zoomies"];
/// **The `[build.toolchain]` cell — the pinned image every CARGO build of this repo
/// runs in, including the forge's build of THIS crate.**
///
/// ## The defect this closes
///
/// Until 2026-09-06 facett declared no toolchain at all, and the comment where it
/// should have been said *"no `toolchain` (the package cell names its own pinned
/// image)"*. That conflated two images that are not the same image.
/// [`PIP_TOOLCHAIN_IMAGE`] is where `python -m build` runs; this one is where CARGO
/// runs. The consequence was measured at the real door on t14s:
///
/// ```text
/// ── kod · cargo build -p facett-build-thing --lib --release --target wasm32-unknown-unknown
/// ── kod · VÄGRAT: [build.toolchain] i `/home/rickard/git/facett`: [build.toolchain]
/// `nordisk-cargo-toolchain`: no `image_sha256` — a named tag is a convenience, NOT a
/// pin, and an unpinned build image can be replaced under you between two builds that
/// report the same provenance
/// ── väg · deklarationen (toml) — kodvägen VÄGRAD: …
/// ```
///
/// The forge's code road builds this crate to wasm32 INSIDE the repo's declared
/// toolchain, and it resolves that toolchain with `…toolchain.unwrap_or_default()`
/// followed by `validate()`
/// (`dwarves/crates/dwarves/src/build_thing/code.rs:605-608`). An undeclared
/// toolchain therefore arrives as `CargoToolchain::default()` — the right IMAGE
/// NAME and an empty digest — and is refused for being unpinned. The refusal is
/// correct; what it cost is the inversion this crate exists to prevent. With the
/// code road shut, the forge falls back to PARSING `.nornir/build-variants.toml`,
/// so the file that is EMITTED to describe the build becomes the thing that drives
/// it, and the declaration nobody could load is the one in Rust.
///
/// ## Why the shape is gunnar's (the digest is the recipe's)
///
/// LAW 5, reuse and never twin. gunnar is the repo that brought
/// `[build.toolchain]` into loader vocabulary
/// (`nornir-build-thing` 0.1.9, 2026-08-26) and `gunnar-recipe`'s `toolchain()` is
/// the shape this follows key for key. The rows differ in [`SIBLINGS`] — a fact
/// about THIS repo's path dependencies — and, since 2026-09-29, in the digest:
/// facett pins what the committed recipe mints (see [`CARGO_TOOLCHAIN_SHA256`]).
///
/// `targets` names all three triples although facett builds on only one of them
/// (`wasm32-unknown-unknown`, the recipe crate and the `facet-wrapped-wasm` bundle).
/// The field is a statement about what the IMAGE carries, not about what this repo
/// uses — `check_declared_targets` holds it against the Containerfile's own `ARG
/// RUST_TARGETS`, and that is the only thing that can put a std inside — so
/// declaring two of three would be declaring the image wrong.
///
/// `network = false` is what makes the compile airgapped: the runner adds
/// `--network none --offline --locked` and binds the host registry read-only.
fn the_cargo_toolchain() -> CargoToolchain {
CargoToolchain {
image: CARGO_TOOLCHAIN_IMAGE.to_string(),
image_sha256: CARGO_TOOLCHAIN_SHA256.to_string(),
rust_version: RUST_VERSION.to_string(),
targets: vec![
"x86_64-unknown-linux-gnu".to_string(),
"x86_64-unknown-linux-musl".to_string(),
"wasm32-unknown-unknown".to_string(),
],
timeout_secs: 3600,
mem_limit_mb: 8192,
cpus: 8,
network: false,
remap_paths: true,
siblings: SIBLINGS.iter().map(|s| (*s).to_string()).collect(),
}
}
/// facett's declaration.
#[derive(Debug, Clone, Copy, Default)]
pub struct FacettBuildThing;
// **The forge loads this crate as CODE**, not as the file it emits.
//
// One source, two link targets: the `cdylib` is the wasm32 module
// `dwarves buildthing facett` loads and CALLS, in a sandbox granted nothing; the
// `rlib` is what the tests and facett's own xtask link. Without it the forge falls
// back to parsing the emitted toml and says so out loud — which is reading the
// description instead of asking the thing it describes.
nornir_build_thing::export_build_thing!(FacettBuildThing);
/// The `[[build.wasm]]` cell of ONE bundle group — the TYPED STAGES that turn cargo's raw
/// `facet_wrapped.wasm` into the bundle the wheel carries.
///
/// This is Road B, and it replaces a step nobody declared. Until 2026-09-06 the
/// three files below were produced by a human running `cargo xtask wrap-wasm` from
/// memory, and the wheel's door worked only afterwards — two lanes hand-built that
/// day because the step is invisible to the declaration.
///
/// Both stages are TYPED. There is no command field here and there must never be
/// one: a build declaration with a command in it is a shell script with extra
/// steps. `BindgenTarget::Web` is an enum value, not a string; the output filenames
/// are DERIVED from `facet-wrapped` (→ `facet_wrapped.js`, `facet_wrapped_bg.wasm`)
/// so the `_bg` split cannot be misspelled into a wheel that stages a file nobody
/// wrote; and the bindgen VERSION is not here at all — it comes from `Cargo.lock`,
/// because a CLI that does not match the lock produces a bundle that loads, runs
/// and paints nothing.
fn the_wasm_bundle(g: &BundleGroup) -> WasmBuild {
WasmBuild {
name: g.variant.to_string(),
out_dir: g.dist_dir.to_string(),
// ① `--target web --no-typescript`, which is what `wrap-wasm` has always
// passed: the loader page is a plain `<script type="module">`, not a
// bundler build, and a `.d.ts` nobody consumes is a file in the bundle that
// no declaration accounts for.
bindgen: Some(BindgenStage { target: BindgenTarget::Web, typescript: false }),
// ② the component manifest — `facet-wrapped/examples/manifest.rs`, ten lines
// that print `Registry::with_builtins().manifest_json()` on stdout so a
// loader can enumerate components without instantiating the module first.
//
// THIS STAGE IS WHY ROAD B IS TWO STAGES AND NOT ONE. `wrap-wasm` is three
// steps, not two, and `manifest.json` is a cargo-example capture rather
// than bindgen output. A bindgen-only cell would have left the wheel's
// third input undeclared and the door refusing on it one stage later —
// which is the very "promises a product it does not produce" defect the
// `from_variant` chain was refuted for.
manifest: Some(ComponentManifestStage {
example: "manifest".to_string(),
file: "manifest.json".to_string(),
}),
}
}
/// The COMPONENTS wheel's `[[build.package]]` cell (PyPI `facett`) — what the release IS.
fn the_package() -> PackageBuild {
PackageBuild {
name: VARIANT.to_string(),
format: PackageFormat::Pip,
distribution: DISTRIBUTION.to_string(),
out_dir: OUT_DIR.to_string(),
source: Some(SOURCE.to_string()),
// WHEEL ONLY, since 0.2.0 (row 10's facet-wrapped-web release carried an sdist).
// This wheel's payload is PREBUILT wasm: the Rust that produces it is not in
// the package and cannot be rebuilt by `pip install` from a source tarball. An
// sdist would be a tarball of the same binaries wearing the "source" label —
// what PyPI users and auditors take an sdist to be is exactly what it would not
// be. The source is the repository; the wheel says where (Project-URL).
sdist: false,
image: Some(PIP_TOOLCHAIN_IMAGE.to_string()),
// EVERY bundle group's three files, each where the wheel holds it
// (`assets/` for core — unchanged — and `assets/<group>/` for the others),
// derived from [`BUNDLE_GROUPS`]. The routing table is package SOURCE
// (emitted into `pkg-facet-wrapped/src/…/assets/`), so it is not an input.
inputs: BUNDLE_GROUPS
.iter()
.flat_map(|g| {
g.outputs().into_iter().map(move |f| {
format!("{}/{f}:src/{DISTRIBUTION}/assets/{}", g.dist_dir, g.wheel_path(&f))
})
})
.collect(),
// NOT a `from_variant` chain, and the field below is why. `from_variant`
// means "cargo-build this variant", and cargo does not produce a wasm
// BUNDLE: it writes `facet_wrapped.wasm` (11 647 012 B) while this package
// stages `facet_wrapped_bg.wasm` (10 116 976 B — bindgen's rewrite), a JS
// glue module and a manifest. Measured 2026-09-06.
from_variant: Vec::new(),
// ⚠ NO `from_wasm` since the bundle groups (2026-09-29), and this is a
// LOSS, said as one. edda's `from_wasm` is an `Option<String>` — ONE
// producer cell — and `validate_package` refuses any input that one cell
// does not write, so a wheel carrying three groups' bundles cannot name a
// producer at all. What moved to facett until edda takes a LIST: the
// bijection (every input is written by exactly one group cell, every group
// output is staged) is `tests/declared.rs::the_wheel_stages_exactly_what_the_group_cells_produce`,
// and the forge now refuses a missing group input BY NAME at staging instead
// of building the cells first.
from_wasm: None,
}
}
/// The DEMO wheel's `[[build.package]]` cell (PyPI `facett-demo`).
///
/// Pure Python: the app, its panes, the gallery's payload table and the error-code
/// catalog, all package SOURCE under [`DEMO_SOURCE`] — so it has NO `inputs` (nothing
/// is built into it) and it DOES have an sdist, because that tarball genuinely is the
/// source and `pip` can rebuild the wheel from it. It names the components wheel only
/// as a dependency (`facett[streamlit]`), never by staging its files.
fn the_demo_package() -> PackageBuild {
PackageBuild {
name: DEMO_PIP_VARIANT.to_string(),
format: PackageFormat::Pip,
distribution: DEMO_DISTRIBUTION.to_string(),
out_dir: DEMO_OUT_DIR.to_string(),
source: Some(DEMO_SOURCE.to_string()),
sdist: true,
image: Some(PIP_TOOLCHAIN_IMAGE.to_string()),
inputs: Vec::new(),
from_variant: Vec::new(),
from_wasm: None,
}
}
/// The `[[build.wasm]]` cell for the DEMO bundle — the second wasm row, and
/// deliberately a thinner one than [`the_wasm_bundle`].
///
/// One stage, not two. `facet-wrapped`'s bundle needs a `manifest` stage because
/// the wheel carries a component manifest a loader reads before instantiating the
/// module; the demo carries no such file — `nornir_pages::stage_dist` writes a site
/// shell around the bundle instead, and a manifest stage here would declare a file
/// nothing produces and nothing reads.
fn the_demo_wasm_bundle() -> WasmBuild {
WasmBuild {
name: DEMO_WASM_VARIANT.to_string(),
out_dir: DEMO_WASM_DIST.to_string(),
// `--target web --no-typescript`, the same two flags `nornir_pages` passes:
// the loader is a plain `<script type="module">` and a `.d.ts` nobody
// consumes is a file in the bundle no declaration accounts for.
bindgen: Some(BindgenStage { target: BindgenTarget::Web, typescript: false }),
manifest: None,
}
}
/// The fixture's cell: bindgen only, like the demo's — the harness's own
/// `IndexHtml` writes the page around it (`xtask panel-wasm`).
fn the_panel_wasm_bundle() -> WasmBuild {
WasmBuild {
name: PANEL_WASM_VARIANT.to_string(),
out_dir: PANEL_DIST.to_string(),
bindgen: Some(BindgenStage { target: BindgenTarget::Web, typescript: false }),
manifest: None,
}
}
/// The three `[[build.container]]` cells — every image this repo masters, in FILE
/// MODE.
///
/// File mode and not synthesis, for a reason that is the same law as everywhere
/// else here: the Containerfiles exist, they are the input `xtask` already builds,
/// and re-saying their `FROM debian@sha256:…`, their `ARG STREAMLIT_VERSION` and
/// their self-asserting `RUN` steps as `base`/`layers` in Rust would be a second
/// copy of a recipe that is already written down. A synthesized cell cannot express
/// a `RUN` at all, so the copy would also be a WORSE one.
///
/// What this does NOT declare is the run: `xtask streamlit` starts the demo and
/// `xtask test-demo` runs its suite, and neither is a build. A build thing builds
/// artifacts; running one belongs in a pipeline.
fn the_containers() -> Vec<ContainerBuild> {
[
(PIP_TOOLCHAIN_IMAGE, "pip-toolchain.Containerfile"),
(PIP_CONSUMER_IMAGE, "pip-consumer.Containerfile"),
(STREAMLIT_IMAGE, "streamlit.Containerfile"),
]
.into_iter()
.map(|(name, file)| ContainerBuild {
name: name.to_string(),
containerfile: format!("{CONTAINERFILE_DIR}/{file}"),
..ContainerBuild::default()
})
.collect()
}
/// **The `[[build.variant]]` row of one bundle group.** The core group's row is
/// exactly the row `facet-wrapped-wasm` has always been (no features, defaults on),
/// so its argv — and so its bytes — do not move.
fn group_row(g: &BundleGroup) -> BuildVariant {
BuildVariant {
name: g.variant.to_string(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Wasm,
package: Some(g.package.to_string()),
// Mandatory for `kind = "wasm"` and it rides `cargo_args`.
target: Some("wasm32-unknown-unknown".to_string()),
profile: Profile::Release,
no_default_features: g.no_default_features,
features: g.features.iter().map(|f| (*f).to_string()).collect(),
..BuildVariant::default()
}
}
/// Rendering capabilities whose isolated and unified builds must stay supported.
pub const FLOW_FEATURES: &[&str] = &[
"host", "labels", "boxes", "arrows", "status", "rate", "layered",
"compact-state", "hit-test", "lod",
];
fn flow_rows() -> Vec<BuildVariant> {
let row = |name: String, features: Vec<String>, wasm: bool| BuildVariant {
name,
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Lib,
shape: nornir_build_thing::variant::ServiceShape::NotApplicable,
package: Some("facett-flow".to_string()),
target: wasm.then(|| "wasm32-unknown-unknown".to_string()),
profile: Profile::Release,
no_default_features: true,
features,
..BuildVariant::default()
};
let mut rows = vec![
row("facett-flow-plain".into(), vec![], false),
row("facett-flow-wgpu".into(), vec!["wgpu".into()], false),
];
for feature in FLOW_FEATURES {
rows.push(row(format!("facett-flow-{feature}"), vec![(*feature).into()], false));
rows.push(row(format!("facett-flow-{feature}-wgpu"), vec![(*feature).into(), "wgpu".into()], false));
}
let all: Vec<String> = FLOW_FEATURES.iter().copied().chain(["wgpu"]).map(str::to_string).collect();
rows.push(row("facett-flow-all".into(), all.clone(), false));
rows.push(row("facett-flow-all-wasm32".into(), all, true));
rows
}
impl BuildThing for FacettBuildThing {
fn manifest(&self) -> Manifest {
Manifest {
abi: ABI_VERSION,
repo: "facett".to_string(),
source_crate: KRATE.to_string(),
summary: "what facett PRODUCES, in three groups: the two PyPI releases, \
facett (the components wheel) and facett-demo (the Streamlit \
app wheel + sdist), built inside a pinned container with the \
network off, beside the SHA256SUMS a downloader checks them \
with and a provenance record over the produced bytes, plus the \
wasm bundles the facett wheel carries; the DEMO, \
native and as the gh-pages wasm bundle; and the three container \
IMAGES this repo masters. facett's seventy library crates are \
linked, not catalogued here; Falsett additionally declares \
isolated and unified feature verification builds."
.to_string(),
}
}
fn variants(&self) -> BuildVariantSet {
// The container ROWS are derived from the container CELLS, not typed out
// beside them: a roster entry and a cell that disagree about an image name
// is a variant the forge refuses as a dangling `container_ref`, and the only
// way two lists cannot disagree is for there to be one list.
let container_rows = the_containers().into_iter().map(|c| BuildVariant {
name: c.name.clone(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Container,
container_ref: Some(c.name.clone()),
..BuildVariant::default()
});
BuildVariantSet {
// ONE delivery, named rather than left implicit, so --suite ORIGINAL
// resolves here instead of falling through to UNSUITED.
base_suite: Some(SUITE.to_string()),
// THE BUNDLE GROUPS first — one wasm row per [`BUNDLE_GROUPS`] entry,
// core (`facet-wrapped-wasm`) leading and unchanged — then the rest.
variants: BUNDLE_GROUPS.iter().map(group_row).chain(vec![
BuildVariant {
name: VARIANT.to_string(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Package,
// The crate the wheel's wasm comes from. It is the package the
// release is ABOUT, and recording it means the receipt names the
// Rust that ended up in the browser.
package: Some("facet-wrapped".to_string()),
profile: Profile::Release,
..BuildVariant::default()
}, BuildVariant {
// THE DEMO WHEEL (PyPI `facett-demo`): pure Python, no cargo package
// behind it — the app that mounts the components wheel.
name: DEMO_PIP_VARIANT.to_string(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Package,
profile: Profile::Release,
..BuildVariant::default()
}, BuildVariant {
// THE DEMO, native. `cargo build -p facett-demo --release` and
// nothing else — no feature axis, because the native build is the
// default one and a variant that re-stated the defaults would be a
// second writer of them.
name: DEMO_VARIANT.to_string(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Exe,
package: Some(DEMO_PACKAGE.to_string()),
profile: Profile::Release,
..BuildVariant::default()
}, BuildVariant {
// THE DEMO, in a browser. Same crate, different artifact, and the
// feature axis is the difference: `--no-default-features` drops the
// native-only passthroughs that do not build for wasm32, and the two
// GPU lanes put the WebGPU backend and the real 3D OSM render back.
name: DEMO_WASM_VARIANT.to_string(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Wasm,
package: Some(DEMO_PACKAGE.to_string()),
// The crate's DEFAULT bin, named: cargo writes a bin's module under
// its name as written (`facett-demo.wasm`) and wasm-bindgen keeps that
// stem (`facett-demo.js`, the names the live page has always served).
// Unnamed, the stage looked for the cdylib spelling `facett_demo.wasm`
// (MEASURED on dwarf 2026-09-29) — and `--bin` also stops the build
// compiling the five native-only demo bins for wasm32.
bin: Some(DEMO_PACKAGE.to_string()),
target: Some("wasm32-unknown-unknown".to_string()),
profile: Profile::Release,
no_default_features: true,
features: DEMO_WASM_FEATURES.iter().map(|f| (*f).to_string()).collect(),
..BuildVariant::default()
}, BuildVariant {
// THE DEMO HOST'S FIXTURE — the `panel` EXAMPLE of `facett-wasm-demo`,
// the smallest complete bundle on the harness's web path, which the
// WebGPU render guard (`facett-wasm-demo/tests/verify_webgpu_render.py`)
// drives. It is a cargo example, and until edda's `BuildVariant` grew
// `example` (2026-09-29) it could not be a row, so `xtask panel-wasm`
// spelled its own argv. Not a shipped artifact: a guard's input.
name: PANEL_WASM_VARIANT.to_string(),
suites: vec![SUITE.to_string()],
kind: ArtifactKind::Wasm,
package: Some(HARNESS_PACKAGE.to_string()),
example: Some(PANEL_EXAMPLE.to_string()),
target: Some("wasm32-unknown-unknown".to_string()),
profile: Profile::Release,
..BuildVariant::default()
}])
.chain(container_rows)
.chain(flow_rows())
.collect(),
wasms: BUNDLE_GROUPS
.iter()
.map(the_wasm_bundle)
.chain(std::iter::once(the_demo_wasm_bundle()))
.chain(std::iter::once(the_panel_wasm_bundle()))
.collect(),
packages: vec![the_package(), the_demo_package()],
containers: the_containers(),
// **The pinned CARGO image.** It used to say "no `toolchain` (the
// package cell names its own pinned image)", and that sentence was the
// defect: the package cell's image is where PYTHON runs, and naming it
// said nothing about where cargo runs. Undeclared, the forge's code
// road resolved `CargoToolchain::default()`, refused it for carrying no
// digest, and fell back to parsing the toml this code EMITS — the
// description driving the build it was written to describe.
toolchain: Some(the_cargo_toolchain()),
allow_unbuilt_bins: allow_unbuilt_bins(),
// Not declared by facett, and each absence is a fact rather than a gap:
// no `offers`/`required`/`exclusive` (neither row has a feature axis), no
// containers/vms/isos/installers/kernels (facett masters no media).
..BuildVariantSet::default()
}
}
}
/// **The `[[bin]]` targets no row builds, each with its reason** —
/// `[build.allow_unbuilt_bins]`. ⚒ Build's gate (`buildgate::tally_bins`) counts every
/// bin cargo can see in this tree; MEASURED 2026-10-16 on loki these nine had neither
/// a row nor a reason (`tests/build_gate.rs` now asks the same question).
fn allow_unbuilt_bins() -> std::collections::BTreeMap<String, AllowEntry> {
[
("facett-build-thing", "this recipe crate's own emit door (`cargo run -p facett-build-thing -- emit`); the forge loads the crate as wasm, the bin is repository tooling, never shipped."),
("xtask", "the `cargo xtask` thin door onto this recipe — repository tooling, never shipped."),
("facett-ops", "`cargo ops <cmd>`: the verbs that are NOT builds (publish a wheel, pack holger's index, run the Streamlit demo, push the Pages site) — repository tooling, never shipped."),
("devid_release_probe", "facett-app's RELEASE-profile probe for the DEV-ID chip, behind `required-features = [\"devid-probe\"]`; built and run by facett-app/tests/devid_release_profile.rs, never by hand — a test subject, not an artifact."),
("facett_robot", "facett-demo's robot-UI driver (`required-features = [\"robotui\"]`): the headless/watch replayer the demo's UI tests drive; the native demo row builds the default features and so never this bin."),
("errcode_registry_json", "a generator: exports the compiled facett_core::errcode::REGISTRY as JSON for the Python/Streamlit demo; run by hand to regenerate, never shipped."),
("errcode_registry_md", "a generator: regenerates the error-code registry markdown from the compiled REGISTRY; run by hand and redirected, never shipped."),
("gen_skull", "a one-off asset baker (`cargo run -p facett-model3d --bin gen_skull`): the committed assets/skull.glb is its output; re-run only to regenerate."),
("facett-flow-bench", "dev/flow-bench — a development-only timing harness in its own [workspace]; no runtime crate depends on it and nothing ships it."),
]
.into_iter()
.map(|(k, v)| (k.to_string(), AllowEntry::Reason(v.to_string())))
.collect()
}
/// The header every emission starts with — says what the file is, and what it is not.
pub const EMITTED_HEADER: &str = "\
# facett — .nornir/build-variants.toml — EMITTED, NOT SOURCE.
#
# Generated by `cargo run -p facett-build-thing -- emit` from facett-build-thing
# (the Rust `impl BuildThing`). The code is the one source; this file DESCRIBES the
# build for readers and for the doors that discover a repo by this path — the
# dwarves forge reads it with its own pin. Editing it changes NOTHING: re-emit after
# changing the code. Do not edit by hand.
#
# Release and verification rows, in five groups. facett is a library
# constellation: its seventy crates are LINKED by consumers, not built as variants,
# and declaring them would be a build matrix invented to look complete. What is
# declared is what facett PRODUCES, plus Falsett's focused feature checks.
#
# ① THE RELEASES — the two PyPI packages (2026-10-02, 0.2.0): `facett-wheel` (PyPI
# `facett`: the components, the loader, Streamlit glue behind the [streamlit] extra;
# a wheel and NO sdist, because its payload is prebuilt wasm a source tarball could
# not rebuild) and `facett-demo-wheel` (PyPI `facett-demo`: the pure-Python Streamlit
# app, wheel + sdist). Each lands beside the SHA256SUMS a downloader runs
# `sha256sum -c` against and a provenance record over the produced bytes — plus the
# wasm BUNDLES the facett wheel carries, one row per BUNDLE GROUP (core = the default
# bundle; docs and model3d since 2026-09-29, sim since 2026-10-02), each a row of its
# own because cargo does not produce a bundle. The loader routes by `bundles.json`,
# emitted beside this file from the same code (pkg-facet-wrapped/src/facett/assets/).
#
# ② THE DEMO (added 2026-09-18) — `facett-demo` as a native binary and as the
# gh-pages wasm bundle. These are not libraries anybody links: they are a program a
# human runs and a page a browser loads, and until this pass they were reachable
# only by typing `cargo xtask demo-wasm` from memory — a produced artifact with no
# declared door, which is the same gap Road B closed for `wrap-wasm`. The wasm row
# carries the feature axis (`--no-default-features` + the two GPU lanes) that the
# xtask literal used to be the only writer of.
#
# ③ THE IMAGES (added 2026-09-18) — the three Containerfiles in
# `facett-ops/src/containerfiles/`, declared in FILE MODE because the bytes on disk are
# the build input: their digest-pinned `FROM`, their `ARG`s and their self-asserting
# `RUN` steps are the recipe, and a synthesized cell cannot express a `RUN` at all.
# The header used to say facett mastered no media; that was true of vms/isos and
# false of images, and this repo is the only writer of all three.
#
# ④ THE FIXTURE (added 2026-09-29) — `facett-wasm-demo-panel`, the demo HOST's
# `panel` EXAMPLE built to wasm + bindgen: the bundle the WebGPU render guard drives.
# A guard's input, not a shipped artifact; a row because `example` is now a field,
# so `xtask panel-wasm` no longer spells its own cargo argv.
#
# ⑤ FALSETT VERIFICATION — default, GPU, each optional capability alone and with
# GPU, and all capabilities together on native and wasm32. These rows verify
# feature unification used by pipeline, plandag and brokkr embedders.
#
# What is deliberately NOT here: RUNNING any of it. `xtask streamlit` starts the
# demo and `xtask test-demo` runs its suite; neither is a build, and a build thing
# that declared a `podman run` would be a pipeline wearing the wrong hat. `cargo build -p facet-wrapped --target
# wasm32-unknown-unknown` writes ONE file, facet_wrapped.wasm; the wheel stages
# facet_wrapped_bg.wasm (a different, wasm-bindgen-rewritten file), a JS glue module
# and a component manifest. `[[build.wasm]]` declares the typed stages that make
# those, and `from_wasm` on the package chains the two so the forge builds them in
# order — an edge that is CHECKED against what the bundle says it writes, not merely
# recorded. The reasoning is in the crate's own module docs; an emission carries
# data, not argument, and a comment written here is deleted by the next `emit`
# without anybody noticing.
#
# The LAST table is `[build.toolchain]`, and it is what makes every cargo row above
# reachable from the code at all. It pins `nordisk-cargo-toolchain` by MANIFEST
# digest — the image every cargo build of this repo runs in, the forge's build of
# `facett-build-thing` to wasm32 included. Undeclared (before 2026-09-06) the forge
# resolved the default, refused it for being an unpinned tag, and fell back to
# PARSING this file: the emitted description driving the build instead of describing
# it. The digest is what the committed edda recipe mints (the same one edda and holger pin) — one reproducible recipe, one
# image — and a box that has never minted it mints it with
# `cargo run -p nornir-build-thing --features cargo-container --example
# mint_cargo_toolchain`; there is no registry to pull it from.";
/// The emission — what `.nornir/build-variants.toml` must contain.
pub fn emitted_toml() -> Result<String> {
emit::emitted_toml(EMITTED_HEADER, &FacettBuildThing)
}
/// Regenerate `<repo_root>/.nornir/build-variants.toml` from the code.
pub fn write_emitted(repo_root: &Path) -> Result<PathBuf> {
emit::write_emitted(EMITTED_HEADER, &FacettBuildThing, repo_root)
}
/// facett's repo root, from this crate's manifest directory.
#[must_use]
pub fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("facett-build-thing has a parent")
.to_path_buf()
}
/// Regenerate the loader's routing table (`<repo_root>/`[`ROUTING_PATH`]) from
/// [`BUNDLE_GROUPS`]. Output only — like the toml, it DESCRIBES the build and is
/// never read back as authority; `tests/thin_door.rs` holds the committed bytes to
/// [`routing_json`].
pub fn write_routing(repo_root: &Path) -> Result<PathBuf> {
let p = repo_root.join(ROUTING_PATH);
std::fs::write(&p, routing_json())
.map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?;
Ok(p)
}
/// **The stage runner the bundle groups need** — edda's `XtaskWasmStageRunner`,
/// plus the one thing it cannot do: capture a group's component manifest from the
/// registry built with THAT GROUP'S features.
///
/// edda's `WasmStageRun::ComponentManifest` carries package, example and out file,
/// and its runner spawns `cargo run --example <e> -p <pkg>` with default features.
/// For `core` that is exactly right, so core is delegated untouched (its manifest
/// bytes do not move). For a feature-gated group it would capture the CORE registry
/// into the docs bundle's `manifest.json` — a loader would then route every core
/// entry to the docs bundle and find none of the docs ones. So the feature axis is
/// read off the ROW (never re-spelled) and the example runs with it.
///
/// The live leg of this crate, behind `wasm-stage` like edda's: the forge compiles
/// this crate to wasm32 as a declaration, and a declaration runs no tools.
#[cfg(feature = "wasm-stage")]
pub mod stages {
use std::path::Path;
use anyhow::{anyhow, Result};
use nornir_build_thing::variant::BuildVariant;
use nornir_build_thing::wasm::{WasmStageRun, WasmStageRunner, XtaskWasmStageRunner};
/// A runner bound to the row whose stages it runs.
pub struct GroupStageRunner<'a> {
/// The group's `[[build.variant]]` row — the feature axis comes from here.
pub variant: &'a BuildVariant,
}
impl GroupStageRunner<'_> {
/// The feature arguments of the row, for the manifest example — the same
/// two fields `cargo_args()` renders, in the same spelling.
#[must_use]
pub fn feature_args(&self) -> Vec<String> {
let mut a = Vec::new();
if self.variant.no_default_features {
a.push("--no-default-features".to_string());
}
if !self.variant.features.is_empty() {
a.push("--features".to_string());
a.push(self.variant.features.join(","));
}
a
}
}
impl WasmStageRunner for GroupStageRunner<'_> {
fn describe(&self) -> String {
format!("{} + the manifest example with row '{}''s features", XtaskWasmStageRunner.describe(), self.variant.name)
}
fn run(&self, run: &WasmStageRun, repo_root: &Path) -> Result<()> {
let feats = self.feature_args();
let WasmStageRun::ComponentManifest { package, example, out_file } = run else {
return XtaskWasmStageRunner.run(run, repo_root);
};
if feats.is_empty() {
return XtaskWasmStageRunner.run(run, repo_root);
}
let out = std::process::Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
.current_dir(repo_root)
.args(["run", "--quiet", "--example", example, "-p", package])
.args(&feats)
.output()
.map_err(|e| anyhow!("spawn the `{example}` example of `{package}` ({}): {e}", feats.join(" ")))?;
if !out.status.success() {
let tail: Vec<String> = String::from_utf8_lossy(&out.stderr).lines().rev().take(20).map(str::to_string).collect();
return Err(anyhow!(
"the `{example}` example of `{package}` with {} exited {} — no manifest to capture for row '{}'\n{}",
feats.join(" "),
out.status,
self.variant.name,
tail.into_iter().rev().collect::<Vec<_>>().join("\n")
));
}
if out.stdout.is_empty() {
return Err(anyhow!(
"the `{example}` example of `{package}` with {} exited 0 and printed NOTHING — an empty \
manifest would route no entry to row '{}'",
feats.join(" "),
self.variant.name
));
}
std::fs::write(out_file, &out.stdout).map_err(|e| anyhow!("write {}: {e}", out_file.display()))
}
}
}