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

nordisk/nornir

xtask/src/main.rs

//! **nornir xtask** — dependency-free build automation.
//!
//! Verbs:
//! - `cargo xtask build [--suite <s>] [--atom <a>] [--list]` — **the ONE build
//!   verb.** Its entire body is `nornir_build_thing::suite_cli::run(root, &args)`:
//!   it reads `.nornir/build-variants.toml` and contains no variant name, no
//!   feature string and no cargo argv of its own. Same command shape in korp,
//!   dwarves, nornir and modgunn (`--suite ORIGINAL`) and in holger (`--suite MVP`).
//! - `cargo xtask demo-wasm`    — build the `nornir-demo` release wasm bundle into
//!   `dist/` (`index.html` + `nornir-demo.js` + `nornir-demo_bg.wasm` + Pages meta),
//!   a self-contained static site.
//! - `cargo xtask deploy-pages` — run `demo-wasm`, then force-push `dist/` to the
//!   `pages` branch, which Codeberg serves at <https://nordisk.codeberg.page/nornir>.
//! - `cargo xtask bench [--heavy|--light]` — run the Mímir bench suite. Two TIERS:
//!   **heavy** = the full ~105-arm authoritative run (needs a FAT box like oden);
//!   **light** = a fast representative smoke (a few working-set arms) that a normal
//!   laptop can run without churning. With no flag the tier is AUTO-selected from the
//!   machine: fat (≥24 cores AND ≥64 GiB) ⇒ heavy, else ⇒ light. So a normal machine
//!   never accidentally kicks off the heavy run. `NORNIR_MACHINE` is stamped from the
//!   hostname so oden's rows never overwrite another host's.
//!
//! - `cargo xtask feature-matrix [--atom <a>] [--list]` — ★ **THE FEATURE-COMBO
//!   GATE** (SK290). `cargo check`s every combination `.nornir/build-variants.toml`
//!   DECLARES, one `cargo check` per distinct
//!   `(--no-default-features, --features …, --bin …)` triple, and exits non-zero
//!   naming the atoms that failed. It reads the SAME declaration `xtask build` reads,
//!   so the matrix cannot drift from the atom list — there is no second list of
//!   combos anywhere.
//!
//!   Why it exists: on 2026-08-01 the leanest DECLARED atom — `nornir-ui`,
//!   `--no-default-features --features viz` (ATOM 4/7) — did not compile, and had
//!   not for an unknown length of time, because nothing ever built it. `cargo
//!   build`/`cargo test` only ever build `default`, and `default` contains every
//!   feature, so a symbol that should be `#[cfg]`-gated and is not is INVISIBLE
//!   there. Measured red:
//!
//!   ```text
//!   error[E0433]: cannot find `DocFormat` in `docs`   --> src/bin/nornir.rs:13130:24
//!   error[E0425]: cannot find function `client_workspace` in this scope
//!                                                     --> src/bin/nornir.rs:9689:21
//!   error: could not compile `nornir` (bin "nornir") due to 18 previous errors
//!   ```
//!
//!   `cargo check` (not `build`), a dedicated `--target-dir`
//!   (`$NORNIR_FEATURE_MATRIX_TARGET_DIR`, default `target/feature-matrix`) so the
//!   per-combo feature churn never invalidates the normal `target/`, and
//!   `--all-targets` OFF on purpose: the point is the shipped artifacts.
//!
//! The gh-pages FLOW itself lives ONCE in the edda leaf `nornir-pages` (LAW #5);
//! this crate is a thin dispatcher that builds nornir's [`PagesConfig`] knobs and
//! calls `nornir_pages::{demo_wasm, deploy_pages}` with nornir's DECLARATION and the
//! demo row's name. Since 2026-09-30 those build the row IN THE PINNED IMAGE
//! (`nornir_build_thing::container_road`: cargo + its wasm-bindgen stage) — never host
//! cargo or host wasm-bindgen (THE LAW: a build thing builds in a container). Only the
//! network `git push` shells. No YAML CI, no shell scripts.
//!
//! ⚠ nornir declares NO `nornir-demo-wasm` row today: the declared set is pinned to
//! the five matrix atoms (`tests/build_variants.rs`, "EXACTLY THE MATRIX — no more, no
//! less"). So `demo-wasm`/`deploy-pages` are REFUSED BY NAME until the owner decides
//! how the Pages demo row enters the declaration — there is no host fallback.

use std::path::Path;

use nornir_pages::PagesConfig;
use nornir_xtask_support::workspace_root_of;

/// The declared row the Pages demo is built from (`nornir-demo`, wasm32, with its
/// `[[build.wasm]]` bindgen cell). Named here because the verb needs ONE row; what
/// the row builds is the declaration's.
const DEMO_VARIANT: &str = "nornir-demo-wasm";

/// nornir's declaration, validated — the same load `cargo xtask build` does.
fn declared(root: &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 FEATURE-COMBO GATE (SK290)
// ─────────────────────────────────────────────────────────────────────────────

/// One combination the declaration asks for. Atoms whose cargo flags are
/// byte-identical share a `Combo` (checked ONCE, every sharing atom named), so
/// the gate never pays twice for one compile.
struct Combo {
    /// Every declared atom this combination belongs to, in declaration order.
    atoms: Vec<String>,
    /// The cargo argv, taken from [`BuildVariant::cargo_args`] with `build`
    /// swapped for `check` — NOT re-derived here. See [`check_args`].
    args: Vec<String>,
    /// The atom's `cfg = ` passthrough, appended to `RUSTFLAGS`. `None` for all
    /// nornir atoms today; carried so a future one is not silently checked
    /// without the `--cfg` it ships with.
    rustflags_cfg: Option<String>,
}

/// The atom's own cargo argv, as a `check` instead of a `build`.
///
/// ⚠ **THIS USED TO BE HAND-ROLLED, AND THE COPY WAS ALREADY WRONG** (LAW 5).
/// Until 2026-08-03 this file walked `v.package` / `v.bin` /
/// `v.no_default_features` / `v.features` and assembled the argv itself — a
/// second materializer beside `nornir_build_thing::variant::BuildVariant::cargo_args`,
/// which every other reader of this declaration goes through (`suite_cli::run`
/// presses it, `--list` prints it, `no_two_atoms_materialize_the_same_cargo_line`
/// compares it).
///
/// The twin had already drifted in three ways, all of them silent:
///
/// * **`--lib` was missing.** `cargo_args` emits it for `kind = "lib"`; the copy
///   did not, so a lib atom would be checked as a bin that does not exist.
/// * **`--target` was missing.** A wasm atom (`target = "wasm32-*"`) would have
///   been checked for the HOST, i.e. the one combination it is not shipped as —
///   green here, broken on release.
/// * **`--profile` was missing**, so atoms declaring a custom profile were checked
///   under a different `cfg`/opt configuration than they ship with.
///
/// None of those bite nornir's five atoms TODAY, which is exactly the problem: a
/// twin that agrees on the current inputs is invisible until the day it does not,
/// and the gate reporting green is what would hide it. Deriving from the shared
/// materializer means adding a lib/wasm/profile atom needs no change here.
///
/// The swap itself is deliberate and is the ONLY difference this verb is entitled
/// to: `cargo_args()[0]` is the subcommand, and asserting that is what
/// [`check_args`] does before replacing it — if the shared materializer ever stops
/// leading with a subcommand, this fails loudly instead of producing
/// `cargo -p nornir check …`.
fn check_args(v: &nornir_build_thing::variant::BuildVariant) -> Result<Vec<String>, String> {
    let mut args = v.cargo_args();
    match args.first().map(String::as_str) {
        Some("build") => {
            args[0] = "check".to_string();
            Ok(args)
        }
        other => Err(format!(
            "atom '{}': BuildVariant::cargo_args() no longer starts with the `build` \
             subcommand (got {other:?}) — this gate swaps that word for `check` and \
             cannot do so blindly. Fix the swap rather than the assertion.",
            v.name
        )),
    }
}

impl Combo {
    /// The human line the report prints — the exact command a reader can paste.
    fn command_line(&self) -> String {
        format!("cargo {}", self.args.join(" "))
    }
}

/// `cargo check` every feature combination `.nornir/build-variants.toml` declares.
///
/// The combos are DERIVED from the declaration through the production loader, so
/// this verb holds no feature string and no atom name of its own: adding an atom
/// to the TOML adds it to the gate, and there is nothing to keep in sync.
///
/// Fails (`Err`) when any combination does not compile, when the declaration is
/// missing, or when the filter selected NOTHING — an empty matrix that reported
/// "ok" would be exactly the vacuous green this gate exists to close.
fn feature_matrix(root: &Path, argv: &[String]) -> Result<(), String> {
    let set = nornir_build_thing::variant::BuildVariantSet::discover(root)
        .map_err(|e| format!("read .nornir/build-variants.toml: {e:#}"))?
        .ok_or_else(|| {
            format!(
                "{} declares no .nornir/build-variants.toml, so there are NO declared \
                 feature combinations to check — this gate has nothing to stand on and \
                 refuses to report a pass",
                root.display()
            )
        })?;

    let only = argv
        .iter()
        .position(|a| a == "--atom")
        .and_then(|i| argv.get(i + 1))
        .map(String::as_str);
    let list_only = argv.iter().any(|a| a == "--list");

    let target_dir = std::env::var("NORNIR_FEATURE_MATRIX_TARGET_DIR").unwrap_or_else(|_| {
        root.join("target")
            .join("feature-matrix")
            .display()
            .to_string()
    });

    let mut combos: Vec<Combo> = Vec::new();
    for v in &set.variants {
        if let Some(want) = only {
            if v.name != want {
                continue;
            }
        }
        // ⚠ NOT re-derived here — the SHARED materializer, `build` → `check`.
        let args = check_args(v)?;
        // The same flags twice is one compile, not two.
        if let Some(hit) = combos
            .iter_mut()
            .find(|c| c.args == args && c.rustflags_cfg == v.cfg)
        {
            hit.atoms.push(v.name.clone());
            continue;
        }
        combos.push(Combo {
            atoms: vec![v.name.clone()],
            args,
            rustflags_cfg: v.cfg.clone(),
        });
    }

    if combos.is_empty() {
        return Err(match only {
            Some(a) => format!(
                "--atom {a} matched no declared atom; declared: {}",
                set.names().join(", ")
            ),
            None => "the declaration contains no atoms, so the matrix is EMPTY — an \
                     empty matrix cannot report red and must not report green"
                .to_string(),
        });
    }

    println!(
        "nornir xtask feature-matrix — {} declared atom(s) ⇒ {} distinct feature combination(s)",
        set.variants.len(),
        combos.len()
    );
    println!("  target-dir: {target_dir}\n");
    for c in &combos {
        println!("  · {} — {}", c.atoms.join(" + "), c.command_line());
    }
    if list_only {
        println!("\n(--list: nothing compiled)");
        return Ok(());
    }
    println!();

    let mut failed: Vec<(&Combo, String)> = Vec::new();
    for (i, c) in combos.iter().enumerate() {
        println!("[{}/{}] {} …", i + 1, combos.len(), c.atoms.join(" + "));
        let mut cmd = std::process::Command::new(env!("CARGO"));
        cmd.current_dir(root)
            .args(&c.args)
            .args(["--target-dir", &target_dir])
            // `cargo check` writes diagnostics to stderr; merge nothing, capture
            // both, and judge the OUTPUT, not just the status — a cargo that fails
            // to launch its own resolution prints and exits, and an empty output
            // read as clean is the trap this file is guarding against.
            .stdin(std::process::Stdio::null());
        if let Some(cfg) = &c.rustflags_cfg {
            let inherited = std::env::var("RUSTFLAGS").unwrap_or_default();
            cmd.env(
                "RUSTFLAGS",
                format!("{inherited} --cfg {cfg}").trim().to_string(),
            );
        }
        let out = match cmd.output() {
            Ok(o) => o,
            Err(e) => {
                failed.push((c, format!("could not launch cargo: {e}")));
                println!("      ✗ could not launch cargo: {e}");
                continue;
            }
        };
        let text = format!(
            "{}{}",
            String::from_utf8_lossy(&out.stdout),
            String::from_utf8_lossy(&out.stderr)
        );
        // ⚠ The grep that missed a whole broken build once: `: error` does NOT
        // match a line that STARTS with `error:` / `error[E0433]:`, which is the
        // shape of every rustc diagnostic AND of a cargo feature-resolution
        // failure. Both forms are matched here.
        let errors: Vec<&str> = text
            .lines()
            .filter(|l| {
                let t = l.trim_start();
                t.starts_with("error:") || t.starts_with("error[") || l.contains(": error")
            })
            .collect();
        if out.status.success() && errors.is_empty() {
            println!("      ✓ compiles");
            continue;
        }
        // Status and output must AGREE. A non-zero exit with no error line means
        // the failure is somewhere this filter cannot see, and saying "0 errors"
        // there is the false-green shape.
        let detail = if errors.is_empty() {
            format!(
                "cargo exited {} but printed no `error:` line — the last 20 lines of its \
                 output follow:\n{}",
                out.status,
                text.lines()
                    .rev()
                    .take(20)
                    .collect::<Vec<_>>()
                    .into_iter()
                    .rev()
                    .map(|l| format!("        {l}"))
                    .collect::<Vec<_>>()
                    .join("\n")
            )
        } else {
            errors
                .iter()
                .take(12)
                .map(|l| format!("        {l}"))
                .collect::<Vec<_>>()
                .join("\n")
        };
        println!("      ✗ {} error line(s)", errors.len());
        failed.push((c, detail));
    }

    if failed.is_empty() {
        println!(
            "\n✔ every declared feature combination compiles ({}/{})",
            combos.len(),
            combos.len()
        );
        return Ok(());
    }

    let mut msg = format!(
        "{}/{} DECLARED feature combination(s) do not compile:\n",
        failed.len(),
        combos.len()
    );
    for (c, detail) in &failed {
        msg.push_str(&format!(
            "\n  ✗ atom(s) {} — {}\n{}\n",
            c.atoms.join(" + "),
            c.command_line(),
            detail
        ));
    }
    Err(msg)
}

/// nornir's per-repo knobs — the entirety of the divergence from the other three
/// xtasks. Everything else (build → wasm-bindgen → index.html → orphan-worktree
/// force-push) is the shared `nornir-pages` flow.
fn cfg() -> PagesConfig {
    PagesConfig {
        // The cargo crate that owns the wasm binary.
        demo_crate: "nornir-demo",
        // The eframe canvas id the wasm entry (`nornir-demo/src/main.rs::start` →
        // `WebDemo::new`) and the `index.html` loader agree on.
        canvas_id: "demo_canvas",
        // The exported JS state-hook name (`nornir-demo/src/main.rs::nornir_state`).
        state_export: "__nornir_state",
        title: "nornir — live web demo",
        description: "nornir: the release dependency-DAG / publish graph, in your browser — the live in-browser demo (facett-rendered, sample fixture).",
        // The live URL Codeberg Pages serves it at (printed banner).
        pages_url: "https://nordisk.codeberg.page/nornir",
        // 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`
        // (`ssh://git@git.gunnar.rs:2222/nordisk/nornir.git`, SSH-only per the nordisk law).
        remote_env: "NORNIR_PAGES_REMOTE",
        commit_label: "nornir 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 this `xtask` crate dir.
        workspace_root: workspace_root_of(env!("CARGO_MANIFEST_DIR").as_ref()),
    }
}

/// A machine is "fat" (⇒ auto-heavy bench) at or above BOTH thresholds. oden is
/// 32 cores / 499 GiB, so it clears them; a normal laptop does not and gets light.
const FAT_CORES: usize = 24;
const FAT_MEM_GIB: u64 = 64;

/// The LIGHT-tier arm filter (`NORNIR_BENCH_ONLY`, comma-separated prefixes): the
/// SQL point-search arms + the batched-embed populate arm. A fast, representative
/// smoke — the harness fails RED if these ids vanish. (The working-set dup arms it
/// named before went with the `symbol_facts` read, Mímir v2.)
const LIGHT_BENCH_ARMS: &str =
    "nornir.mimir.search.sql_point,nornir.mimir.populate.embed_members_batched";

/// Logical cores, best-effort (never panics; 1 on failure).
fn cpu_cores() -> usize {
    std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(1)
}

/// Total RAM in GiB from `/proc/meminfo` (`MemTotal` is in kB); 0 if unreadable
/// (⇒ treated as not-fat, i.e. light — the safe default for an unknown box).
fn mem_total_gib() -> u64 {
    std::fs::read_to_string("/proc/meminfo")
        .ok()
        .and_then(|s| {
            s.lines()
                .find(|l| l.starts_with("MemTotal:"))
                .and_then(|l| l.split_whitespace().nth(1))
                .and_then(|kb| kb.parse::<u64>().ok())
        })
        .map(|kb| kb / (1024 * 1024))
        .unwrap_or(0)
}

/// The host name stamped into `NORNIR_MACHINE` so per-host bench rows never mix
/// (oden's numbers must never overwrite another box's). `/etc/hostname` → `$HOSTNAME`
/// → "unknown".
fn hostname() -> String {
    std::fs::read_to_string("/etc/hostname")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .or_else(|| std::env::var("HOSTNAME").ok())
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "unknown".to_string())
}

/// nornir's CI plan: its locks, its workspace tests, (full) its ORIGINAL rows and
/// (nightly) its bench.
fn ci_plan() -> nornir_xtask_support::ci::Plan {
    use nornir_xtask_support::ci::{Mode, Plan, Step};
    let root = workspace_root_of(env!("CARGO_MANIFEST_DIR").as_ref());
    Plan {
        repo: "nornir".into(),
        root: root.clone(),
        steps: vec![
            nornir_xtask_support::lockcheck::step(&root),
            Step::cargo("test", &["test", "--workspace", "--locked"]),
            Step::cargo("rows:ORIGINAL", &["xtask", "build", "--suite", "ORIGINAL"]).from(Mode::Full),
            Step::cargo("bench", &["xtask", "bench", "--light"]).from(Mode::Nightly),
        ],
    }
}

fn main() {
    let verb = std::env::args().nth(1);
    match verb.as_deref() {
        // `cargo xtask ci [--quick|--nightly] [--list] [--only <prefix>]`: nornir's CI
        // plan on the shared runner (nornir-xtask-support `ci`), one JSON receipt.
        Some("ci") => {
            let rest: Vec<String> = std::env::args().skip(2).collect();
            std::process::exit(nornir_xtask_support::ci::main(&ci_plan(), &rest));
        }
        // ── THE ONE BUILD VERB ───────────────────────────────────────────────
        // Three lines on purpose. Everything that decides WHAT gets built — the
        // atoms, their features, their bins, their profiles, which delivery they
        // belong to and which one is the default — lives in
        // `.nornir/build-variants.toml` and is read by `suite_cli`. If this arm
        // ever grows a variant name or a `--features` string, the declaration has
        // acquired a second reader and they will drift; that is the exact defect
        // this verb replaced (`nornir-buildspec-holger` re-declared holger's
        // variants in Rust: 8 in code vs 13 in the TOML).
        Some("build") => {
            let rest: Vec<String> = std::env::args().skip(2).collect();
            let root = workspace_root_of(env!("CARGO_MANIFEST_DIR").as_ref());
            let res = nornir_build_thing::suite_cli::parse(&rest).and_then(|args| {
                // `--list` resolves and prints without compiling, so the banner must
                // not claim a build. "resolved" and "built" reading the same is the
                // silent-green shape this whole pass is about.
                let listed = args.list;
                nornir_build_thing::suite_cli::run(&root, &args).map(|names| (listed, names))
            });
            match res {
                Ok((true, _)) => {}
                Ok((false, built)) => {
                    println!("\n✔ built {} atom(s): {}", built.len(), built.join(", "))
                }
                Err(e) => {
                    eprintln!("\n✗ {e:#}");
                    std::process::exit(1);
                }
            }
        }
        // ── ★ THE FEATURE-COMBO GATE (SK290) ────────────────────────────────
        // No combo list of its own: every combination checked here is read off
        // `.nornir/build-variants.toml` through the production loader, the same
        // one `xtask build` and `tests/build_variants.rs` use.
        Some("feature-matrix") => {
            let rest: Vec<String> = std::env::args().skip(2).collect();
            let root = workspace_root_of(env!("CARGO_MANIFEST_DIR").as_ref());
            match feature_matrix(&root, &rest) {
                Ok(()) => {}
                Err(e) => {
                    eprintln!("\n✗ {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✔ nornir-demo 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✔ nornir-demo published to Codeberg Pages (pages commit {sha})");
            }
            Err(e) => {
                eprintln!("\n✗ deploy-pages failed: {e}");
                std::process::exit(1);
            }
        },
        Some("bench") => {
            let rest: Vec<String> = std::env::args().skip(2).collect();
            let force_heavy = rest.iter().any(|a| a == "--heavy");
            let force_light = rest.iter().any(|a| a == "--light");
            let (cores, mem_gib) = (cpu_cores(), mem_total_gib());
            let fat = cores >= FAT_CORES && mem_gib >= FAT_MEM_GIB;
            let heavy = if force_heavy {
                true
            } else if force_light {
                false
            } else {
                fat
            };
            let why = if force_heavy || force_light {
                "forced"
            } else if fat {
                "fat machine → auto-heavy"
            } else {
                "normal machine → auto-light"
            };
            let host = hostname();
            println!(
                "nornir xtask bench: {host} — {cores} cores, {mem_gib} GiB RAM ⇒ {} tier ({why})",
                if heavy { "HEAVY" } else { "LIGHT" }
            );

            let mut cmd = std::process::Command::new(env!("CARGO"));
            cmd.current_dir(workspace_root_of(env!("CARGO_MANIFEST_DIR").as_ref()))
                .args(["run", "--release", "--example", "nornir-bench"])
                // Stamp the host so oden's rows never overwrite another machine's
                // (bench tables are per-host; mixing hosts makes numbers meaningless).
                .env("NORNIR_MACHINE", &host);
            if !heavy {
                // LIGHT tier: a fast, representative smoke — the working-set search arms
                // (the ones the Tier-0 cache work moves) + the batched-embed populate arm.
                // The harness fails RED if these ids ever vanish, so a stale filter can
                // never silently pass as a green zero-arm run.
                cmd.env("NORNIR_BENCH_ONLY", LIGHT_BENCH_ARMS);
                println!("  light arms: {LIGHT_BENCH_ARMS}");
            } else {
                println!("  heavy: the full linked bench suite (all ~105 arms)");
            }
            match cmd.status() {
                Ok(s) if s.success() => {
                    println!(
                        "\n✔ bench ({} tier) complete",
                        if heavy { "heavy" } else { "light" }
                    );
                }
                Ok(s) => {
                    eprintln!("\n✗ bench exited: {s}");
                    std::process::exit(s.code().unwrap_or(1));
                }
                Err(e) => {
                    eprintln!("\n✗ bench failed to launch cargo: {e}");
                    std::process::exit(1);
                }
            }
        }
        other => {
            eprintln!(
                "nornir xtask — usage:\n  \
                 cargo xtask build          build the declared atoms (--suite/--atom/--list)\n  \
                 cargo xtask demo-wasm      build the nornir-demo wasm bundle into dist/\n  \
                 cargo xtask deploy-pages   build + force-push dist/ to the `pages` branch\n  \
                 cargo xtask bench          run the Mímir bench suite (auto heavy on fat boxes, light on normal)\n  \
                 cargo xtask bench --heavy  force the full ~105-arm authoritative run\n  \
                 cargo xtask bench --light  force the fast representative smoke\n  \
                 cargo xtask feature-matrix cargo-check EVERY declared atom's feature combo (--atom <a>, --list)\n"
            );
            if other.is_some() {
                eprintln!("unknown verb: {}", other.unwrap());
            }
            std::process::exit(2);
        }
    }
}