razeri-plugin-api 0.19.0

Extension traits and registries for Razeri plugins
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! Records how this crate is being built: the compiler, and a fingerprint of
//! this crate's source.
//!
//! A plugin loaded from a directory shares Rust types with the razeri that
//! loads it, and the two lay those types out alike only when they are the
//! same build. Both sides compile this crate, so what it records here is what
//! each side declares, and the loader compares the two before it calls into a
//! plugin. A crate's version moves when it is published, not every time its
//! source does, which is why the source is fingerprinted as well. The number
//! is read from the files alone, so the same source gives the same number on
//! every machine.

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

fn main() {
    println!("cargo::rerun-if-changed=src");
    let root = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("set by cargo"));
    let fingerprint = source_fingerprint(&root.join("src"));
    println!("cargo::rustc-env=RAZERI_PLUGIN_API_SOURCE={fingerprint:016x}");
    println!("cargo::rustc-env=RAZERI_PLUGIN_API_COMPILER={}", compiler());
}

/// The compiler cargo builds this crate with, as `rustc -V` names it: its
/// version, its commit and its date.
fn compiler() -> String {
    let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
    let output = Command::new(&rustc)
        .arg("-V")
        .output()
        .unwrap_or_else(|e| panic!("cannot ask {} for its version: {e}", rustc.display()));
    let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
    assert!(
        output.status.success() && !version.is_empty(),
        "{} -V answered nothing, so this build cannot say which compiler made it",
        rustc.display()
    );
    version
}

/// FNV-1a over every file under `dir`, in the order of their paths relative
/// to it, each path followed by the file's bytes with carriage returns left
/// out, so a checkout that ends its lines with `\r\n` fingerprints the same.
fn source_fingerprint(dir: &Path) -> u64 {
    let mut files = Vec::new();
    collect(dir, dir, &mut files);
    files.sort();
    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    let mut feed = |bytes: &[u8]| {
        for &byte in bytes {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
        }
    };
    for (relative, path) in files {
        feed(relative.as_bytes());
        feed(&[0]);
        let contents = std::fs::read(&path)
            .unwrap_or_else(|e| panic!("cannot read {} to fingerprint it: {e}", path.display()));
        let without_returns: Vec<u8> = contents.into_iter().filter(|&b| b != b'\r').collect();
        feed(&without_returns);
        feed(&[0]);
    }
    hash
}

/// Every file under `dir`, as its path relative to `root` spelled with `/`,
/// beside its full path.
fn collect(root: &Path, dir: &Path, files: &mut Vec<(String, PathBuf)>) {
    let entries = std::fs::read_dir(dir)
        .unwrap_or_else(|e| panic!("cannot read {} to fingerprint it: {e}", dir.display()));
    for entry in entries {
        let path = entry.expect("a directory entry").path();
        if path.is_dir() {
            collect(root, &path, files);
        } else {
            let relative = path.strip_prefix(root).expect("under the root");
            let spelled: Vec<String> = relative
                .components()
                .map(|c| c.as_os_str().to_string_lossy().into_owned())
                .collect();
            files.push((spelled.join("/"), path));
        }
    }
}