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

nordisk/dwarves

dwarves-gui/build.rs

//! Build script — convert the committed `assets/dwarves.webp` forge logo (two
//! dwarves at the rune-anvil forge, znippy-rabbit hammer) into a PNG in `OUT_DIR`,
//! so the About popup can embed it.
//!
//! Why convert: the shared `facett-about` widget decodes an [`Identity`]'s
//! `logo_png` with a **PNG-only** `image` build (its runtime decode carries no webp
//! feature). We keep the SOURCE asset as the artist's `.webp` (embed the webp), and
//! transcode it once at build time — pure Rust, zero shell (L9) — so the logo
//! actually renders in the card. The transcode is wasm-irrelevant (the About popup
//! is native-only), but a build script runs on the host toolchain regardless, so the
//! PNG is always available to `include_bytes!` from `OUT_DIR`.

use std::path::Path;

fn main() {
    // The build stamp: `dwarves-ui --version` and the About box print
    // `dwarves v<ver> (<git hash>)` so an operator can tell WHICH build is running.
    emit_git_hash();

    // The BUILD RECEIPT — the section `--attest` reads back out of the finished binary.
    emit_build_receipt();

    // ── THE LOGO LIVES INSIDE THIS PACKAGE, and it has to ────────────────────────
    //
    // Until 2026-08-24 this read `../assets/dwarves.webp` — the dwarves repo's shared
    // `assets/` dir, one level up from this crate. Inside the checkout that is fine. In
    // a PUBLISHED crate it does not exist: `cargo package` tarballs only files from
    // under the package root, so `../assets/` resolves outside the extracted crate and
    // the `panic!` below — correctly, loudly — made the published crate unbuildable for
    // everyone who tried to install it. Same defect class as `dwarves-web-client`
    // reading the sibling's proto, and the same fix: a package owns the bytes it needs
    // at build time, because `../` is a path that only exists in a checkout.
    //
    // The repo-root `assets/dwarves.webp` stays where it is (`README.md` renders it), so
    // this is a second copy of 90 KB kept honest by `tests/logo_asset_parity.rs`.
    let webp = Path::new("assets/dwarves.webp");
    println!("cargo:rerun-if-changed=assets/dwarves.webp");

    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by cargo");
    let dest = Path::new(&out_dir).join("dwarves_logo.png");

    match image::open(webp) {
        Ok(img) => {
            img.save_with_format(&dest, image::ImageFormat::Png)
                .expect("re-encode dwarves logo as PNG");
        }
        Err(e) => {
            // Fail LOUD — the About logo is a shipped surface, not optional.
            panic!(
                "dwarves-gui build: could not decode {} as an image: {e}",
                webp.display()
            );
        }
    }
}

/// Emit `cargo:rustc-env=DWARVES_GIT_HASH=<short sha>` — the short git commit the binary
/// was built from — by reading the git metadata as **plain files only**: NO `git`, NO
/// `std::process::Command`, NO shell (the nordisk pure-Rust zero-shell law). Handles a
/// normal `.git` dir, a linked-worktree `.git` FILE (`gitdir:` → the worktree gitdir with
/// its own HEAD but a shared `commondir` for refs), loose refs, and `packed-refs`. Falls
/// back to `unknown` so `env!("DWARVES_GIT_HASH")` always resolves (e.g. a tarball build).
fn emit_git_hash() {
    use std::path::{Path, PathBuf};

    fn find_git_dir(start: &Path) -> Option<PathBuf> {
        let mut cur = Some(start);
        while let Some(d) = cur {
            let g = d.join(".git");
            if g.is_dir() {
                return Some(g);
            }
            if g.is_file() {
                // Linked worktree: `.git` is a file `gitdir: <path>`.
                if let Ok(s) = std::fs::read_to_string(&g) {
                    if let Some(p) = s.lines().find_map(|l| l.strip_prefix("gitdir:")) {
                        return Some(PathBuf::from(p.trim()));
                    }
                }
            }
            cur = d.parent();
        }
        None
    }

    fn resolve(git_dir: &Path) -> Option<String> {
        let head_path = git_dir.join("HEAD");
        let head = std::fs::read_to_string(&head_path).ok()?;
        println!("cargo:rerun-if-changed={}", head_path.display());
        let head = head.trim();

        // Refs live in the COMMON git dir (for a worktree, `commondir` points there;
        // otherwise it is the git dir itself).
        let common = std::fs::read_to_string(git_dir.join("commondir"))
            .ok()
            .map(|s| git_dir.join(s.trim()))
            .and_then(|p| std::fs::canonicalize(&p).ok())
            .unwrap_or_else(|| git_dir.to_path_buf());

        if let Some(reference) = head.strip_prefix("ref:") {
            let reference = reference.trim();
            let loose = common.join(reference);
            if let Ok(s) = std::fs::read_to_string(&loose) {
                println!("cargo:rerun-if-changed={}", loose.display());
                return Some(s.trim().to_string());
            }
            // packed-refs fallback (the loose ref file may not exist).
            let packed_path = common.join("packed-refs");
            println!("cargo:rerun-if-changed={}", packed_path.display());
            let packed = std::fs::read_to_string(&packed_path).ok()?;
            return packed.lines().find_map(|l| {
                let l = l.trim();
                if l.starts_with('#') || l.starts_with('^') {
                    return None;
                }
                let (sha, name) = l.split_once(' ')?;
                (name == reference).then(|| sha.to_string())
            });
        }
        // Detached HEAD: the file already holds the sha.
        Some(head.to_string())
    }

    let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo");
    let hash = find_git_dir(Path::new(&manifest))
        .and_then(|g| resolve(&g))
        .map(|sha| sha.chars().take(12).collect::<String>())
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=DWARVES_GIT_HASH={hash}");
}

/// **Write this artifact's build receipt into `.nordisk.buildthing`.**
///
/// The UI half of the same fix `crates/dwarves/build.rs` carries: the finished binary
/// must CARRY its resolved feature set so `cargo xtask build --attest` can read it back
/// out with an object parser and audit every claim against the symbol table. Before
/// this, `dwarves-ui` built green with no section and `--attest` refused it.
///
/// The recipe hash is mandatory — `emit` refuses a receipt without one, because the
/// version says which recipe was INTENDED and only the hash says which one RAN. It is
/// the content hash of `.nornir/build-variants.toml`, the file that decides this atom's
/// package, bin, features and profile.
///
/// One `build.rs` serves both `dwarves-ui` and `dwarves-solo`, and cargo does not tell a
/// build script which bin it is for. They are separate cargo invocations with different
/// features, so the receipts genuinely differ; only the NAME is ambiguous, so
/// `NORDISK_BUILD_ATOM` is honoured when the builder exports it and the fallback names
/// the crate truthfully rather than guessing a bin.
fn emit_build_receipt() {
    use nornir_build_thing::contract::Coordinate;
    use nornir_build_thing::receipt::{emit, ReceiptBuilder};

    let recipe_path =
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.nornir/build-variants.toml");
    // A PUBLISHED crate has no recipe — the SAME fix `crates/dwarves/build.rs` took in
    // 6a29e0e + 6af3ec2, which was never propagated here. `.nornir/build-variants.toml`
    // lives one level above this manifest in the nordisk checkout; from a registry
    // tarball that path resolves outside the extracted crate and simply is not there.
    // There is no atom, no build matrix and nothing to attest, so there is no receipt to
    // emit either — and a build.rs that panics on its absence makes the published crate
    // unbuildable for everyone. dwarves 0.1.5 failed `cargo publish --verify` on exactly
    // this shape; dwarves-gui carried the identical defect while its `publish = true`
    // flag was already on (4425148).
    //
    // ONLY `NotFound` is tolerated, and that distinction is the whole fix. A recipe that
    // exists but cannot be read — bad permissions, a truncated file, an I/O error — still
    // panics, because inside the workspace that is precisely the "receipt silently
    // vanished" defect this mechanism was built to catch. Swallowing every error here
    // would turn a loud, correct guard into one that can never report red.
    let recipe = match std::fs::read(&recipe_path) {
        Ok(bytes) => bytes,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            // `embed_build_receipt!()` is a bare `include!` (src/main.rs:34, shared by
            // ALL THREE bins — dwarves-ui, dwarves-solo, dwarves-ui-gpu), so the file has
            // to EXIST even when there is no receipt to put in it. Returning early
            // without writing it is the second half of the same bug, and 6af3ec2 is the
            // commit that had to come back and fix it: "the no-recipe branch must still
            // WRITE the receipt file, not just skip". An empty file is the honest
            // content — the receipt lives in a linker section, so no section means "this
            // artifact carries no receipt", which is exactly true of a build with no
            // recipe.
            //
            // The name comes from `receipt::GENERATED_FILE` rather than a string typed
            // here, so this branch cannot drift from the writer it stands in for.
            let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is set in build.rs");
            let path =
                std::path::Path::new(&out_dir).join(nornir_build_thing::receipt::GENERATED_FILE);
            std::fs::write(&path, "// no build recipe: no receipt for this artifact\n")
                .unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
            println!(
                "cargo:warning=no build recipe at {} — building outside the nordisk \
                 workspace (published crate), so this artifact carries no build receipt",
                recipe_path.display()
            );
            return;
        }
        Err(e) => panic!("read the recipe {}: {e}", recipe_path.display()),
    };
    println!("cargo:rerun-if-changed={}", recipe_path.display());

    let build_thing = Coordinate {
        krate: "dwarves-build-thing".to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
        hash: Some(nornir_hash::sha256_hex(&recipe)),
    };
    let atom = std::env::var("NORDISK_BUILD_ATOM")
        .ok()
        .filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "dwarves-ui".to_string());
    let target = std::env::var("TARGET").unwrap_or_default();

    match ReceiptBuilder::from_cargo_env(build_thing, atom, target).and_then(|b| b.build()) {
        Ok(receipt) => {
            if let Err(e) = emit(&receipt) {
                panic!("build receipt refused: {e:#}");
            }
        }
        Err(e) => panic!("build receipt could not be assembled: {e:#}"),
    }
}