vyre-build-scan 0.1.0

Build-time filesystem scanner for flat trait-impl registries
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

#[test]
fn scan_output_is_included_by_consumer_crate() {
    let fixture = temp_fixture("include_flow");
    create_fixture_crate(&fixture);

    let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
    let output = Command::new(cargo)
        .arg("test")
        .arg("--manifest-path")
        .arg(fixture.join("Cargo.toml"))
        .arg("--target-dir")
        .arg(fixture.join("target"))
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "fixture cargo test failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        !fixture.join("src/gates_registry.rs").exists(),
        "registry must not be written into src/"
    );

    let generated = find_file(&fixture.join("target"), "gates_registry.rs");
    let generated = generated.expect("generated registry should exist under target OUT_DIR");
    let contents = fs::read_to_string(generated).unwrap();
    assert_eq!(
        registry_entries(&contents),
        [
            "&crate::gates::alpha::REGISTERED,",
            "&crate::gates::beta::REGISTERED,",
            "&crate::gates::gamma::REGISTERED,"
        ],
        "generated registry should expose exactly the three fixture REGISTERED consts"
    );
    assert!(
        contents.contains("pub static ALL_GATES: &[&'static dyn crate::gates::Gate]"),
        "generated registry should preserve the requested ALL_GATES slice type"
    );

    let _ = fs::remove_dir_all(fixture);
}

fn create_fixture_crate(root: &Path) {
    let build_scan_path = Path::new(env!("CARGO_MANIFEST_DIR"));
    fs::create_dir_all(root.join("src/gates")).unwrap();
    fs::write(
        root.join("Cargo.toml"),
        format!(
            r#"[package]
name = "vyre-build-scan-include-fixture"
version = "0.0.0"
edition = "2021"

[build-dependencies]
vyre-build-scan = {{ path = "{}" }}
"#,
            build_scan_path.display()
        ),
    )
    .unwrap();
    fs::write(
        root.join("build.rs"),
        r#"fn main() {
    vyre_build_scan::scan(&vyre_build_scan::Registry {
        scan_dir: "src/gates",
        const_name: "ALL_GATES",
        element_type: "&'static dyn crate::gates::Gate",
        item_const_name: "REGISTERED",
        output_file: "gates_registry.rs",
        module_prefix: "crate::gates",
    });
}
"#,
    )
    .unwrap();
    fs::write(
        root.join("src/lib.rs"),
        r#"pub mod gates;

#[cfg(test)]
mod tests {
    use super::gates::ALL_GATES;

    #[test]
    fn all_gates_shape_is_stable() {
        let _: &[&'static dyn crate::gates::Gate] = ALL_GATES;
        let names: Vec<&'static str> = ALL_GATES.iter().map(|gate| gate.name()).collect();
        assert_eq!(names, ["alpha", "beta", "gamma"]);
    }
}
"#,
    )
    .unwrap();
    fs::write(
        root.join("src/gates/mod.rs"),
        r#"pub trait Gate: Sync {
    fn name(&self) -> &'static str;
}

pub mod alpha;
pub mod beta;
pub mod gamma;

include!(concat!(env!("OUT_DIR"), "/gates_registry.rs"));
"#,
    )
    .unwrap();
    write_gate(root, "alpha", "Alpha");
    write_gate(root, "beta", "Beta");
    write_gate(root, "gamma", "Gamma");
}

fn write_gate(root: &Path, module: &str, ty: &str) {
    fs::write(
        root.join(format!("src/gates/{module}.rs")),
        format!(
            r#"pub struct {ty};

impl crate::gates::Gate for {ty} {{
    fn name(&self) -> &'static str {{
        "{module}"
    }}
}}

pub const REGISTERED: {ty} = {ty};
"#
        ),
    )
    .unwrap();
}

fn registry_entries(contents: &str) -> Vec<&str> {
    contents
        .lines()
        .map(str::trim)
        .filter(|line| line.starts_with("&crate::gates::"))
        .collect()
}

fn find_file(root: &Path, name: &str) -> Option<PathBuf> {
    let mut pending = vec![root.to_path_buf()];
    while let Some(dir) = pending.pop() {
        for entry in fs::read_dir(dir).ok()? {
            let entry = entry.ok()?;
            let path = entry.path();
            if path.is_dir() {
                pending.push(path);
            } else if path.file_name().and_then(|file| file.to_str()) == Some(name) {
                return Some(path);
            }
        }
    }
    None
}

fn temp_fixture(suffix: &str) -> PathBuf {
    let mut base = env::temp_dir();
    base.push(format!("vyre-build-scan-{}-{}", std::process::id(), suffix));
    let _ = fs::remove_dir_all(&base);
    base
}