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