eclisp-sys 0.1.3

FFI Bindings for Embeddable Common Lisp
use std::{env, path::PathBuf, process::Command};

fn ecl_config(flag: &str) -> String {
    let output = Command::new("ecl-config")
        .arg(flag)
        .output()
        .expect("Failed to run ecl-config");

    assert!(output.status.success(), "ecl-config {flag} failed");

    String::from_utf8(output.stdout).unwrap()
}

fn main() {
    println!("cargo:rerun-if-changed=wrapper.h");
    println!("cargo:rerun-if-env-changed=ECLISP_INCLUDE_DIRS");

    let cflags = ecl_config("--cflags");

    let mut cc_build = cc::Build::new();
    cc_build.file("shim.c");

    let mut builder = bindgen::Builder::default()
        .header("wrapper.h")
        .clang_args(cflags.split_whitespace());

    for name in ["bdw-gc", "gmp"] {
        if let Ok(lib) = pkg_config::Config::new().cargo_metadata(false).probe(name) {
            for path in lib.include_paths {
                builder = builder.clang_arg(format!("-I{}", path.display()));
                cc_build.include(&path);
            }
        }
    }

    if let Some(path) = find_gc_include() {
        builder = builder.clang_arg(format!("-I{}", path.display()));
        cc_build.include(&path);
    }

    let bindings = builder
        .opaque_type("ecl_clfloat")
        .opaque_type("ecl_ffi_values")
        .opaque_type("cl_lispunion")
        .allowlist_function("eclisp_.*")
        .allowlist_function("cl_boot")
        .allowlist_function("cl_shutdown")
        .allowlist_function("eclisp_.*")
        .allowlist_function("cl_.*")
        .allowlist_function("si_string_to_object")
        .allowlist_function("ecl_make_simple_base_string")
        .allowlist_function("ecl_null_terminated_base_string")
        .allowlist_function("ecl_base_string_pointer_safe")
        .allowlist_type("eclisp_root")
        .allowlist_type("cl_object")
        .allowlist_var("ECL_.*")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("Failed to generate bindings");

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    bindings
        .write_to_file(out_dir.join("bindings.rs"))
        .expect("Failed to write bindings");

    let libs = ecl_config("--libs");

    for flag in shlex::split(&libs).expect("Invalid ECL linker flags") {
        if let Some(path) = flag.strip_prefix("-L") {
            if PathBuf::from(path).is_dir() {
                println!("cargo:rustc-link-search=native={path}");
            }
        } else if let Some(lib) = flag.strip_prefix("-l") {
            println!("cargo:rustc-link-lib={lib}");
        }
    }

    for flag in shlex::split(&cflags).unwrap() {
        cc_build.flag(&flag);
    }

    cc_build.compile("eclisp_shim");

    println!("cargo:rerun-if-changed=shim.c");
}

fn find_gc_include() -> Option<PathBuf> {
    if let Ok(lib) = pkg_config::Config::new()
        .cargo_metadata(false)
        .probe("bdw-gc")
    {
        return lib.include_paths.into_iter().next();
    }

    if cfg!(target_os = "macos") {
        let output = Command::new("brew")
            .args(["--prefix", "bdw-gc"])
            .output()
            .ok()?;

        if output.status.success() {
            let prefix = String::from_utf8(output.stdout).ok()?;
            return Some(PathBuf::from(prefix.trim()).join("include"));
        }
    }

    None
}