xtask/src/main.rs
//! dwarves xtask — build automation for the `dwarves-gui` live web demo.
//!
//! Pure Rust, zero shell (Brokkr's Software Criteria + the zero-shell charter): the
//! gh-pages FLOW itself — compile the demo crate to release wasm, `wasm-bindgen` it
//! into `dist/`, write the cache-busted static `index.html` + Codeberg-Pages meta,
//! then force-push `dist/` to an orphan `pages` branch — is the near-byte-identical
//! block that four repos (nornir/holger/dwarves/facett) grew, so it now lives ONCE in
//! the `nornir-pages` edda leaf (LAW #5, DRY). This crate keeps only the dwarves knobs
//! (the [`PagesConfig`]) and the thin verb dispatcher; the ~457-LOC `demo.rs` copy is
//! gone. The `dwarves` CLI is still never touched.
//!
//! - `cargo xtask build --suite ORIGINAL [--variant <v>] [--list]` — **THE build verb.**
//! (`--atom` is the deprecated spelling of `--variant`; it still works.)
//! Its entire body is [`nornir_build_thing::suite_cli::run`]: this file names no
//! variant, no feature string and no bin, so what gets built comes from
//! `.nornir/build-variants.toml` and the declaration cannot drift from the build.
//! dwarves ships ONE delivery (`ORIGINAL`), the same shape korp, nornir, modgunn
//! and holger take.
//! - `cargo xtask demo-wasm` — build the declared `dwarves-gui-wasm` row IN THE PINNED
//! IMAGE (edda `nornir_pages::demo_wasm` → `nornir_build_thing::container_road`:
//! cargo + its wasm-bindgen stage, never host cargo or host wasm-bindgen) into the
//! row's dist, plus the static page. ⚠ dwarves declares NO such row today (the set is
//! the six-row matrix), so the verb is REFUSED BY NAME until the owner decides how the
//! Pages demo row enters the declaration — there is no host fallback.
//! - `cargo xtask deploy-pages` — run `demo-wasm`, then force-push `dist/` to the
//! `pages` branch → <https://nordisk.codeberg.page/dwarves>.
//! - `cargo xtask-docs docs` — render dwarves' real `.nornir/*.md` → the typeset
//! SVG book the Manual pane embeds (behind the `docs` feature; see [`mod@docs`]).
/// **`build-history`** — seal / verify / regenerate the `znippy-build-thing`
/// package: step 3 of the release order, after the build and before the release.
mod build_history;
mod ci;
// The diagram generator lives in the LIB half of this crate so its rendering can
// be tested (`xtask/tests/build_flow_svg.rs`); a bin's modules cannot be reached
// from a test, and the one thing this diagram must prove is that its two
// verdicts do not render alike.
use xtask::build_flow_svg;
#[cfg(feature = "docs")]
mod docs;
use nornir_pages::PagesConfig;
/// The declared row the Pages demo is built from (`dwarves-gui`, wasm32, with its
/// `[[build.wasm]]` bindgen cell). What the row builds is the declaration's.
const DEMO_VARIANT: &str = "dwarves-gui-wasm";
/// dwarves' declaration, validated — the same load `cargo xtask build` does.
fn declared(root: &std::path::Path) -> Result<nornir_build_thing::BuildVariantSet, String> {
nornir_build_thing::BuildVariantSet::discover_validated(root)
.map_err(|e| format!("{e:#}"))?
.ok_or_else(|| format!("{} declares no build-variant set", root.display()))
}
/// The dwarves-specific gh-pages config — the 100% of divergence from the shared flow.
/// Every knob is exactly the value the old inline `demo.rs` hard-coded.
fn cfg() -> PagesConfig {
PagesConfig {
// The cargo crate that owns the wasm binary.
demo_crate: "dwarves-gui",
// The eframe canvas id the wasm entry (`dwarves-gui/src/main.rs::start` →
// `WebDemo::new`) and the `index.html` loader agree on.
canvas_id: "demo_canvas",
// The exported JS state-hook name (`dwarves-gui/src/main.rs::dwarves_state`),
// lifted onto `window` for a headless drive / the browser console.
state_export: "__dwarves_state",
// The `<title>` (its first word is also the loading-spinner label → "dwarves").
title: "dwarves — live web demo",
description: "dwarves: the CI/CD forge view (pipeline roster + run listing), in your browser — the live in-browser demo (facett-rendered, sample fixture, interim).",
// The live URL for the deploy banner.
pages_url: "https://nordisk.codeberg.page/dwarves",
// The host pinned in `.domains`.
pages_host: "nordisk.codeberg.page",
// The branch Codeberg Pages serves from.
pages_branch: "pages",
// The env var naming the push remote (unset → `origin`, which is
// `ssh://git@git.gunnar.rs:2222/nordisk/dwarves.git`).
remote_env: "DWARVES_PAGES_REMOTE",
// The human label in the squashed deploy commit message.
commit_label: "dwarves live web demo",
// Read by nothing since edda's host road left (2026-09-30): the row's
// features are the declaration's.
extra_build_args: &[],
// The workspace root = the parent of the `xtask` crate dir. The leaf can't read
// the caller's compile-time manifest dir, so we compute it here (same value the
// old inline `workspace_root()` produced).
workspace_root: std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask dir has a parent (the workspace root)")
.to_path_buf(),
}
}
/// Regenerate the **embedded** korp-installer UEFI stub: compile the
/// `korp-installer-efistub` fixture for `x86_64-unknown-uefi` ONCE (a dev-box step —
/// needs the target here, NOT at dwarves run/boot time) and copy the produced
/// `bootx64.efi` into `dwarves-gui/src/embedded/korp-installer-bootx64.efi`, which
/// `boot_run.rs` embeds via `include_bytes!`. Run after changing the efistub source so
/// the committed blob (and thus every mastered installer ISO) stays current. This is the
/// self-contained charter: the toolchain is needed HERE (once), never on the box that
/// masters or boots the ISO.
fn gen_installer_stub(workspace_root: &std::path::Path) -> Result<std::path::PathBuf, String> {
gen_efistub(
workspace_root,
"korp-installer-efistub",
"korp-installer-bootx64.efi",
)
}
/// Regenerate the **embedded** `korp-start` second-boot stub: compile the
/// `korp-start-efistub` fixture for `x86_64-unknown-uefi` ONCE and copy its `bootx64.efi`
/// into `dwarves-gui/src/embedded/korp-start-bootx64.efi`. The dwarves reboot-into-korp
/// flow lays THESE bytes on the installed disk's FAT ESP (`EFI/BOOT/BOOTX64.EFI`) so the
/// second boot needs zero host toolchain — same self-contained charter as the installer
/// stub. Run after changing the `korp-start-efistub` source.
fn gen_start_stub(workspace_root: &std::path::Path) -> Result<std::path::PathBuf, String> {
gen_efistub(
workspace_root,
"korp-start-efistub",
"korp-start-bootx64.efi",
)
}
/// Regenerate the **embedded** `korp-live` run-from-RAM stub (the live-ISO deployment):
/// compile the `korp-live-efistub` fixture for `x86_64-unknown-uefi` and copy its
/// `bootx64.efi` into `dwarves-gui/src/embedded/korp-live-bootx64.efi`. The live-boot flow
/// lays THESE bytes on a bootable ISO's ESP so the ISO runs korp straight from RAM (no
/// install) — same self-contained charter as the other stubs.
fn gen_live_stub(workspace_root: &std::path::Path) -> Result<std::path::PathBuf, String> {
gen_efistub(workspace_root, "korp-live-efistub", "korp-live-bootx64.efi")
}
/// Regenerate the **embedded PRODUCTION first-stage loader** —
/// `dwarves-gui/src/embedded/korp-boot-stage1.efi`, the real `EFI/BOOT/BOOTX64.EFI`.
///
/// This is deliberately NOT [`gen_efistub`]: that helper only knows how to reach
/// `korp-installer/tests/fixtures/{name}`, and the production loader is *not* a test
/// fixture — it lives at `korp-installer/boot/korp-boot-efi`. Rather than teach the
/// fixture helper a second path shape, this verb rides the crate's own host-side seam
/// [`korp_installer::firststage::build_first_stage`], which carries the FAT
/// `rustup target add x86_64-unknown-uefi` preflight error (never a raw `E0463`
/// "can't find crate for `core`") and the "is it really a PE?" check.
///
/// The loader is what makes the second boot a REAL chainload: it finds its own ESP via
/// `EFI_LOADED_IMAGE_PROTOCOL` (no NVRAM `Boot####`), reads `\korp\cmdline`, `LoadImage`s
/// `\vmlinuz` on a device path rooted at that ESP, installs the cmdline as UCS-2
/// `LoadOptions` and `StartImage`s — printing `KORP-BOOT: HANDOFF -> kernel` immediately
/// before it hands over. `boot_run.rs` embeds these bytes via `include_bytes!` and lays
/// them on the installed disk's ESP, so the appliance needs zero host toolchain.
fn gen_boot_stage1(workspace_root: &std::path::Path) -> Result<std::path::PathBuf, String> {
// Keep the UEFI artefacts out of the loader's own source tree AND off any tmpfs
// (a shared /tmp filling up breaks every concurrent build) — dwarves' own target/.
let scratch = workspace_root.join("target/korp-boot-stage1");
std::fs::create_dir_all(&scratch).map_err(|e| format!("create {}: {e}", scratch.display()))?;
let built =
korp_installer::firststage::build_first_stage(&scratch).map_err(|e| format!("{e:#}"))?;
let bytes = std::fs::read(&built).map_err(|e| format!("read {}: {e}", built.display()))?;
if !korp_installer::firststage::is_uefi_pe(&bytes) {
return Err(format!(
"{} is not a UEFI PE — the firmware would refuse it silently",
built.display()
));
}
let dst = workspace_root.join("dwarves-gui/src/embedded/korp-boot-stage1.efi");
std::fs::create_dir_all(dst.parent().unwrap())
.map_err(|e| format!("create embedded dir: {e}"))?;
std::fs::write(&dst, &bytes).map_err(|e| format!("write {}: {e}", dst.display()))?;
Ok(dst)
}
/// Compile a sibling `tunnr/korp-installer` (edda before 2026-09-26) UEFI efistub fixture for
/// `x86_64-unknown-uefi` and copy its `bootx64.efi` into the dwarves embedded dir under
/// `dst_name` — the one seam both `gen-installer-stub` and `gen-start-stub` ride. The Rust
/// COMPILER is needed HERE (once, dev-box), never on the box that masters or boots.
fn gen_efistub(
workspace_root: &std::path::Path,
fixture_dir: &str,
dst_name: &str,
) -> Result<std::path::PathBuf, String> {
let fixture = workspace_root.join(format!(
"../tunnr/crates/korp-installer/tests/fixtures/{fixture_dir}/Cargo.toml"
));
if !fixture.is_file() {
return Err(format!(
"efistub fixture not found at {} — need the sibling tunnr checkout",
fixture.display()
));
}
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let status = std::process::Command::new(&cargo)
.args([
"build",
"--release",
"--target",
"x86_64-unknown-uefi",
"--manifest-path",
])
.arg(&fixture)
.status()
.map_err(|e| format!("spawn cargo for the efistub: {e}"))?;
if !status.success() {
return Err(
"compiling the efistub failed — run `rustup target add x86_64-unknown-uefi` on \
THIS dev box (the toolchain is only needed here, once)"
.to_string(),
);
}
let built = fixture
.parent()
.unwrap()
.join("target/x86_64-unknown-uefi/release/bootx64.efi");
let dst = workspace_root
.join("dwarves-gui/src/embedded")
.join(dst_name);
std::fs::create_dir_all(dst.parent().unwrap())
.map_err(|e| format!("create embedded dir: {e}"))?;
std::fs::copy(&built, &dst)
.map_err(|e| format!("copy {} → {}: {e}", built.display(), dst.display()))?;
Ok(dst)
}
/// `cargo xtask build …` — resolve `(suite, variant)` against the declaration and build.
///
/// # Why this links `nornir-build-thing` and does NOT shell `dwarves buildthing`
///
/// `dwarves buildthing <product>` is the FORGE front-end onto the very same
/// [`nornir_build_thing::suite_cli::run`], so there is exactly ONE implementation of
/// the build logic (LAW 5) with two thin doors onto it. Which door belongs here is
/// decided by a fact special to this repo: **`dwarves` is the product being built.**
/// An xtask that shelled the `dwarves` binary would need dwarves already built and
/// installed in order to build dwarves — a bootstrap knot with no base case, and one
/// that would make a fresh clone unbuildable. Linking the dep-light leaf has no such
/// problem and no `dwarves` process anywhere in the loop.
///
/// The leaf is also the only legal choice constellation-wide: `nornir` may not gain a
/// crate edge on the forge (`nornir/tests/no_dwarves_edge.rs`), so if the xtasks were
/// to LINK anything it could not be dwarves. Keeping all five repos on the same door
/// is what makes `cargo xtask build --suite ORIGINAL` mean one thing everywhere.
fn run_build(args: &[String]) -> i32 {
let root = cfg().workspace_root;
let parsed = match nornir_build_thing::suite_cli::parse(args) {
Ok(p) => p,
Err(e) => {
eprintln!("dwarves xtask build: {e:#}");
return 2;
}
};
match nornir_build_thing::suite_cli::run(&root, &parsed) {
Ok(built) => {
// `--list` RESOLVED and printed; it built nothing. Saying "built" there
// would be exactly the small lie this whole pass is about.
if !parsed.list && !built.is_empty() {
println!("dwarves xtask build: built {}", built.join(", "));
}
0
}
Err(e) => {
eprintln!("dwarves xtask build FAILED: {e:#}");
1
}
}
}
fn main() {
let argv: Vec<String> = std::env::args().skip(1).collect();
let verb = std::env::args().nth(1);
match verb.as_deref() {
Some("build") => std::process::exit(run_build(&argv[1..])),
Some("ci") => std::process::exit(ci::run(&argv[1..])),
Some("build-history") => std::process::exit(build_history::run(&argv[1..])),
// `cargo xtask open-crates` — the open crates through `dwarves release` (A28 M27).
Some("open-crates") => std::process::exit(xtask::open_crates::run(&cfg().workspace_root, &argv[1..])),
// `cargo xtask build-flow-svg` — the audited build flow, drawn from the
// data model in `build_flow_svg::STEPS` (each box carries the file:line
// it was read at). Same idiom as gunnar's `cargo xtask svg`.
Some("build-flow-svg") => match build_flow_svg::run(&cfg().workspace_root) {
Ok(dst) => {
println!("\n✔ build-flow-svg: {}", dst.display());
std::process::exit(0)
}
Err(e) => {
eprintln!("build-flow-svg: {e}");
std::process::exit(1)
}
},
Some("demo-wasm") => match declared(&cfg().workspace_root).and_then(|set| {
nornir_pages::demo_wasm(&cfg(), &set, DEMO_VARIANT).map_err(|e| e.to_string())
}) {
Ok(dist) => {
println!(
"\n✔ demo-wasm: dwarves-gui wasm bundle → {}",
dist.display()
);
}
Err(e) => {
eprintln!("\n✗ demo-wasm failed: {e}");
std::process::exit(1);
}
},
Some("deploy-pages") => match declared(&cfg().workspace_root).and_then(|set| {
nornir_pages::deploy_pages(&cfg(), &set, DEMO_VARIANT).map_err(|e| e.to_string())
}) {
Ok(sha) => {
println!("\n✔ deploy-pages: dwarves-gui published to Codeberg Pages (pages commit {sha})");
}
Err(e) => {
eprintln!("\n✗ deploy-pages failed: {e}");
std::process::exit(1);
}
},
Some("gen-installer-stub") => match gen_installer_stub(&cfg().workspace_root) {
Ok(dst) => println!(
"\n✔ gen-installer-stub: embedded korp-installer bootx64.efi → {}",
dst.display()
),
Err(e) => {
eprintln!("\n✗ gen-installer-stub failed: {e}");
std::process::exit(1);
}
},
Some("gen-start-stub") => match gen_start_stub(&cfg().workspace_root) {
Ok(dst) => println!(
"\n✔ gen-start-stub: embedded korp-start bootx64.efi → {}",
dst.display()
),
Err(e) => {
eprintln!("\n✗ gen-start-stub failed: {e}");
std::process::exit(1);
}
},
Some("gen-boot-stage1") => match gen_boot_stage1(&cfg().workspace_root) {
Ok(dst) => println!(
"\n✔ gen-boot-stage1: embedded PRODUCTION first-stage korp-boot → {}",
dst.display()
),
Err(e) => {
eprintln!("\n✗ gen-boot-stage1 failed: {e}");
std::process::exit(1);
}
},
Some("gen-live-stub") => match gen_live_stub(&cfg().workspace_root) {
Ok(dst) => println!(
"\n✔ gen-live-stub: embedded korp-live bootx64.efi → {}",
dst.display()
),
Err(e) => {
eprintln!("\n✗ gen-live-stub failed: {e}");
std::process::exit(1);
}
},
Some("docs") => {
#[cfg(feature = "docs")]
{
docs::run();
}
#[cfg(not(feature = "docs"))]
{
eprintln!(
"xtask: the `docs` verb needs the `docs` feature (nornir's typst \
renderer). Run it via the dedicated alias:\n\n cargo xtask-docs docs\n"
);
std::process::exit(2);
}
}
other => {
if let Some(v) = other {
eprintln!("xtask: unknown verb `{v}`");
}
eprintln!("dwarves xtask — build automation for the live web demo");
eprintln!();
eprintln!("usage: cargo xtask <verb>");
eprintln!();
eprintln!("verbs:");
eprintln!(" build THE build verb — resolve (suite, variant) against");
eprintln!(" .nornir/build-variants.toml and build it:");
eprintln!(
" cargo xtask build --suite ORIGINAL [--variant <v>] [--list]"
);
eprintln!(" ci the CI plan: cargo xtask ci [--quick|--nightly] [--list];");
eprintln!(" cargo xtask ci lockcheck --siblings <dir> (THE LOCK CHECK);");
eprintln!(" cargo xtask ci status --runs <dir> [--out STATUS.md]");
eprintln!(" build-history seal / verify / regenerate the znippy-build-thing");
eprintln!(" package — step 3, after the build and BEFORE release:");
eprintln!(
" cargo xtask build-history seal --package h.znippy <bin>…"
);
eprintln!(
" cargo xtask build-history verify --package h.znippy <bin>…"
);
eprintln!(
" cargo xtask build-history show --package h.znippy [--recipe k]"
);
eprintln!(" open-crates the open crates a lone clone of Holger needs, on crates.io");
eprintln!(" through `dwarves release` (a PLAN unless named):");
eprintln!(" cargo xtask open-crates [--siblings <dir>] [--live-dry-run | --execute]");
eprintln!(" gen-installer-stub rebuild the EMBEDDED korp-installer bootx64.efi");
eprintln!(" (dev-box only; the mastered ISO needs no toolchain)");
eprintln!(" gen-start-stub rebuild the EMBEDDED korp-start bootx64.efi (2nd-boot");
eprintln!(" start-app; dev-box only, the installed disk needs none)");
eprintln!(" gen-boot-stage1 rebuild the EMBEDDED PRODUCTION first-stage loader");
eprintln!(" (korp-boot: EFI/BOOT/BOOTX64.EFI → chainloads \\vmlinuz)");
eprintln!(" gen-live-stub rebuild the EMBEDDED korp-live bootx64.efi (live-ISO)");
eprintln!(" demo-wasm build the dwarves-gui release wasm bundle into dist/");
eprintln!(" deploy-pages build, then force-push dist/ to the `pages` branch");
eprintln!(" docs render .nornir/*.md → the typeset SVG book the Manual");
eprintln!(" pane embeds (needs `cargo xtask-docs docs`)");
std::process::exit(2);
}
}
}