Gunnar

mikronova/draupnir

crates/draupnir-build-thing/build.rs

//! The recipe's content hash — sha256 over every `src/**/*.rs`, in sorted path
//! order, `path \0 len \0 bytes` per file — exported as
//! `DRAUPNIR_BUILD_THING_CONTENT_HASH` for `recipe::CONTENT_HASH`. The same shape
//! tunnr's, gunnar's and korp's recipe crates use: a `Coordinate` is
//! `krate:version:hash`, and the hash is what tells two builds of the same
//! version apart.

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

use sha2::{Digest, Sha256};

const HASH_VAR: &str = "DRAUPNIR_BUILD_THING_CONTENT_HASH";

fn main() {
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR");
    let src = Path::new(&manifest_dir).join("src");

    let mut files = Vec::new();
    collect_rs(&src, &mut files);
    files.sort();
    assert!(
        !files.is_empty(),
        "no *.rs found under {} — the recipe would hash the empty string and every \
         version of it would carry the same content hash",
        src.display()
    );

    let mut hasher = Sha256::new();
    for path in &files {
        let rel = path
            .strip_prefix(&src)
            .expect("collected under src/")
            .to_string_lossy()
            .replace('\\', "/");
        let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
        hasher.update(rel.as_bytes());
        hasher.update(b"\0");
        hasher.update(bytes.len().to_le_bytes());
        hasher.update(b"\0");
        hasher.update(&bytes);
        println!("cargo:rerun-if-changed={}", path.display());
    }
    let digest = hasher.finalize();
    let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
    println!("cargo:rustc-env={HASH_VAR}={hex}");
}

fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_rs(&path, out);
        } else if path.extension().is_some_and(|e| e == "rs") {
            out.push(path);
        }
    }
}