Build Thing — guide
Class:
guide· audience: a techie drivingdwarves buildthingor the ⚒ tab over a real checkout. This is thef0file for the build thing itself — what it is, its vocabulary, and the two roads onto it. The design source of truth is the module doc of edda'snornir-build-thing/src/plugin.rs— condensed here, never contradicted. Siblings: holger-build-thing-design.md (the holger build target), buildthing-installer.md (the per-site installer door).
What a build thing IS
A build thing is FUNCTIONALITY, not a table (plugin.rs, verbatim). Two halves:
- The declared build space —
<repo>/.nornir/build-variants.toml: the rows a repo can be built as. Data, on disk, diffable. - The
BuildThingimpl — code, in a crate the app depends on (gunnar-build-thing,holger-build-space, …). The app compiles it in, so the app describes and validates itself; a missing build thing is a compile error, not a file nobody noticed was absent. Validity logic (an INVALID chip, a refused option) is code and does not serialize — that is why the JSON-export idea was rejected.
For an artifact pulled from a registry, the same crate compiles to wasm32-unknown-unknown — one source, two link targets — and the artifact carries that wasm module in the .nordisk.buildthing linker section plus a receipt. The forge reads the section with an object parser and calls the build thing in a capability-less sandbox. The forge never executes the artifact: an aarch64/musl artifact cannot run on an x86 forge anyway, and running one is remote code execution on the box holding the deploy keys. If a pulled artifact must genuinely RUN, that is modgunn's OCI or tunnr's KVM, never bare on the forge host.
Vocabulary
| word | is | notes |
| variant | one named [[build.variant]] row — a point in the repo's build space |
the unit --variant names; --atom/--cell are retired aliases, kept forever |
| kind | the output type: exe · lib · wasm · service · container · vm · iso · installer · kernel |
cargo builds exe/lib/wasm/service; every other kind delegates to its own same-named table ([[build.iso]], [[build.kernel]], …) bound by name, exactly like iso |
| features | ONE axis of a variant — the cargo feature set | dwarves-ui vs dwarves-ui-gpu differ ONLY here |
| role | server · client · solo · library · tool |
who the artifact is for |
| linkage | fat (static/musl-shaped) vs dynamic |
|
| suites | the deliveries (utgåvor) a variant belongs to (MVP, ORIGINAL) |
--suite is required once a product declares more than one |
| profile | debug · release (± more) |
cargo profile per row |
A bare delegate table ([[build.kernel]] with no variant row) is INERT — it validates and renders but is never dispatched. The [[build.variant]] row of the same name is what makes it a build point.
Kind → pane (PaneKind::for_artifact, edda contract.rs — data, no product ifs; a recipe may override):
exe -> Exec argv + stdout, run in a container
container · service -> Log start/stop + log + ports
iso · vm · installer -> Console serial AND graphical, both shown, never a toggle
lib · wasm · kernel -> None nothing to run — kernel gets the configurator LEAF instead
Options and params
[[build.installer.options]] rows (OptionSpec, edda options.rs):
- an option is opt-out of a compiled capability:
offmeans the cargofeaturebehind it is SUBTRACTED from the appliance binary — the code is not compiled in, not present-but-declining. Not a runtime flag. params_when_on/params_when_offgate which params are accepted: WireGuard off ⇒--param hub_confis refused by name rather than staged onto a medium whose binary cannot read it; DHCP off ⇒ the static address becomes required.bakes(OptionBake) records what the choice bakes onto the medium.
--param values land on declared ParamSpec rows (edda params.rs): name=value, name=@path (file contents), name=@fixture (the forge mints one). Landing is either a kernel-cmdline boot_param token or a staged_file in the staging tree. A name that reads as a secret (private · secret · passw · token · psk · preshared) is refused by name — cmdline, declaration and build record are all readable. Unknown names, wrong shapes: refused by name, never ignored.
Kernel as options (2026-08-26)
Every [[build.kernel]] repo's kconfig symbols are declared option rows — the same vocabulary as installer options, emitted by edda kernel_features.rs::option_rows:
- a preset is not a separate mechanism: it is the named default-bundle the rows start from (
full·normal·slim·nuked);default=onliterally means "enabled in the chosen preset". requiresis the Kconfig NAME hierarchy:PparentsCwhenPis the longest symbol present that prefixesC_…— derived, never hand-listed.- contract locks: a symbol of the measured boot contract is
LOCKED, and dropping it is refused naming what breaks. - the operator's decision (
KernelDecision) renders one veto fragment — every drop spelled# CONFIG_X is not set, cascade included, so the file states the whole consequence of the clicks. The same decision through a pane click and a CLI flag is the same bytes (edda 37b5b5f). - the veto fragment rides
merged_fragmentLAST (preset → profile → the repo's committed fragment → the operator — later wins), and a fragment that vetoes anything moves the content id; a comment-only fragment must not (changes_anything).
Road 1 — the ⚒ tab
The scene (dwarves-gui/src/build_scene.rs) knows no product by name:
1 type an ARTIFACT NAME the code resolves the recipe itself
2 the home matrix appears every row that recipe declares
3 pick ONE row one at a time
4 pick ONE column the delivery targets that row declares
…the feature/package axis: click a package away, see which feature goes dark
…the declared options/params render as settings; invalid input disarms the door
5 ▶ BYGG a pane appears BELOW, per the kind's PaneKind
The kernel leaf is recipe-gated (Forge::browse), three honest states: nothing resolved → the BROWSE_NONE_HINT ("pick a recipe" — claims nothing about the forge); a recipe with no [[build.kernel]] → Leaf::Refused naming the recipe; a recipe that declares one → the configurator itself, answering "what is in a slim kernel" with nothing selected at all.
Road 2 — the CLI
dwarves buildthing <product> [--suite S] [--variant V] [--list] [--list-options]
[--param K=V]… [--opt name=on|off]…
[--opt kernel.preset=P] [--opt kernel.<symbol>=off]…
dwarves buildthing <product> --installer [--variant CELL] \
--kernel <bzImage> --app <app.znippy> [--param K=V]…
<product> is a member name (resolved $NORDISK_ROOT/<p> → ../<p> → <p>; the chosen checkout and the rule that chose it are printed), a path, or a build-thing coordinate (gunnar-build-thing:0.1.0) — the receipt's pointer typed by hand for the before-first-build case. The verb contributes NO variant name, NO feature string, NO bin name: it walks the ONE resolver every member's cargo xtask build uses. The installer door takes its kernel donor explicitly: --kernel <bzImage> (or $KORP_TEST_KERNEL) plus the packed --app, composed by korp_installer::isocompose::ForgeComposer under the appliance-iso feature.
Measured on tunnr, 2026-08-26:
$ dwarves buildthing tunnr --list-options # 49 declared rows, builds nothing
── tunnr-kernel · kind kernel
opt kernel.preset bundles: full · normal · slim · nuked default=nuked
opt kernel.efi CONFIG_EFI default=on LOCKED(breaks CONFIG_EFI_STUB)
opt kernel.unix CONFIG_UNIX default=on
opt kernel.virtio CONFIG_VIRTIO default=on LOCKED(breaks CONFIG_VIRTIO, CONFIG_VIRTIO_PCI, CONFIG_VIRTIO_NET, CONFIG_VIRTIO_BLK)
opt kernel.wireguard CONFIG_WIREGUARD default=off # …44 more rows
$ dwarves buildthing tunnr --variant tunnr-kernel --opt kernel.preset=slim --list
kernel decision: preset=slim profile=vanilla clicked_off=[] (changes nothing)
tunnr-kernel server kernel (not a cargo build — built by the kernel builder)
$ dwarves buildthing tunnr --variant tunnr-kernel --opt kernel.virtio=off
--opt kernel.virtio=off REFUSED — dropping CONFIG_VIRTIO breaks the measured
boot contract: CONFIG_VIRTIO, CONFIG_VIRTIO_PCI, CONFIG_VIRTIO_NET, CONFIG_VIRTIO_BLK
$ dwarves buildthing tunnr --variant tunnr-kernel --opt kernel.unix=off # a REAL build
That last build ran to completion in the bounded toolchain container: .kernel-out/tunnr-kernel/kernel.config carries # CONFIG_UNIX is not set (line 729), the bzImage came out 1717248 bytes against the untouched preset's measured 1725440, and the pin (tunnr-kernel.kernel-pin.json) seals the moved decision — fragment_sha256 d437b919…, bzimage_sha256 d4ed666b…. The veto reached the build's own merge and the content id moved with it. One vocabulary throughout: what --list-options prints is exactly what --opt/--param accept, read off the same declarations, so the listing and the refusals cannot disagree.
Provenance — <artifact>.intoto.jsonl
With DWARVES_PROVENANCE_KEY set, every green cargo build gets a DSSE envelope over an in-toto Statement v1 / SLSA Provenance v1 beside each artifact (crates/dwarves/src/provenance_step.rs, feature sign-step): subject = file name + sha256, buildType = edda BUILD_TYPE_VARIANT_V1, resolvedDependencies = every source tree the container mounted (git+https://git.gunnar.rs/<org>/<repo> + gitCommit) and the toolchain image digest, builder.id = https://git.gunnar.rs/nordisk/dwarves/buildthing@v1. The seal is verified against its root before the build turns green.
DWARVES_PROVENANCE_KEY=dev— the PUBLISHED development key (derived from korp's publishedDEVELOPMENT_SEED; keyid428a6180…7616, the build prints its Holgerkeys:row). Proves the path, vouches for nothing.DWARVES_PROVENANCE_KEY=<dir>—provenance-signing.pk8(0600) +provenance-signing.der+korp-root-ca.der: a real key, never inside a container.
Verify: cosign verify-blob-attestation --key <pub.pem> --signature <a>.intoto.jsonl --type slsaprovenance1 --insecure-ignore-tlog=true <a>, or holger-provenance's verify_slsa example (allow-list the builder id above).
Guards a contributor must know
- The scene names no product.
dwarves-gui/tests/robot_build_scene.rs(the_scene_source_names_no_product) tokenises the scene's own source and scans it for 24 product names — zero may appear. The only kind dispatch isPaneKind::for_artifact; a new kind gets a pane by data, never by anifover a name. - Mirrors change together. Where a repo keeps
build-variants.tomlon disk beside a compiled build thing (gunnar:CELLS),tests/toml_parity.rsholds the two to saying the same thing — the file is a mirror, not a second source of truth. - One writer per rule (LAW 5). Resolution, refusal wording, the kernel feature model, the option vocabulary — each lives ONCE in edda
nornir-build-thing; the CLI, the pane and every product xtask call it. A brought-over join is a twin; do not.