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(())
}