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

nordisk/gunnar

xtask/src/cli_stage.rs

//! **A front medium carries the SIGNED Gunnar command-line tool its site offers under
//! `/download/`** (E4, owner 2026-10-11) — this stages it before the compose.
//!
//! For a front medium (`gunnar-front-installer*`), before the payload is built:
//!
//! 1. the CLI is built and SIGNED through the Build Thing: this checkout's row
//!    `gunnar-cli-musl` (the product's program `gunnar-cli`, x86_64 musl, `sign = {
//!    purpose = "plugin" }`; lane GUN-PRODUCTS, 2026-10-30, it was the client-only
//!    `gunnar` of the retired row `gunnar-musl`) — the forge writes `gunnar-cli.p7s` +
//!    `gunnar-cli.blake3` beside it on the host, verified against the root before it
//!    reports green;
//! 2. the binary, its two sidecars and the published trust root
//!    (`<dwarves>/trust/korp-root-ca.der`) are copied to `<gunnar>/.cli-out/`,
//!    where gunnar-front's `build.rs` embeds them. They are staged under the names
//!    the front embeds ([`STAGED`]: `gunnar`, `gunnar.p7s`, `gunnar.blake3`; served as
//!    `gunnar-<triple>`), so the download's URL did not move.
//!
//! A front medium is not composed without it: a site that cannot hand a
//! customer the CLI leaves every encrypted flow without its tool.

use std::path::{Path, PathBuf};
use std::process::Command;

/// Where gunnar-front's `build.rs` looks for the staged CLI.
pub const STAGE_DIR: &str = ".cli-out";
/// This checkout's Build Thing row that builds and signs it.
pub const ROW: &str = "gunnar-cli-musl";
/// The row's target.
pub const TRIPLE: &str = "x86_64-unknown-linux-musl";
/// The binary's file name, as the row builds it.
pub const BIN: &str = "gunnar-cli";
/// The name it is staged under, the one gunnar-front's `build.rs` embeds.
pub const STAGED: &str = "gunnar";
/// The published trust root's file name.
pub const ROOT_CA: &str = "korp-root-ca.der";

/// Whether a variant's medium serves the CLI: the front media only.
pub fn carries_cli(variant: &str) -> bool {
    variant.starts_with("gunnar-front-installer")
}

/// Where the Build Thing leaves the row's artifacts: `CARGO_TARGET_DIR`, else
/// `<gunnar>/target` (nornir-build-thing `HostPaths::from_env`).
pub fn built_at(gunnar_root: &Path) -> PathBuf {
    std::env::var_os("CARGO_TARGET_DIR")
        .filter(|v| !v.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| gunnar_root.join("target"))
        .join(TRIPLE)
        .join("release")
}

/// **Copy the signed CLI and the root into `dir`** — refused by name when the
/// binary, its signature or its digest is absent (unsigned is never carried).
/// The same bytes already staged are left as they are, so an unchanged CLI does
/// not re-run gunnar-front's build script.
pub fn stage_from(from: &Path, root_ca: &Path, dir: &Path) -> Result<PathBuf, String> {
    std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
    let mut copies: Vec<(PathBuf, PathBuf)> = ["", ".p7s", ".blake3"]
        .into_iter()
        .map(|ext| {
            (
                from.join(format!("{BIN}{ext}")),
                dir.join(format!("{STAGED}{ext}")),
            )
        })
        .collect();
    copies.push((root_ca.to_path_buf(), dir.join(ROOT_CA)));
    for (src, _) in &copies {
        if !src.is_file() {
            return Err(format!(
                "REFUSED [cli-not-built] no {} — the `{ROW}` row is unbuilt or unsigned, and a front medium is not composed \
                 without the signed CLI its site serves",
                src.display()
            ));
        }
    }
    for (src, dst) in &copies {
        if std::fs::read(src)
            .ok()
            .is_some_and(|b| std::fs::read(dst).ok().as_deref() == Some(b.as_slice()))
        {
            continue;
        }
        let tmp = dst.with_extension("staging");
        std::fs::copy(src, &tmp)
            .map_err(|e| format!("{} → {}: {e}", src.display(), tmp.display()))?;
        std::fs::rename(&tmp, dst)
            .map_err(|e| format!("{} → {}: {e}", tmp.display(), dst.display()))?;
    }
    Ok(dir.to_path_buf())
}

/// **Build, sign and stage** the CLI for a front medium.
pub fn stage(gunnar_root: &Path, dwarves: &Path, cargo: &str) -> Result<PathBuf, String> {
    eprintln!(
        "iso-installer: cli     building + signing `{ROW}` from {}",
        gunnar_root.display()
    );
    let mut cmd = Command::new(cargo);
    cmd.current_dir(dwarves);
    cmd.env_remove("CARGO_MANIFEST_DIR");
    cmd.args([
        "run",
        "-p",
        "dwarves",
        "--features",
        "appliance-iso,sign-step",
        "--bin",
        "dwarves",
        "--",
        "buildthing",
    ])
    .arg(gunnar_root)
    .args(["--variant", ROW]);
    let status = cmd.status().map_err(|e| format!("spawn {cargo}: {e}"))?;
    if !status.success() {
        return Err(format!("REFUSED [cli-not-built] the Build Thing did not build and sign `{ROW}` (exit {status}); a front medium is not composed without it"));
    }
    let dir = stage_from(
        &built_at(gunnar_root),
        &dwarves.join("trust").join(ROOT_CA),
        &gunnar_root.join(STAGE_DIR),
    )?;
    let digest = std::fs::read_to_string(dir.join(format!("{STAGED}.blake3"))).unwrap_or_default();
    eprintln!(
        "iso-installer: cli     staged {} (blake3 {})",
        dir.join(STAGED).display(),
        digest.trim()
    );
    Ok(dir)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn only_the_front_media_carry_the_cli() {
        for v in [
            "gunnar-front-installer-open",
            "gunnar-front-installer-metered",
        ] {
            assert!(carries_cli(v), "{v}");
        }
        for v in [
            "gunnar-installer-open",
            "gunnar-installer",
            "gunnar-installer-metered",
            "gunnar-installer-metered-aws",
        ] {
            assert!(!carries_cli(v), "{v}");
        }
    }

    fn scratch(name: &str) -> PathBuf {
        let d = std::env::temp_dir().join(format!("cli-stage-{name}-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&d);
        std::fs::create_dir_all(&d).unwrap();
        d
    }

    #[test]
    fn the_signed_cli_and_the_root_are_staged_where_the_front_embeds_them() {
        let d = scratch("ok");
        let (from, root, out) = (
            d.join("release"),
            d.join("trust").join(ROOT_CA),
            d.join("gunnar").join(STAGE_DIR),
        );
        std::fs::create_dir_all(&from).unwrap();
        std::fs::create_dir_all(root.parent().unwrap()).unwrap();
        for (n, b) in [
            ("gunnar-cli", "elf"),
            ("gunnar-cli.p7s", "cms"),
            ("gunnar-cli.blake3", "abc\n"),
        ] {
            std::fs::write(from.join(n), b).unwrap();
        }
        std::fs::write(&root, "root").unwrap();
        assert_eq!(stage_from(&from, &root, &out).unwrap(), out);
        for (n, b) in [
            ("gunnar", "elf"),
            ("gunnar.p7s", "cms"),
            ("gunnar.blake3", "abc\n"),
            (ROOT_CA, "root"),
        ] {
            assert_eq!(std::fs::read_to_string(out.join(n)).unwrap(), b, "{n}");
        }
        let _ = std::fs::remove_dir_all(&d);
    }

    #[test]
    fn an_unsigned_cli_is_refused_by_name_and_not_staged() {
        let d = scratch("unsigned");
        let (from, root, out) = (d.join("release"), d.join(ROOT_CA), d.join(STAGE_DIR));
        std::fs::create_dir_all(&from).unwrap();
        std::fs::write(from.join("gunnar-cli"), "elf").unwrap();
        std::fs::write(from.join("gunnar-cli.blake3"), "abc").unwrap();
        std::fs::write(&root, "root").unwrap();
        let e = stage_from(&from, &root, &out).unwrap_err();
        assert!(
            e.contains("[cli-not-built]") && e.contains("gunnar-cli.p7s"),
            "{e}"
        );
        assert!(
            !out.join("gunnar").exists(),
            "nothing staged without its signature"
        );
        let _ = std::fs::remove_dir_all(&d);
    }

    #[test]
    fn the_rows_artifacts_are_read_from_the_musl_release_dir() {
        if std::env::var_os("CARGO_TARGET_DIR").is_none() {
            assert_eq!(
                built_at(Path::new("/g")),
                PathBuf::from("/g/target/x86_64-unknown-linux-musl/release")
            );
        }
        assert!(built_at(Path::new("/g")).ends_with("x86_64-unknown-linux-musl/release"));
    }
}