rigless 0.1.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Writes what this build calls itself, for `src/runtime/version.rs` to include.

#![expect(
    clippy::disallowed_methods,
    reason = "a build script is told what to build by cargo through its environment"
)]

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

#[path = "src/runtime/version/provenance.rs"]
mod provenance;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo::rustc-check-cfg=cfg(emulator_absent)");
    // `cargo fuzz` builds the crate with this, and the targets under `fuzz/` reach it through it.
    println!("cargo::rustc-check-cfg=cfg(fuzzing)");
    // Miri interprets Rust and can't execute the code the core generates, which is no reason to
    // have no crate.
    if env::var_os("CARGO_CFG_MIRI").is_some() {
        println!("cargo::rustc-cfg=emulator_absent");
    }
    let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("cargo sets it"));
    let out = PathBuf::from(env::var("OUT_DIR").expect("cargo sets it"));
    write_version(&manifest, &out);
}

/// What this build calls itself, for `src/runtime/version.rs` to include: the crate's version, and
/// the source it came from when that isn't a release.
fn write_version(manifest: &Path, out: &Path) {
    let version = env::var("CARGO_PKG_VERSION").expect("cargo sets it");
    let source = source_of(manifest, &version);
    let written = provenance::describe(&version, &source);
    fs::write(out.join("version.rs"), format!("{written:?}")).expect("the version file");
}

/// Where this build's source came from. A package carries cargo's own record of it. A checkout asks
/// git, and watches everything that can change the answer: the commit, the tags, the index, and every
/// file that builds, so an edit is never reported as the commit it was made on. It asks without git's
/// optional locks, or `status` rewrites the index it watches and the next build runs it again.
fn source_of(manifest: &Path, version: &str) -> provenance::Provenance {
    let record = manifest.join(".cargo_vcs_info.json");
    if let Ok(json) = fs::read_to_string(&record) {
        println!("cargo:rerun-if-changed={}", record.display());
        return provenance::from_vcs_info(&json);
    }
    let git = |args: &[&str]| {
        Command::new("git")
            .arg("--no-optional-locks")
            .arg("-C")
            .arg(manifest)
            .args(args)
            .output()
            .ok()
            .filter(|output| output.status.success())
            .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
    };
    let Some(commit) = git(&["rev-parse", "HEAD"]) else {
        return provenance::Provenance::Unknown;
    };
    let mut watched: Vec<String> = ["HEAD", "index", "packed-refs", "refs/tags"]
        .iter()
        .map(|&path| path.to_owned())
        .collect();
    watched.extend(git(&["symbolic-ref", "-q", "HEAD"]));
    for path in watched {
        let Some(found) = git(&["rev-parse", "--git-path", &path]) else {
            continue;
        };
        // A path that doesn't exist would rerun this script on every build, so only what's there.
        let found = manifest.join(found);
        if found.exists() {
            println!("cargo:rerun-if-changed={}", found.display());
        }
    }
    for builds in ["src", "Cargo.toml", "Cargo.lock"] {
        println!("cargo:rerun-if-changed={}", manifest.join(builds).display());
    }
    let changed = git(&[
        "status",
        "--porcelain",
        "--untracked-files=no",
        "--",
        "src",
        "build.rs",
        "Cargo.toml",
        "Cargo.lock",
    ]);
    let dirty = changed.is_none_or(|changes| !changes.is_empty());
    let tags = git(&["tag", "--points-at", "HEAD"]).unwrap_or_default();
    provenance::from_checkout(version, &commit, &tags, dirty)
}