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

nordisk/dwarves

.nornir/buildthing-guide.md

Build Thing — guide

Class: guide · audience: a techie driving dwarves buildthing or the ⚒ tab over a real checkout. This is the f0 file 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's nornir-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:

  1. The declared build space — <repo>/.nornir/build-variants.toml: the rows a repo can be built as. Data, on disk, diffable.
  2. The BuildThing impl — 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):

--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:

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.

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