js2rust-bridge 0.1.0

FFI bridge runtime for js2rust-generated Zig code
// build.rs for js2rust-bridge crate
//
// Responsibilities:
// 1. Run `zig build` on all generated Zig projects under out/
// 2. Inform Cargo about the static libraries to link
//
// The FFI bindings are generated by js2rust-bridge-macro (proc-macro),
// NOT by this build script.

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

fn main() {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let ws_root = manifest_dir.parent().unwrap();
    let out_dir = ws_root.join("out");

    // Collect all group directories under out/
    let groups = collect_groups(&out_dir);

    if groups.is_empty() {
        println!(
            "cargo:warning=No Zig projects found under {}/out/ — run `cargo run -p js2rustc` first",
            ws_root.display()
        );
        return;
    }

    // Build each Zig project and collect lib paths
    let mut lib_dirs = Vec::new();

    for group in &groups {
        let zig_project_dir = out_dir.join(group);

        // Re-run if any Zig source changes
        println!(
            "cargo:rerun-if-changed={}",
            zig_project_dir.join("src").to_string_lossy()
        );
        println!(
            "cargo:rerun-if-changed={}",
            zig_project_dir.join("cabi_exports.json").to_string_lossy()
        );

        // Run zig build
        let status = Command::new("zig")
            .args(["build", "-Doptimize=ReleaseSafe"])
            .current_dir(&zig_project_dir)
            .status();

        match status {
            Ok(s) if s.success() => {
                // Find the compiled .lib
                if let Some(lib_dir) = find_static_lib_dir(&zig_project_dir, group) {
                    lib_dirs.push(lib_dir);
                }
            }
            Ok(_) => {
                eprintln!(
                    "cargo:warning=zig build failed for group '{}'",
                    group
                );
            }
            Err(e) => {
                eprintln!(
                    "cargo:warning=Failed to run zig build for group '{}': {}",
                    group, e
                );
            }
        }
    }

    // Deduplicate lib dirs and emit link directives
    let mut emitted_dirs = std::collections::HashSet::new();
    for lib_dir in &lib_dirs {
        let lib_dir_str = lib_dir.to_string_lossy().to_string();
        if emitted_dirs.insert(lib_dir_str.clone()) {
            println!("cargo:rustc-link-search=native={}", lib_dir_str);
        }
    }

    // Link each group's static library
    for group in &groups {
        println!("cargo:rustc-link-lib=static={group}");
    }

    // NT API symbols (ntdll.lib provides LdrRegisterDllNotification etc.)
    println!("cargo:rustc-link-lib=ntdll");
}

/// Collect all group directories under out/ that have a build.zig file.
fn collect_groups(out_dir: &Path) -> Vec<String> {
    let mut groups = Vec::new();

    if let Ok(entries) = fs::read_dir(out_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir()
                && path.join("build.zig").exists()
                && let Some(name) = path.file_name().and_then(|n| n.to_str())
            {
                groups.push(name.to_string());
            }
        }
    }

    groups
}

/// Locate the directory containing the compiled static library for a group.
fn find_static_lib_dir(zig_project_dir: &Path, lib_name: &str) -> Option<PathBuf> {
    let lib_filename = format!("{lib_name}.lib");
    let zig_out = zig_project_dir.join("zig-out").join("lib");
    if zig_out.is_dir() && has_static_lib(&zig_out, &lib_filename) {
        return Some(zig_out);
    }
    let zig_cache = zig_project_dir.join(".zig-cache").join("lib");
    if zig_cache.is_dir() && has_static_lib(&zig_cache, &lib_filename) {
        return Some(zig_cache);
    }
    search_for_static_lib(zig_project_dir, &lib_filename)
}

fn has_static_lib(dir: &Path, filename: &str) -> bool {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            if entry.file_name().to_string_lossy() == filename {
                return true;
            }
        }
    }
    false
}

fn search_for_static_lib(dir: &Path, filename: &str) -> Option<PathBuf> {
    if !dir.is_dir() {
        return None;
    }
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                if let Some(result) = search_for_static_lib(&path, filename) {
                    return Some(result);
                }
            } else if path.file_name().and_then(|n| n.to_str()) == Some(filename) {
                return path.parent().map(|p| p.to_path_buf());
            }
        }
    }
    None
}