use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn parse_bool_env(key: &str) -> bool {
match env::var(key) {
Ok(v) => matches!(
v.as_str(),
"1" | "true" | "yes" | "on" | "TRUE" | "YES" | "ON"
),
Err(_) => false,
}
}
fn main() {
println!("cargo:rustc-check-cfg=cfg(has_pregenerated)");
println!("cargo:rustc-check-cfg=cfg(has_wasm_pregenerated)");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=third-party/box2d/include/box2d/box2d.h");
println!("cargo:rerun-if-changed=third-party/box2d");
println!("cargo:rerun-if-env-changed=BOXDD_SYS_SKIP_CC");
println!("cargo:rerun-if-env-changed=BOXDD_SYS_WASM_CC");
println!("cargo:rerun-if-env-changed=BOX2D_LIB_DIR");
println!("cargo:rerun-if-env-changed=BOXDD_SYS_LINK_KIND");
println!("cargo:rerun-if-env-changed=BOXDD_SYS_FORCE_BINDGEN");
println!("cargo:rerun-if-env-changed=BOXDD_SYS_STRICT_FEATURES");
println!("cargo:rerun-if-env-changed=EMSDK");
println!("cargo:rerun-if-env-changed=WASI_SDK_PATH");
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rerun-if-env-changed=CARGO_CFG_DOCSRS");
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let profile = env::var("PROFILE").unwrap_or_else(|_| "release".into());
let is_debug = profile == "debug";
let pregenerated = manifest_dir.join("src").join("bindings_pregenerated.rs");
let wasm_pregenerated = manifest_dir
.join("src")
.join("wasm_bindings_pregenerated.rs");
let has_pregenerated = pregenerated.exists();
let has_wasm_pregenerated = wasm_pregenerated.exists();
if has_pregenerated {
println!("cargo:rustc-cfg=has_pregenerated");
}
if target_arch == "wasm32" && has_wasm_pregenerated {
println!("cargo:rustc-cfg=has_wasm_pregenerated");
}
let is_docsrs = env::var("DOCS_RS").is_ok() || env::var("CARGO_CFG_DOCSRS").is_ok();
let mut used_wasm_pregenerated = false;
if target_arch == "wasm32"
&& has_wasm_pregenerated
&& let Ok(s) = std::fs::read_to_string(&wasm_pregenerated)
{
let _ = std::fs::write(out_dir.join("bindings.rs"), s);
used_wasm_pregenerated = true;
println!(
"cargo:warning=Using wasm pregenerated bindings: {}",
wasm_pregenerated.display()
);
}
let force_bindgen = parse_bool_env("BOXDD_SYS_FORCE_BINDGEN");
if force_bindgen || (!has_pregenerated && !is_docsrs && !used_wasm_pregenerated) {
#[cfg(feature = "bindgen")]
generate_bindings(&manifest_dir, &out_dir);
#[cfg(not(feature = "bindgen"))]
{
if force_bindgen {
panic!(
"BOXDD_SYS_FORCE_BINDGEN=1 but the `boxdd-sys` crate was built without the `bindgen` feature"
);
}
panic!(
"pregenerated bindings are missing and the `boxdd-sys` crate was built without the `bindgen` feature"
);
}
}
if is_docsrs {
println!("cargo:warning=DOCS_RS detected: skipping native Box2D C build");
return;
}
if parse_bool_env("BOXDD_SYS_SKIP_CC") {
println!("cargo:warning=Skipping native C build due to BOXDD_SYS_SKIP_CC");
return;
}
if target_arch == "wasm32" {
if target_env == "emscripten" {
build_with_cc_wasm(&manifest_dir, &target_env, is_debug);
} else if target_os == "wasi" {
if env::var("WASI_SDK_PATH").is_ok() || parse_bool_env("BOXDD_SYS_WASM_CC") {
build_with_cc_wasi(&manifest_dir, is_debug);
} else {
println!(
"cargo:warning=WASI target detected but no WASI_SDK_PATH; skipping native C build"
);
}
} else if parse_bool_env("BOXDD_SYS_WASM_CC") {
build_with_cc_wasm(&manifest_dir, &target_env, is_debug);
} else {
println!(
"cargo:warning=WASM (unknown) skeleton: skipping native C build (set BOXDD_SYS_WASM_CC=1 to enable)"
);
}
} else {
if try_link_system(&target_arch) {
return;
}
build_box2d_from_source(&manifest_dir, &target_env, &target_os, is_debug);
}
}
#[cfg(feature = "bindgen")]
fn generate_bindings(manifest_dir: &Path, out_dir: &Path) {
let header = manifest_dir
.join("third-party")
.join("box2d")
.join("include")
.join("box2d")
.join("box2d.h");
let include_dir = header.parent().unwrap().to_path_buf();
let bindings = bindgen::Builder::default()
.header(header.to_string_lossy())
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.clang_args(["-x", "c", "-std=c17"]) .clang_arg(format!("-I{}", include_dir.display()))
.allowlist_function("b2.*")
.allowlist_type("b2.*")
.allowlist_var("B2_.*")
.layout_tests(false)
.generate()
.expect("Failed to generate box2d bindings");
let out = out_dir.join("bindings.rs");
bindings
.write_to_file(&out)
.expect("Couldn't write bindings!");
}
#[cfg(not(feature = "bindgen"))]
#[allow(dead_code)]
fn generate_bindings(_manifest_dir: &Path, _out_dir: &Path) {
unreachable!("generate_bindings is only available with the `bindgen` feature enabled");
}
fn build_box2d_from_source(manifest_dir: &Path, target_env: &str, target_os: &str, is_debug: bool) {
let third_party = manifest_dir.join("third-party");
let box2d_root = third_party.join("box2d");
let box2d_include = box2d_root.join("include");
let box2d_src = box2d_root.join("src");
if !box2d_include.exists() || !box2d_src.exists() {
println!(
"cargo:warning=Box2D submodule not found at {}; run: git submodule update --init --recursive",
box2d_root.display()
);
}
let mut build = cc::Build::new();
build.include(&box2d_include);
let mut files: Vec<PathBuf> = Vec::new();
collect_c_files(&box2d_src, &mut files);
for f in files {
build.file(f);
}
if target_env == "msvc" {
let use_static_crt = env::var("CARGO_CFG_TARGET_FEATURE")
.unwrap_or_default()
.split(',')
.any(|f| f == "crt-static");
build.static_crt(use_static_crt);
if use_static_crt {
build.flag("/MT");
} else {
build.flag("/MD");
}
if is_debug {
build.debug(true);
build.opt_level(0);
} else {
build.debug(false);
build.opt_level(2);
}
build.flag_if_supported("/std:c17");
build.flag_if_supported("/std:c11");
if cfg!(feature = "validate") {
build.define("BOX2D_VALIDATE", None);
}
let is_x86_64 = env::var("CARGO_CFG_TARGET_ARCH").ok().as_deref() == Some("x86_64");
if cfg!(feature = "disable-simd") {
build.define("BOX2D_DISABLE_SIMD", None);
} else if cfg!(feature = "simd-avx2") && is_x86_64 {
build.define("BOX2D_AVX2", None);
build.flag_if_supported("/arch:AVX2");
}
} else {
build.flag_if_supported("-std=c17");
if target_os == "linux"
|| target_os == "macos"
|| env::var("CARGO_CFG_TARGET_ENV").ok().as_deref() == Some("gnu")
{
build.flag_if_supported("-ffp-contract=off");
}
if cfg!(feature = "validate") {
build.define("BOX2D_VALIDATE", None);
}
let is_x86_64 = env::var("CARGO_CFG_TARGET_ARCH").ok().as_deref() == Some("x86_64");
if cfg!(feature = "disable-simd") {
build.define("BOX2D_DISABLE_SIMD", None);
} else if cfg!(feature = "simd-avx2") && is_x86_64 {
build.define("BOX2D_AVX2", None);
build.flag_if_supported("-mavx2");
}
if target_os == "linux" {
build.define("_POSIX_C_SOURCE", Some("199309L"));
build.flag_if_supported("-pthread");
println!("cargo:rustc-link-lib=pthread");
}
}
build.compile("box2d");
}
fn link_kind_from_env() -> Option<&'static str> {
match env::var("BOXDD_SYS_LINK_KIND").ok().as_deref() {
Some("static") | Some("STATIC") => Some("static"),
Some("dylib") | Some("DYLIB") | Some("shared") | Some("SHARED") => Some("dylib"),
_ => None,
}
}
fn warn_or_error_system_ignores_features() {
let simd = cfg!(feature = "simd-avx2");
let nosimd = cfg!(feature = "disable-simd");
let validate = cfg!(feature = "validate");
if simd || nosimd || validate {
if parse_bool_env("BOXDD_SYS_STRICT_FEATURES") {
panic!(
"System library mode ignores crate features (simd-avx2/disable-simd/validate). Use source build instead or unset BOXDD_SYS_STRICT_FEATURES."
);
} else {
println!(
"cargo:warning=System library mode ignores crate features: {}{}{}",
if simd { "simd-avx2 " } else { "" },
if nosimd { "disable-simd " } else { "" },
if validate { "validate" } else { "" },
);
}
}
}
fn try_link_system(_target_arch: &str) -> bool {
if let Ok(dir) = env::var("BOX2D_LIB_DIR") {
println!("cargo:rustc-link-search=native={}", dir);
if let Some(kind) = link_kind_from_env() {
println!("cargo:rustc-link-lib={}=box2d", kind);
} else {
println!("cargo:rustc-link-lib=box2d");
}
warn_or_error_system_ignores_features();
return true;
}
#[cfg(feature = "pkg-config")]
{
if pkg_config::Config::new()
.cargo_metadata(true)
.probe("box2d")
.is_ok()
{
warn_or_error_system_ignores_features();
return true;
}
}
false
}
fn collect_c_files(dir: &Path, out: &mut Vec<PathBuf>) {
if let Ok(rd) = fs::read_dir(dir) {
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
collect_c_files(&path, out);
} else if let Some(ext) = path.extension()
&& ext == "c"
{
out.push(path);
}
}
}
}
fn build_with_cc_wasm(manifest_dir: &Path, target_env: &str, is_debug: bool) {
let third_party = manifest_dir.join("third-party");
let box2d_root = third_party.join("box2d");
let box2d_include = box2d_root.join("include");
let box2d_src = box2d_root.join("src");
let mut build = cc::Build::new();
build.include(&box2d_include);
let mut files: Vec<PathBuf> = Vec::new();
collect_c_files(&box2d_src, &mut files);
for f in files {
build.file(f);
}
if target_env == "emscripten" {
if let Ok(emsdk) = env::var("EMSDK") {
let mut clang = PathBuf::from(&emsdk);
clang.push("upstream");
clang.push("bin");
clang.push(if cfg!(windows) { "clang.exe" } else { "clang" });
if clang.exists() {
build.compiler(clang);
}
let mut sysroot = PathBuf::from(&emsdk);
sysroot.push("upstream");
sysroot.push("emscripten");
sysroot.push("cache");
sysroot.push("sysroot");
if sysroot.exists() {
build.flag(format!("--sysroot={}", sysroot.display()));
}
}
build.flag("-target");
build.flag("wasm32-unknown-emscripten");
} else {
build.flag("-target");
build.flag("wasm32-unknown-unknown");
}
build.flag_if_supported("-ffp-contract=off");
if is_debug {
build.debug(true);
build.opt_level(0);
} else {
build.debug(false);
build.opt_level(2);
}
build.flag_if_supported("-std=c17");
build.static_flag(true);
build.compile("box2d");
}
fn build_with_cc_wasi(manifest_dir: &Path, is_debug: bool) {
let third_party = manifest_dir.join("third-party");
let box2d_root = third_party.join("box2d");
let box2d_include = box2d_root.join("include");
let box2d_src = box2d_root.join("src");
let mut build = cc::Build::new();
build.include(&box2d_include);
let mut files: Vec<PathBuf> = Vec::new();
collect_c_files(&box2d_src, &mut files);
for f in files {
build.file(f);
}
if let Ok(wasi_sdk) = env::var("WASI_SDK_PATH") {
let mut clang = PathBuf::from(&wasi_sdk);
clang.push("bin");
clang.push(if cfg!(windows) { "clang.exe" } else { "clang" });
if clang.exists() {
build.compiler(clang);
}
let mut sysroot = PathBuf::from(&wasi_sdk);
sysroot.push("share");
sysroot.push("wasi-sysroot");
if sysroot.exists() {
build.flag(format!("--sysroot={}", sysroot.display()));
}
}
build.flag("-target");
build.flag("wasm32-wasip1");
build.flag_if_supported("-ffp-contract=off");
if is_debug {
build.debug(true);
build.opt_level(0);
} else {
build.debug(false);
build.opt_level(2);
}
build.flag_if_supported("-std=c17");
build.static_flag(true);
build.compile("box2d");
}