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

nordisk/holger

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"
        );
    }
}