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

nordisk/facett

xtask/src/main.rs

//! **facett xtask** — the THIN DOOR onto facett's build thing, invoked as `cargo
//! xtask <cmd>`. Every build step is Rust here, never YAML CI.
//!
//! THE LAW (workspace): *xtask is a THIN DOOR onto the build thing, never a second
//! build system* — and it carries ONE dependency, the repo's own recipe
//! (`facett-build-thing`, which re-exports `nornir-build-thing`). This crate builds
//! ROWS and nothing else; `tests/one_dependency.rs` holds the one edge.
//!
//! ## Commands
//! - `cargo xtask build [--variant <name>] [--list]` — the thin door onto
//!   `facett-build-thing` (`xtask::door`): every declared row, argv by
//!   `build_variant_in_container` (the pinned image, never host cargo), non-cargo
//!   rows refused by name.
//! - `cargo xtask wrap-wasm [<group>…]` — every bundle group row of the recipe
//!   (`BUNDLE_GROUPS`, core first): the cdylib plus its declared wasm-bindgen +
//!   manifest stages (the bundles the wheel carries).
//! - `cargo xtask buildgate` — THE build gate (`nornir_build_thing::buildgate`).
//!
//! ## The verbs that are not builds: `cargo ops <cmd>`
//! `publish-pip`, `publish-holger`, `verify-install`, `streamlit`, `test-demo`,
//! `demo-wasm`, `deploy-pages` and `panel-wasm` moved to the `facett-ops` crate on
//! 2026-09-30 (`.nornir/xtask-thin-door-audit.md` §7): they need bollard, znippy, zip,
//! the Pages flow — crates the recipe must not carry (it compiles to the wasm32 plugin
//! the forge loads), and this door must not either. `cargo xtask <moved verb>` is
//! REFUSED BY NAME with the door that runs it (exit 2); xtask does not shell to it.
//!
//! ## Prerequisites
//! A container engine socket (rootless podman) holding — or able to mint — the
//! pinned toolchain image. The image carries the wasm target and builds
//! wasm-bindgen at the `Cargo.lock` version offline.

use facett_build_thing::nornir_build_thing;
use facett_build_thing::repo_root;

type Result<T> = std::result::Result<T, String>;

/// The verbs that moved to `facett-ops`, each refused here with its new door.
pub const MOVED_TO_OPS: [&str; 8] = [
    "publish-pip",
    "publish-holger",
    "verify-install",
    "streamlit",
    "test-demo",
    "demo-wasm",
    "deploy-pages",
    "panel-wasm",
];

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let cmd = args.first().map(String::as_str).unwrap_or("help");
    let result: Result<()> = match cmd {
        // `cargo xtask buildgate` — THE build gate. The whole verb is
        // `nornir_build_thing::buildgate::cli::run`, shared verbatim with korp and
        // znippy-zoomies (LAW 5). It exits the process itself because the gate's
        // verdict is an exit CODE, not a `Result`.
        "buildgate" => {
            std::process::exit(nornir_build_thing::buildgate::cli::run(&repo_root(), &args[1..]));
        }
        // ── THE ONE BUILD VERB ───────────────────────────────────────────────
        //
        // Everything that decides WHAT gets built lives in facett's declaration
        // (`facett-build-thing`), and `xtask::door` asks the CODE for it and hands
        // each row to `build_variant_in_container` — the forge's argv, inside the pinned image.
        // If this arm ever grows a variant name or a `--features` string, the
        // declaration has acquired a second writer.
        "build" => build_declared(),
        "wrap-wasm" => wrap_wasm(),
        // The verbs that build nothing live in facett-ops. Refused BY NAME with the
        // door that runs them — the owner's `cargo xtask publish-pip` hears where it
        // went instead of "unknown command".
        "publish-pip" => moved(&args),
        "publish-holger" => moved(&args),
        "verify-install" => moved(&args),
        "streamlit" => moved(&args),
        "test-demo" => moved(&args),
        "demo-wasm" => moved(&args),
        "deploy-pages" => moved(&args),
        "panel-wasm" => moved(&args),
        "help" | "--help" | "-h" => {
            print_help();
            Ok(())
        }
        other => {
            eprintln!("xtask: unknown command `{other}`\n");
            print_help();
            std::process::exit(2);
        }
    };
    if let Err(e) = result {
        eprintln!("\nxtask: {cmd} failed: {e}");
        std::process::exit(1);
    }
}

/// A verb that moved to facett-ops: refused BY NAME with the door that runs it (exit
/// 2). xtask does not shell to it — pointing is not running.
fn moved(args: &[String]) -> ! {
    eprintln!(
        "xtask: `{}` is not a build — it moved to facett-ops on 2026-09-30.\n  run:  cargo ops {}\n  \
         (xtask is the THIN DOOR: `build`, `wrap-wasm`, `buildgate`; ONE dependency, the recipe)",
        args[0],
        args.join(" ")
    );
    std::process::exit(2);
}

fn print_help() {
    eprintln!(
        "facett xtask — the thin door onto facett-build-thing (no YAML CI)\n\n\
         USAGE:\n  cargo xtask <command>\n\n\
         COMMANDS:\n\
         \x20 build                Build a DECLARED row by name — `--list` to see them all,\n\
         \x20                      `--variant <name>` to build one. Resolves facett-build-thing\n\
         \x20                      (the code, not the emitted toml), built IN the pinned image;\n\
         \x20                      container/package rows are REFUSED with the door that builds them\n\
         \x20 wrap-wasm [group…]   Every bundle group row (core = `{}` into {}/, then\n\
         \x20                      the others), each with its wasm-bindgen + manifest stages\n\
         \x20 buildgate            Compile everything the repo declares and go RED with the\n\
         \x20                      numbers: bins declared vs built, and every tests/*.rs that\n\
         \x20                      compiled to a binary holding zero tests\n\
         \x20 help                 Show this help\n\n\
         NOT BUILDS — `cargo ops <command>` (facett-ops): {}\n",
        facett_build_thing::WASM_VARIANT,
        facett_build_thing::WASM_DIST,
        MOVED_TO_OPS.join(", "),
    );
}

/// `cargo xtask build [--variant <name>] [--list]` — build a DECLARED row.
///
/// The whole body is [`xtask::door::run`]: resolve against `facett-build-thing` (the
/// CODE — the emitted toml is never read back), materialize the argv with
/// `build_variant_in_container` (pinned image), run the row's declared `[[build.wasm]]`
/// stages, and REFUSE every container/package row by name with the door that builds
/// it. This file contributes no row name, feature string or triple.
fn build_declared() -> Result<()> {
    let root = repo_root();
    let rest: Vec<String> = std::env::args().skip(2).collect();
    // `--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.
    let listed = rest.iter().any(|a| a == "--list");
    let built = xtask::door::run(&root, &rest)?;
    if !listed {
        println!("\nxtask: ✔ built {} row(s): {}", built.len(), built.join(", "));
    }
    Ok(())
}

/// `cargo xtask wrap-wasm [<group>…]` — **every bundle group's row, by the recipe's list.**
///
/// The bundles the wheel carries, one per `facett_build_thing::BUNDLE_GROUPS` entry
/// (core first — `facet-wrapped-wasm`, byte-identical to the pre-group bundle — then
/// the others): each row's cargo argv (from `build_variant_in_container`, inside the
/// pinned image, entered ONCE for every group), then its `[[build.wasm]]` cell's typed
/// stages, in the same image — wasm-bindgen `--target web --no-typescript` at the
/// `Cargo.lock` version, and the `manifest` example (run with the row's features)
/// captured into `manifest.json` — into the cell's `out_dir`. Every one of those facts
/// is the recipe's; this verb names no row, it walks the recipe's list. Group names
/// as arguments narrow it (`wrap-wasm core`); an unknown one is refused by name.
fn wrap_wasm() -> Result<()> {
    let root = repo_root();
    let set = facett_build_thing::declared();
    let asked: Vec<String> = std::env::args().skip(2).collect();
    for a in &asked {
        if facett_build_thing::bundle_group(a).is_none() {
            let known: Vec<&str> = facett_build_thing::BUNDLE_GROUPS.iter().map(|g| g.name).collect();
            return Err(format!("wrap-wasm: no bundle group `{a}` — the recipe declares {known:?}"));
        }
    }
    let road = xtask::door::enter(&set, &root)?;
    for g in facett_build_thing::BUNDLE_GROUPS.iter().filter(|g| asked.is_empty() || asked.iter().any(|a| a == g.name)) {
        let row = set.require(g.variant).map_err(|e| format!("{e:#}"))?;
        let produced = xtask::door::build_one(&set, row, &root, &road)?;
        for p in &produced {
            let n = std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
            println!("xtask: [{}] {} ({n} bytes)", g.name, p.display());
        }
        println!("xtask: ✔ bundle group `{}` in {}", g.name, root.join(g.dist_dir).display());
    }
    Ok(())
}