xtask/src/door.rs
//! **`cargo xtask build [--variant <name>] [--list]` — the thin door.**
//!
//! THE LAW (workspace, 2026-09): *xtask is a THIN DOOR onto the build thing, never a
//! second build system.* This module owns no package name, no feature string, no
//! triple and no list of rows. It does four things, each through the engine the
//! recipe re-exports (`facett_build_thing::nornir_build_thing`):
//!
//! 1. **resolves** the ask against the CODE — [`facett_build_thing::declared`], never
//! the emitted `.nornir/build-variants.toml` — with edda's shared
//! `suite_cli::parse`/`resolve`, so the flags and refusals read the same in every
//! repo;
//! 2. **builds** every cargo row IN THE PINNED `[build.toolchain]` IMAGE with
//! `cargo_container::build_variant_in_container` over `DraupnirCargoRunner` —
//! the forge's own argv (`cargo_args()`), `--locked --offline`, network none. There
//! is no host road: "a build thing builds in a container or a KVM, never on the
//! host" (see [`Road`]);
//! 3. **runs the typed stages** a `[[build.wasm]]` cell declares (wasm-bindgen, the
//! component manifest) with edda's `wasm::run_stages`, IN the same pinned image
//! (edda's `container_road::ContainerStages`, not the host-side
//! `XtaskWasmStageRunner`), invoked from typed fields;
//! 4. **refuses BY NAME** every row cargo cannot build, with the door that does
//! ([`facett_build_thing::door_for`]).
//!
//! ## What it replaced
//!
//! Until 2026-09-29 `build` was `suite_cli::run`, which loads the declaration with
//! `BuildVariantSet::discover_validated` — i.e. it READ THE EMITTED TOML, the
//! description, as authority — and answered a container row with "skipped (use the
//! image/vm/iso builders)". And `wrap-wasm` spelled its own
//! `cargo build --release --lib -p facet-wrapped --target wasm32-unknown-unknown`,
//! its own artifact path and its own `wasm-bindgen --target web --no-typescript`
//! beside a recipe that declared all three. See `.nornir/xtask-thin-door-audit.md`.
use std::path::{Path, PathBuf};
use facett_build_thing::nornir_build_thing::{
self as nbt,
suite_cli::{self, BuildArgs},
variant::{BuildVariant, BuildVariantSet},
};
/// A resolved ask: the parsed flags, the delivery, and the rows it selects.
pub struct Resolved<'a> {
pub args: BuildArgs,
pub suite: &'a str,
pub chosen: Vec<&'a BuildVariant>,
}
/// The refusal for one row cargo cannot build, naming it and its door.
pub fn refusal(v: &BuildVariant) -> Option<String> {
facett_build_thing::door_for(v)
.map(|door| format!("'{}' is kind '{}', not a cargo build — {door}", v.name, v.kind.as_str()))
}
/// **Resolve `argv` against `set`** — parse, refuse what this door does not do, and
/// select rows. Asking for ONE row cargo cannot build (without `--list`) is refused
/// here BY NAME, before anything compiles.
pub fn resolve<'a>(set: &'a BuildVariantSet, argv: &[String]) -> Result<Resolved<'a>, String> {
let args = suite_cli::parse(argv).map_err(|e| format!("{e:#}"))?;
if !args.builds.is_empty() {
return Err(format!(
"--build {}: this door builds facett's own rows; chaining a build thing behind an \
edge is the forge's (`dwarves buildthing facett …`)",
args.builds.join(" --build ")
));
}
if !args.params.is_empty() {
return Err("--param supplies install-time inputs for iso/installer cells; facett \
declares none, and `build` materializes cargo rows"
.to_string());
}
let (suite, chosen) = suite_cli::resolve(set, &args).map_err(|e| format!("{e:#}"))?;
if !args.list && args.atom.is_some() && let Some(r) = chosen.first().and_then(|v| refusal(v)) {
return Err(format!("refused: {r}"));
}
Ok(Resolved { args, suite, chosen })
}
/// The `--list` rendering: edda's shared lines, with every non-cargo row's
/// "(not a cargo build …)" line replaced by its NAMED door.
pub fn list_lines(r: &Resolved<'_>) -> Vec<String> {
let mut lines = suite_cli::list_lines(r.suite, &r.chosen);
// `list_lines` is a header, then exactly two lines per row, in `chosen` order.
for (i, v) in r.chosen.iter().enumerate() {
if let (Some(door), Some(line)) = (facett_build_thing::door_for(v), lines.get_mut(2 + 2 * i)) {
*line = format!(" (not a cargo build — REFUSED here; {door})");
}
}
lines
}
/// **The build environment, resolved and entered ONCE** — edda's
/// `nornir_build_thing::container_road::Road`: the pinned `[build.toolchain]` image,
/// the host paths it binds, and the engine. There is NO host fallback: a box that
/// cannot enter the image hears so by name, before the first row is touched.
///
/// It lived here (and its stage runner in `xtask/src/stages.rs`) until 2026-09-30,
/// when nornir and dwarves wanted the same road for their Pages demos; it moved to
/// edda whole, so the three repos build their wasm rows with one writer.
pub use nbt::container_road::Road;
/// **The declared toolchain, validated**, with the one run-time cap a caller may
/// apply (`CARGO_BUILD_JOBS` may only LOWER the container's cpu bound). Refuses BY
/// NAME: no `[build.toolchain]`, and every `CargoToolchain::validate` arm.
pub fn toolchain(set: &BuildVariantSet) -> Result<nbt::cargo_container::CargoToolchain, String> {
nbt::container_road::toolchain(set).map_err(|e| format!("{e:#}"))
}
/// **Enter the road** — ask the engine for the PINNED reference; mint only when it
/// is absent (see `container_road::Road::enter`).
pub fn enter(set: &BuildVariantSet, root: &Path) -> Result<Road, String> {
Road::enter(set, root).map_err(|e| format!("{e:#}"))
}
/// **Build ONE cargo row IN THE CONTAINER** — refused BY NAME with facett's door when
/// cargo cannot build it, else `container_road::build_row`: the forge's argv via
/// `build_variant_in_container`, then the row's `[[build.wasm]]` stages in the same
/// image (`container_road::ContainerStages`). Returns the files produced.
pub fn build_one(set: &BuildVariantSet, v: &BuildVariant, root: &Path, road: &Road) -> Result<Vec<PathBuf>, String> {
if let Some(r) = refusal(v) {
return Err(format!("refused: {r}"));
}
nbt::container_road::build_row(set, v, root, road).map_err(|e| format!("{e:#}"))
}
/// **The whole verb.** Returns the rows BUILT (or, with `--list`, resolved).
pub fn run(root: &Path, argv: &[String]) -> Result<Vec<String>, String> {
let set = facett_build_thing::declared();
// The code road validates what the toml road validated at load — every shape
// rule and the on-disk inputs — so reading the code costs no check.
set.validate_all_in(root).map_err(|e| format!("facett-build-thing is refused: {e:#}"))?;
let r = resolve(&set, argv)?;
if r.args.list {
for l in list_lines(&r) {
println!("{l}");
}
return Ok(r.chosen.iter().map(|v| v.name.clone()).collect());
}
let (cargo_rows, refused): (Vec<&BuildVariant>, Vec<&BuildVariant>) =
r.chosen.iter().copied().partition(|v| refusal(v).is_none());
if !refused.is_empty() {
eprintln!("not cargo builds, REFUSED — each named with the door that does build it:");
for v in &refused {
eprintln!(" · {}", refusal(v).unwrap_or_default());
}
}
if cargo_rows.is_empty() {
return Err(format!(
"delivery '{}': nothing was built — every selected row is refused above. That is not a green.",
r.suite
));
}
// Entered ONCE, before the first row: a box that cannot enter it hears so here.
let road = enter(&set, root)?;
let mut built = Vec::new();
for v in cargo_rows {
for p in build_one(&set, v, root, &road)? {
let n = std::fs::metadata(&p).map(|m| m.len()).unwrap_or(0);
println!(" {} → {} ({n} bytes)", v.name, p.display());
}
built.push(v.name.clone());
}
Ok(built)
}