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

nordisk/holger

xtask/src/appliance_pack.rs

//! **`appliance-pack`** — pack a built holger server binary into the
//! app-`.znippy` the appliance boot chain execs, plus its blake3 pin file.
//!
//! This is the packer the installer-cell wall report named as holger's missing
//! input #2 — and it is deliberately THIN: the archive writer is the shared
//! znippy codec (`znippy_common::create_archive_to_vec`, the exact function
//! `korp_installer::payload::pack_znippy` wraps), and the entry name is the
//! `korp.entry=` selector the boot chain resolves (`for_product("holger")`
//! bakes `korp.entry=holger`; `korp-start` does `get_file(archive, entry)` and
//! `memfd`+`fexecve`s the result). One codec, one entry contract, no dialect.
//!
//! Outputs, named for the installer medium's vocabulary:
//!
//! ```text
//!   <out>/holger-app.znippy   the app payload (streamed raw onto P2 at install)
//!   <out>/holger-app.blake3   blake3 hex of the ARCHIVE bytes — the pin the
//!                             ESP carries and every boot re-checks
//! ```
//!
//! The verdict is APPLIED OUTPUT: after writing, the archive is read back
//! through `get_file` and the extracted entry must be byte-identical to the
//! input binary — the packer proves its own product, never just its exit code.
//!
//! Honest wall, stated where the operator will meet it: the OS-less installed
//! shape execs the entry with NO loader and NO /lib, so a production payload
//! must be STATICALLY linked. The packer warns (loudly, by PT_INTERP) rather
//! than refusing, because the container/lab shapes run a dynamic payload today
//! — but a dynamic binary on real metal refuses at exec, which is why the
//! warning is the loud kind.
//!
//! The musl wall this paragraph used to describe HAS FALLEN (2026-09-17). It
//! said holger's musl-static build was blocked by openzl-sys-rs's cross-C++
//! need and cited `.nornir/build-variants.toml` as the measurement. That
//! citation was dangling: the emitted declaration names no wall and no
//! openzl — it declares `mvp-server-musl` as a live variant
//! (`target = "x86_64-unknown-linux-musl"`) and the installer row consumes it
//! by name, as `installer_iso.rs` already says ("the STATIC `mvp-server-musl`
//! payload"). A stale wall in a doc comment is worse than no comment: it tells
//! the next operator to stop reaching for the thing that now works.

use std::path::PathBuf;

use anyhow::{bail, Context, Result};

/// Defaults: the MVP gateway binary, the entry `for_product("holger")` bakes.
const DEFAULT_BIN: &str = "target/release/holger-mvp";
const DEFAULT_ENTRY: &str = "holger";
const DEFAULT_OUT: &str = ".znippy-out";
/// zstd level for the release artifact — dense over fast (the archive is
/// written once and streamed to disks many times).
const DEFAULT_LEVEL: i32 = 19;

/// Does this ELF ask for an interpreter (`PT_INTERP`)? A dynamically linked
/// payload cannot exec on the OS-less installed shape.
fn wants_interpreter(elf: &[u8]) -> bool {
    // ELF64: e_phoff at 0x20 (u64), e_phentsize at 0x36 (u16), e_phnum at 0x38
    // (u16); a program header's p_type is its first u32; PT_INTERP = 3.
    if elf.len() < 0x40 || &elf[..4] != b"\x7fELF" {
        return false;
    }
    let phoff = u64::from_le_bytes(elf[0x20..0x28].try_into().unwrap()) as usize;
    let phentsize = u16::from_le_bytes(elf[0x36..0x38].try_into().unwrap()) as usize;
    let phnum = u16::from_le_bytes(elf[0x38..0x3a].try_into().unwrap()) as usize;
    (0..phnum).any(|i| {
        let off = phoff + i * phentsize;
        elf.get(off..off + 4)
            .map(|t| u32::from_le_bytes(t.try_into().unwrap()) == 3)
            .unwrap_or(false)
    })
}

/// Run the verb. Args: `--bin <path>` `--entry <name>` `--out <dir>`
/// `--level <zstd>` — every default spelled above.
pub fn run() -> Result<()> {
    let mut bin = PathBuf::from(DEFAULT_BIN);
    let mut entry = DEFAULT_ENTRY.to_string();
    let mut out_dir = PathBuf::from(DEFAULT_OUT);
    let mut level = DEFAULT_LEVEL;
    let mut args = std::env::args().skip(2);
    while let Some(a) = args.next() {
        let mut val = |flag: &str| args.next().with_context(|| format!("{flag} takes a value"));
        match a.as_str() {
            "--bin" => bin = PathBuf::from(val("--bin")?),
            "--entry" => entry = val("--entry")?,
            "--out" => out_dir = PathBuf::from(val("--out")?),
            "--level" => level = val("--level")?.parse().context("--level is a zstd level")?,
            other => bail!(
                "unknown argument '{other}' (appliance-pack takes --bin/--entry/--out/--level)"
            ),
        }
    }

    let bytes = std::fs::read(&bin).with_context(|| {
        // ⚠ THE ADVICE NAMES THE DECLARATION, NOT A FEATURE LIST (2026-09-21).
        //
        // It used to spell `--features mvp-server,appliance`, which is a SUBSET
        // of the declared row and produces a payload that serves NO FORMATS —
        // the appliance then dies at first boot with `holger-mvp-1` and, being
        // PID 1, takes the machine with it. Measured through the fleet proof,
        // which had copied the same short list. A build line in a message is a
        // second writer like any other; this one now points at the one writer.
        format!(
            "read {} — build it first through the declaration: \
             `cargo xtask build --suite MVP --variant mvp-server-musl`, then pass the result \
             with --bin. Do NOT hand-spell a feature list: the declared row carries \
             auth-passkey, sign, allow-meter, the twelve fmt-* formats, sync and jobs \
             beside mvp-server+appliance, and a payload missing the fmt-* features serves no \
             repository at all",
            bin.display()
        )
    })?;
    if wants_interpreter(&bytes) {
        eprintln!(
            "⚠ {} is DYNAMICALLY linked (PT_INTERP present): it packs and verifies, but the \
             OS-less installed shape has no loader and will refuse it at exec — the musl wall \
             recorded in .nornir/build-variants.toml is what stands between this payload and \
             real metal",
            bin.display()
        );
    }

    let (archive, _report) =
        znippy_common::create_archive_to_vec(&[(entry.clone(), bytes.clone())], level)
            .context("pack the app .znippy")?;
    std::fs::create_dir_all(&out_dir).with_context(|| format!("create {}", out_dir.display()))?;
    let archive_path = out_dir.join("holger-app.znippy");
    let pin_path = out_dir.join("holger-app.blake3");
    std::fs::write(&archive_path, &archive)
        .with_context(|| format!("write {}", archive_path.display()))?;
    let pin = blake3::hash(&archive).to_hex().to_string();
    std::fs::write(&pin_path, format!("{pin}\n"))
        .with_context(|| format!("write {}", pin_path.display()))?;

    // APPLIED OUTPUT: the archive on disk gives the binary back, byte for byte.
    let roundtrip = znippy_common::get_file(&archive_path, &entry).with_context(|| {
        format!(
            "read the entry '{entry}' back out of {}",
            archive_path.display()
        )
    })?;
    if roundtrip != bytes {
        bail!(
            "the packed entry read back {} bytes but the input was {} — refusing to bless \
             an archive that does not return its payload",
            roundtrip.len(),
            bytes.len()
        );
    }

    println!(
        "✅ appliance-pack: {} ({} bytes, entry '{entry}') → {} ({} bytes, zstd {level})",
        bin.display(),
        bytes.len(),
        archive_path.display(),
        archive.len(),
    );
    println!("   pin {} → {}", &pin[..12], pin_path.display());
    Ok(())
}