xtask/src/wasm_plugins.rs
//! `cargo xtask build-wasm-plugins` — **a door onto holger's declared wasm guest
//! rows**, and the only place that decides where a built module lives.
//!
//! | question | answer |
//! |---|---|
//! | which crates? | **the recipe's `kind = "wasm"` rows** (`holger_recipe::declared()`: `plugin-allocation`, `plugin-skidbladnir`, `plugin-rust-toolchain`). This file names no package — until 2026-09-27 it kept its own list, a second spelling of the build beside a recipe that had no row for them at all. |
//! | where is it built? | in the declared toolchain container, through the SAME `assemble_one_tier` / `build_variant_in_container` road `cargo xtask build` takes. Never host cargo. |
//! | where does cargo put it? | `<target>/wasm32-unknown-unknown/release/<crate>.wasm`, `<target>` = `CARGO_TARGET_DIR` else `<repo>/target` (`HostPaths::from_env`). Nothing is copied. |
//! | is it signed? | **not by this verb.** The signing key never enters a build container and xtask holds none; the SIGNED module (`.wasm.p7s` + `.wasm.blake3`, what a trust-required host loads) is `dwarves buildthing ./holger --suite MVP --variant <row>`. This verb is for the agreement guards and the gate's rescue arms, which load unsigned. |
//! | is it committed? | **no.** A committed binary would be the one copy nothing rebuilds. |
//!
//! `--list` prints the rows, crates and target paths without building.
use std::path::{Path, PathBuf};
use nornir_build_thing::variant::ArtifactKind;
/// The wasm target every module is built for.
pub const TARGET: &str = "wasm32-unknown-unknown";
/// `(row, package)` for every `kind = "wasm"` row holger's recipe declares —
/// READ off the declaration, never restated here.
pub fn wasm_rows() -> Vec<(String, String)> {
holger_recipe::declared()
.variants
.into_iter()
.filter(|v| v.kind == ArtifactKind::Wasm)
.map(|v| {
let pkg = v
.package
.clone()
.unwrap_or_else(|| panic!("wasm row '{}' names no package", v.name));
(v.name, pkg)
})
.collect()
}
/// Cargo's output path for a crate's module, relative to the target dir.
pub fn module_rel_path(crate_name: &str) -> PathBuf {
Path::new(TARGET)
.join("release")
.join(format!("{}.wasm", crate_name.replace('-', "_")))
}
/// Where cargo will write — the same rule the container road binds.
fn target_dir(repo_root: &Path) -> PathBuf {
nornir_build_thing::cargo_container::HostPaths::from_env(repo_root).target_dir
}
fn repo_root() -> PathBuf {
// xtask/.. is the holger repo root.
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask/.. is the holger repo root")
.to_path_buf()
}
/// Print the declared wasm rows and where each module lands, without building.
pub fn list() {
let root = repo_root();
let target = target_dir(&root);
let rows = wasm_rows();
println!("wasm guest rows ({}), from holger-recipe:", rows.len());
for (row, pkg) in &rows {
println!(
" {row:<22} {pkg}\n → {}",
target.join(module_rel_path(pkg)).display()
);
}
println!(
"\nBuild them (container, unsigned) with:\n cargo xtask build-wasm-plugins\n\
Build them SIGNED with:\n dwarves buildthing ./holger --suite MVP --variant <row>"
);
}
/// Build every declared wasm row in the toolchain container, then **assert each
/// module actually exists**.
///
/// The existence check is not belt-and-braces: a build succeeding does not prove
/// a `.wasm` was produced — a crate whose `[lib] crate-type` lost `cdylib`
/// builds green and emits nothing. So the artifact is stat'd by name.
pub fn run() -> anyhow::Result<()> {
let root = repo_root();
let rows = wasm_rows();
anyhow::ensure!(
!rows.is_empty(),
"holger-recipe declares no kind = \"wasm\" row — nothing to build"
);
println!(
"=== building {} declared wasm guest row(s) in the toolchain container ===",
rows.len()
);
let road = crate::cargo_road(&root)?;
let mut failed = Vec::new();
for (row, _) in &rows {
match crate::assemble_one_tier(&road, row, &root) {
Ok(true) => {}
Ok(false) => failed.push(row.clone()),
Err(e) => failed.push(format!("{row}: {e:#}")),
}
}
anyhow::ensure!(
failed.is_empty(),
"wasm guest row(s) failed to build: {}",
failed.join("; ")
);
let target = target_dir(&root);
let mut missing = Vec::new();
for (row, pkg) in &rows {
let path = target.join(module_rel_path(pkg));
match std::fs::metadata(&path) {
Ok(m) if m.len() > 0 => println!(
" ✅ {row} ({pkg}) → {} ({} bytes)",
path.display(),
m.len()
),
Ok(_) => missing.push(format!("{} is ZERO bytes", path.display())),
Err(e) => missing.push(format!("{} is missing: {e}", path.display())),
}
}
if !missing.is_empty() {
anyhow::bail!(
"the build succeeded but produced no usable module:\n {}\n\
Check that each crate declares `[lib] crate-type = [\"cdylib\"]`.",
missing.join("\n ")
);
}
println!(
"\n✅ {} module(s) built under {} (unsigned — the signed road is dwarves buildthing)",
rows.len(),
target.join(TARGET).display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// The crate name → module file name mapping is what every error message and
/// every agreement guard hardcodes. Assert it as data.
#[test]
fn a_crate_name_maps_to_the_module_file_the_guards_look_for() {
assert_eq!(
module_rel_path("holger-handler-skidbladnir-wasm"),
Path::new("wasm32-unknown-unknown/release/holger_handler_skidbladnir_wasm.wasm")
);
assert_eq!(
module_rel_path("holger-handler-rust-toolchain-wasm"),
Path::new("wasm32-unknown-unknown/release/holger_handler_rust_toolchain_wasm.wasm")
);
}
/// Every `plugin/*-wasm` crate in the workspace has a declared, SIGNED row —
/// a guest crate added without one would be built by nothing that signs it.
#[test]
fn every_wasm_guest_crate_has_a_signed_row() {
let rows = wasm_rows();
let set = holger_recipe::declared();
for (row, _) in &rows {
assert!(
set.get(row).unwrap().sign.is_some(),
"wasm row '{row}' is not signed"
);
}
let plugin = repo_root().join("plugin");
let mut crates: Vec<String> = std::fs::read_dir(&plugin)
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().ends_with("-wasm"))
.map(|e| {
let toml = std::fs::read_to_string(e.path().join("Cargo.toml")).unwrap();
toml.lines()
.find_map(|l| {
l.strip_prefix("name = \"")
.map(|r| r.trim_end_matches('"').to_string())
})
.unwrap()
})
.collect();
crates.sort();
let mut declared: Vec<String> = rows.into_iter().map(|(_, p)| p).collect();
declared.sort();
assert_eq!(
crates, declared,
"plugin/*-wasm crates vs the recipe's wasm rows"
);
}
}