ezffi 0.1.1

Generate C-FFI bindings from Rust types/functions via a single proc-macro attribute
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};

fn main() {
    println!("cargo:rerun-if-env-changed=DOCS_RS");
    if env::var("DOCS_RS").is_ok() {
        // docs.rs only renders rustdoc; skip cbindgen header generation,
        // it spawns sub-builds per feature variant and blows past the timeout
        return;
    }

    unsafe { std::env::set_var("RUSTC_BOOTSTRAP", "ezffi") };

    activate_git_hooks();

    let crate_name = env::var("CARGO_PKG_NAME").unwrap();
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_dir = env::var("OUT_DIR").unwrap();

    println!("cargo:rerun-if-changed=cbindgen.toml");
    println!("cargo:rerun-if-changed=src/");

    let target_dir = Path::new(&out_dir)
        .ancestors()
        .nth(3) // target/<PROFILE>
        .expect("Failed to find target dir");

    let include_dir = target_dir.join("include").join(&crate_name);
    fs::create_dir_all(&include_dir).unwrap();

    let config_path = Path::new(&crate_dir).join("cbindgen.toml");
    let base_config =
        cbindgen::Config::from_file(&config_path).expect("Failed to read cbindgen.toml");

    let profile = match env::var("PROFILE").as_deref() {
        Ok("release") => cbindgen::Profile::Release,
        _ => cbindgen::Profile::Debug,
    };

    // Statement-expression trailers go alongside the types they refer to.
    const EZFFI_TRAILER: &str = "\n\
        #define EZFFI_OPTION_UNWRAP(Type, opt_ptr) ({ Type __ez_v; ezffi_option_unwrap((opt_ptr), &__ez_v); __ez_v; })\n\
        #define EZFFI_RESULT_UNWRAP(Type, res_ptr) ({ Type __ez_v; ezffi_result_unwrap((res_ptr), &__ez_v); __ez_v; })\n";
    const STRING_TRAILER: &str = "\n\
        #define EZFFI_STR(lit) ((EzffiStr){ .ptr = (uint8_t*)(lit), .len = sizeof(lit) - 1 })\n\
        #define EZFFI_CSTR(cstr) ((EzffiStr){ .ptr = (uint8_t*)(cstr), .len = strlen(cstr) })\n";
    const SLICE_TRAILER: &str = "\n\
        #define EZFFI_SLICE(p, n) ((EzffiSlice){ .ptr = (void*)(p), .len = (n) })\n\
        #define EZFFI_SLICE_FROM_ARRAY(arr) ((EzffiSlice){ .ptr = (void*)(arr), .len = sizeof(arr) / sizeof((arr)[0]) })\n";

    // One header per module/collection. The cbindgen sub-build for each
    // variant runs with `--no-default-features --features <feature>`, so its
    // expansion contains *only* that module's items and the header is
    // self-contained.
    #[rustfmt::skip]
    const VARIANTS: &[(&str, &str, Option<&str>)] = &[
        ("ezffi.h",        "_option_result", Some(EZFFI_TRAILER)),
        ("string.h",       "_string",        Some(STRING_TRAILER)),
        ("slice.h",        "_slice",         Some(SLICE_TRAILER)),
        ("std/arc.h",      "_arc",           None),
        ("std/btreemap.h", "_btreemap",      None),
        ("std/btreeset.h", "_btreeset",      None),
        ("std/hashmap.h",  "_hashmap",       None),
        ("std/hashset.h",  "_hashset",       None),
        ("std/rc.h",       "_rc",            None),
        ("std/vec.h",      "_vec",           None),
        ("std/vecdeque.h", "_vecdeque",      None),
    ];

    for &(path, feature, variant_trailer) in VARIANTS {
        let mut config = base_config.clone();
        config.trailer = variant_trailer.map(String::from);

        // `EzffiSlice` never shows up in an `extern "C"` signature
        // inside this crate, so cbindgen ignores it
        if feature == "_slice" {
            config.export.include.push("EzffiSlice".to_string());
        }

        let out_path = include_dir.join(path);
        if let Some(parent) = out_path.parent() {
            fs::create_dir_all(parent).unwrap();
        }

        cbindgen::Builder::new()
            .with_crate(&crate_dir)
            .with_config(config)
            .with_parse_expand_profile(profile)
            .with_parse_expand_default_features(false)
            .with_parse_expand_features(&[feature.to_string()])
            .generate()
            .expect("Unable to generate bindings")
            .write_to_file(&out_path);
    }
}

fn activate_git_hooks() {
    let Some(manifest_dir) = env::var("CARGO_MANIFEST_DIR").ok() else {
        return;
    };
    let mut dir = PathBuf::from(manifest_dir);
    let workspace_root = loop {
        if dir.join(".git").exists() {
            break dir;
        }
        if !dir.pop() {
            return;
        }
    };

    let hooks_dir = workspace_root.join(".githooks");
    if !hooks_dir.is_dir() {
        return;
    }

    let _ = Command::new("git")
        .args(["config", "core.hooksPath", ".githooks"])
        .current_dir(&workspace_root)
        .status()
        .ok();
}