mquickjs-sys 0.2.0

Low-level FFI bindings to the MicroQuickJS engine
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn mquickjs_dir(manifest_dir: &Path) -> PathBuf {
    let vendored = manifest_dir.join("vendor").join("mquickjs");
    if vendored.exists() {
        return vendored;
    }
    manifest_dir.join("..").join("..").join("mquickjs")
}

fn host_exe_suffix(host: &str) -> &'static str {
    if host.contains("windows") {
        ".exe"
    } else {
        ""
    }
}

fn build_mqjs_stdlib(mquickjs_dir: &Path, out_dir: &Path, host: &str) -> PathBuf {
    let exe_path = out_dir.join(format!("mqjs_stdlib{}", host_exe_suffix(host)));

    let mut build = cc::Build::new();
    build.host(host).target(host);
    build.include(mquickjs_dir);
    build.warnings(false);
    build.flag_if_supported("-std=c99");
    build.flag_if_supported("-O2");

    let compiler = build.get_compiler();
    let mut cmd = compiler.to_command();
    if compiler.is_like_msvc() {
        cmd.arg(format!("/Fe:{}", exe_path.display()));
    } else {
        cmd.arg("-o").arg(&exe_path);
    }

    cmd.arg(mquickjs_dir.join("mqjs_stdlib.c"));
    cmd.arg(mquickjs_dir.join("mquickjs_build.c"));

    let status = cmd.status().expect("failed to build mqjs_stdlib");
    if !status.success() {
        panic!("mqjs_stdlib build failed with status {status}");
    }

    exe_path
}

fn generate_atom_header(
    mqjs_stdlib: &Path,
    out_dir: &Path,
    target_pointer_width: &str,
) -> PathBuf {
    let mut cmd = Command::new(mqjs_stdlib);
    cmd.arg("-a");
    if target_pointer_width == "32" {
        cmd.arg("-m32");
    }

    let output = cmd.output().expect("failed to run mqjs_stdlib");
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        panic!("mqjs_stdlib failed: {stderr}");
    }

    let header_path = out_dir.join("mquickjs_atom.h");
    std::fs::write(&header_path, output.stdout).expect("failed to write mquickjs_atom.h");
    header_path
}

fn generate_stdlib_source(
    mqjs_stdlib: &Path,
    out_dir: &Path,
    target_pointer_width: &str,
) -> PathBuf {
    let mut cmd = Command::new(mqjs_stdlib);
    if target_pointer_width == "32" {
        cmd.arg("-m32");
    }

    let output = cmd.output().expect("failed to run mqjs_stdlib");
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        panic!("mqjs_stdlib failed: {stderr}");
    }

    let source_path = out_dir.join("mquickjs_stdlib.c");
    let mut contents = String::from("#include <stddef.h>\n#include <stdint.h>\n#include \"mquickjs_priv.h\"\n\n");
    contents.push_str(
        "JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\
JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\
JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\
JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\
JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\
JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\
JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);\n\n",
    );
    contents.push_str(&String::from_utf8_lossy(&output.stdout));
    std::fs::write(&source_path, contents).expect("failed to write mquickjs_stdlib.c");
    source_path
}

fn main() {
    let manifest_dir =
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
    let mquickjs_dir = mquickjs_dir(&manifest_dir);

    let sources = ["mquickjs.c", "cutils.c", "dtoa.c", "libm.c"];
    for source in &sources {
        println!(
            "cargo:rerun-if-changed={}",
            mquickjs_dir.join(source).display()
        );
    }
    for header in ["mquickjs.h", "mquickjs_priv.h", "mquickjs_build.h", "cutils.h", "libm.h"] {
        println!(
            "cargo:rerun-if-changed={}",
            mquickjs_dir.join(header).display()
        );
    }
    for generator in ["mqjs_stdlib.c", "mquickjs_build.c"] {
        println!(
            "cargo:rerun-if-changed={}",
            mquickjs_dir.join(generator).display()
        );
    }
    println!(
        "cargo:rerun-if-changed={}",
        manifest_dir.join("src").join("stdlib_stubs.c").display()
    );

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let wrapper = manifest_dir.join("wrapper.h");
    println!("cargo:rerun-if-changed={}", wrapper.display());
    println!("cargo:rustc-env=MQUICKJS_SYS_OUT_DIR={}", out_dir.display());
    let joined_sources = sources
        .iter()
        .map(|source| mquickjs_dir.join(source).display().to_string())
        .collect::<Vec<_>>()
        .join(";");
    println!("cargo:rustc-env=MQUICKJS_SYS_C_SOURCES={}", joined_sources);

    let host = env::var("HOST").expect("HOST not set");
    let target_pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH")
        .unwrap_or_else(|_| "64".to_string());

    let mqjs_stdlib = build_mqjs_stdlib(&mquickjs_dir, &out_dir, &host);
    let atom_header = generate_atom_header(&mqjs_stdlib, &out_dir, &target_pointer_width);
    let stdlib_source = generate_stdlib_source(&mqjs_stdlib, &out_dir, &target_pointer_width);
    println!("cargo:rerun-if-changed={}", atom_header.display());
    println!("cargo:rerun-if-changed={}", stdlib_source.display());

    let bindings = bindgen::Builder::default()
        .header(wrapper.to_string_lossy())
        .clang_arg(format!("-I{}", mquickjs_dir.display()))
        .clang_arg(format!("-I{}", out_dir.display()))
        .allowlist_type("JS.*")
        .allowlist_function("JS_.*")
        .allowlist_var("JS_.*")
        .allowlist_var("js_stdlib")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("unable to generate bindings");

    bindings
        .write_to_file(out_dir.join("bindings.rs"))
        .expect("could not write bindings");

    let mut build = cc::Build::new();
    build.include(&mquickjs_dir);
    build.include(&out_dir);
    build.warnings(false);

    for source in &sources {
        build.file(mquickjs_dir.join(source));
    }
    build.file(&stdlib_source);
    build.file(manifest_dir.join("src").join("stdlib_stubs.c"));

    let compiler = build.get_compiler();
    if compiler.is_like_msvc() {
        build.define("_CRT_SECURE_NO_WARNINGS", None);
        build.flag_if_supported("/std:c11");
    } else {
        build.flag_if_supported("-std=c99");
        build.flag_if_supported("-O2");
    }

    build.compile("mquickjs");
}