xtask/src/main.rs
//! Workspace task runner (pure Rust, zero shell).
//!
//! Usage: `cargo run -p xtask -- <task>`. Tasks:
//!
//! * `build-wasm-guest` — build the real `wasm32-unknown-unknown` guest in
//! `crates/knut-bifrost/wasm-guest/` (excluded from the native workspace), so
//! the `#[ignore]`d `knut-bifrost` `tests/wasm_guest.rs` has its artifact to
//! load. This is the CI/local step that gate depends on (TEST-GAP-2).
//!
//! The nornir bench suite lives in `examples/nornir-bench.rs` — run it with
//! `cargo bench -p xtask` or `nornir bench run`.
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
fn main() -> Result<()> {
let task = std::env::args().nth(1);
match task.as_deref() {
Some("build-wasm-guest") => build_wasm_guest(),
Some(other) => {
eprintln!("unknown task `{other}`");
print_usage();
std::process::exit(2);
}
None => {
print_usage();
Ok(())
}
}
}
fn print_usage() {
eprintln!("knut xtask — workspace task runner");
eprintln!();
eprintln!("tasks:");
eprintln!(" build-wasm-guest build the wasm32 guest for knut-bifrost's WasmTransform host");
eprintln!();
eprintln!("(the nornir bench suite is `cargo bench -p xtask`)");
}
/// The workspace root — the parent of this `xtask` crate's manifest dir.
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask is a workspace member with a parent")
.to_path_buf()
}
/// The `cargo` binary to invoke — honour `$CARGO` (set when we ourselves were
/// launched by cargo) so the toolchain stays consistent, else fall back to
/// `cargo` on `PATH`.
fn cargo_bin() -> String {
std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())
}
/// Build the `knut-bifrost-wasm-guest` crate for `wasm32-unknown-unknown`
/// (release), leaving the artifact where `tests/wasm_guest.rs` expects it:
/// `crates/knut-bifrost/wasm-guest/target/wasm32-unknown-unknown/release/knut_bifrost_wasm_guest.wasm`.
///
/// The guest is excluded from the native workspace (it only builds for wasm32),
/// so this drives it out-of-line via its own manifest. The `wasm32` target is
/// ensured best-effort first (`rustup target add`); if `rustup` is absent we
/// press on and let the build surface a clear error.
fn build_wasm_guest() -> Result<()> {
const TARGET: &str = "wasm32-unknown-unknown";
let root = workspace_root();
let manifest = root.join("crates/knut-bifrost/wasm-guest/Cargo.toml");
if !manifest.exists() {
bail!("wasm-guest manifest not found at {}", manifest.display());
}
ensure_wasm_target(TARGET);
eprintln!("xtask: building knut-bifrost-wasm-guest for {TARGET} (release)…");
let status = Command::new(cargo_bin())
.args([
"build",
"-p",
"knut-bifrost-wasm-guest",
"--target",
TARGET,
"--release",
"--manifest-path",
])
.arg(&manifest)
.status()
.context("failed to spawn cargo for the wasm-guest build")?;
if !status.success() {
bail!(
"wasm-guest build failed (exit {}); is the `{TARGET}` target installed? \
`rustup target add {TARGET}`",
status.code().unwrap_or(-1)
);
}
// The guest carries an empty `[workspace]` table, so its target dir is under
// its own crate dir, not the root target/.
let artifact = manifest
.parent()
.unwrap()
.join("target")
.join(TARGET)
.join("release")
.join("knut_bifrost_wasm_guest.wasm");
verify_artifact(&artifact)?;
eprintln!("xtask: wasm guest built at {}", artifact.display());
eprintln!(
"xtask: run the gated test with:\n {} test -p knut-bifrost --features wasm --test wasm_guest -- --ignored",
cargo_bin()
);
Ok(())
}
/// Best-effort `rustup target add <target>`; a missing `rustup` (or an already
/// installed target) is not fatal — the subsequent build reports the real error.
fn ensure_wasm_target(target: &str) {
match Command::new("rustup")
.args(["target", "add", target])
.status()
{
Ok(s) if s.success() => {}
Ok(_) => eprintln!("xtask: `rustup target add {target}` did not succeed; continuing"),
Err(_) => eprintln!("xtask: `rustup` not found; assuming `{target}` is available"),
}
}
/// Confirm the built artifact exists and is non-empty.
fn verify_artifact(path: &Path) -> Result<()> {
let meta = std::fs::metadata(path)
.with_context(|| format!("wasm artifact missing after build: {}", path.display()))?;
if meta.len() == 0 {
bail!("wasm artifact is empty: {}", path.display());
}
Ok(())
}