boxdd-sys 0.4.0

Low-level FFI bindings for Box2D built from upstream via submodule
Documentation
use flate2::{Compression, write::GzEncoder};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn expected_lib_name() -> &'static str {
    if cfg!(target_env = "msvc") {
        "box2d.lib"
    } else {
        "libbox2d.a"
    }
}

fn default_target_triple() -> String {
    if let Ok(t) = env::var("TARGET") {
        return t;
    }
    if let Ok(t) = env::var("CARGO_CFG_TARGET_TRIPLE") {
        return t;
    }
    let arch = std::env::consts::ARCH;
    let os = std::env::consts::OS;
    match os {
        "windows" => format!("{}-pc-windows-msvc", arch),
        "macos" => format!("{}-apple-darwin", arch),
        "linux" => format!("{}-unknown-linux-gnu", arch),
        _ => format!("{}-unknown-{}", arch, os),
    }
}

fn compose_archive_name(
    crate_short: &str,
    version: &str,
    target: &str,
    link_type: &str,
    extra: Option<&str>,
    crt: &str,
) -> String {
    let extra = extra.unwrap_or("");
    if crt.is_empty() {
        if extra.is_empty() {
            format!(
                "{}-prebuilt-{}-{}-{}.tar.gz",
                crate_short, version, target, link_type
            )
        } else {
            format!(
                "{}-prebuilt-{}-{}-{}{}.tar.gz",
                crate_short, version, target, link_type, extra
            )
        }
    } else if extra.is_empty() {
        format!(
            "{}-prebuilt-{}-{}-{}-{}.tar.gz",
            crate_short, version, target, link_type, crt
        )
    } else {
        format!(
            "{}-prebuilt-{}-{}-{}{}-{}.tar.gz",
            crate_short, version, target, link_type, extra, crt
        )
    }
}

fn compose_manifest_bytes(
    crate_short: &str,
    version: &str,
    target: &str,
    link_type: &str,
    crt: &str,
    features: Option<&str>,
) -> Vec<u8> {
    let mut buf = Vec::new();
    use std::io::Write;
    let _ = writeln!(
        &mut buf,
        "{} prebuilt\nversion={}\ntarget={}\nlink={}\ncrt={}",
        crate_short, version, target, link_type, crt
    );
    if let Some(f) = features
        && !f.is_empty()
    {
        let _ = writeln!(&mut buf, "features={}", f);
    }
    buf
}

fn locate_sys_out_dir(workspace_root: &Path, target: &str) -> Result<PathBuf, String> {
    let profile = env::var("PROFILE").unwrap_or_else(|_| "release".into());
    let target_dir = env::var("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| workspace_root.join("target"));
    // When `--target <triple>` is used, cargo outputs under `target/<triple>/<profile>/...`.
    // When no `--target` is used, cargo outputs under `target/<profile>/...`.
    let build_root_target = target_dir.join(target).join(&profile).join("build");
    let build_root_host = target_dir.join(&profile).join("build");
    let build_root = if build_root_target.exists() {
        build_root_target
    } else if build_root_host.exists() {
        build_root_host
    } else {
        return Err(format!(
            "Build root not found at {} (or {})",
            build_root_target.display(),
            build_root_host.display()
        ));
    };

    let mut candidates: Vec<PathBuf> = match std::fs::read_dir(&build_root) {
        Ok(rd) => rd
            .filter_map(|e| e.ok())
            .filter_map(|e| {
                let p = e.path();
                let name = p.file_name()?.to_string_lossy().to_string();
                if name.starts_with("boxdd-sys-") {
                    let out = p.join("out");
                    if out.exists() { Some(out) } else { None }
                } else {
                    None
                }
            })
            .collect(),
        Err(_) => Vec::new(),
    };
    if candidates.is_empty() {
        return Err(format!(
            "No boxdd-sys build out directories found under {}",
            build_root.display()
        ));
    }
    candidates.sort_by_key(|p| std::fs::metadata(p).and_then(|m| m.modified()).ok());
    Ok(candidates.pop().unwrap())
}

fn append_headers(
    tar: &mut tar::Builder<GzEncoder<fs::File>>,
    src_dir: &Path,
    dst_root: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut stack = vec![src_dir.to_path_buf()];
    while let Some(dir) = stack.pop() {
        for entry in fs::read_dir(&dir)? {
            let entry = entry?;
            let p = entry.path();
            let rel = p.strip_prefix(src_dir).unwrap();
            if p.is_dir() {
                stack.push(p);
            } else if p
                .extension()
                .and_then(|s| s.to_str())
                .map(|s| s.eq_ignore_ascii_case("h"))
                .unwrap_or(false)
            {
                let mut f = fs::File::open(&p)?;
                let dst_path = format!("{}/{}", dst_root, rel.display());
                tar.append_file(dst_path, &mut f)?;
            }
        }
    }
    Ok(())
}

fn append_license_if_exists(
    tar: &mut tar::Builder<GzEncoder<fs::File>>,
    src: &Path,
    dst: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    if src.exists() {
        let mut f = fs::File::open(src)?;
        let mut hdr = tar::Header::new_gnu();
        hdr.set_size(f.metadata()?.len());
        hdr.set_mode(0o644);
        hdr.set_cksum();
        tar.append_data(&mut hdr, dst, &mut f)?;
        println!("Added license: {} => {}", src.display(), dst);
    } else {
        eprintln!("WARN: license file missing: {}", src.display());
    }
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let workspace_root = manifest_dir.parent().unwrap();

    let target = default_target_triple();
    let crate_version = env::var("CARGO_PKG_VERSION").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_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default();
    let crt = if target_os == "windows" && target_env == "msvc" {
        if target_features.split(',').any(|f| f == "crt-static") {
            "mt"
        } else {
            "md"
        }
    } else {
        ""
    };

    let link_type = "static";

    // Optional feature list for manifest
    let features = env::var("BOXDD_SYS_PKG_FEATURES").unwrap_or_default();

    let pkg_dir = PathBuf::from(env::var("BOXDD_SYS_PACKAGE_DIR").unwrap_or_else(|_| {
        env::var("OUT_DIR")
            .unwrap_or_else(|_| workspace_root.join("packages").display().to_string())
    }));
    fs::create_dir_all(&pkg_dir)?;

    let ar_name = compose_archive_name("boxdd", &crate_version, &target, link_type, None, crt);
    let out_path = pkg_dir.join(&ar_name);
    println!("Packaging to: {}", out_path.display());

    let f = fs::File::create(&out_path)?;
    let enc = GzEncoder::new(f, Compression::default());
    let mut tar = tar::Builder::new(enc);

    // Add headers: include/box2d/**
    let include_root = manifest_dir
        .join("third-party")
        .join("box2d")
        .join("include");
    if include_root.exists() {
        append_headers(&mut tar, &include_root, "include/box2d")?;
        println!("Added headers from {}", include_root.display());
    } else {
        eprintln!("WARN: include dir not found: {}", include_root.display());
    }

    // Licenses (project + upstream if present)
    append_license_if_exists(
        &mut tar,
        &workspace_root.join("LICENSE-MIT"),
        "licenses/PROJECT-LICENSE-MIT",
    )?;
    append_license_if_exists(
        &mut tar,
        &workspace_root.join("LICENSE-APACHE"),
        "licenses/PROJECT-LICENSE-APACHE",
    )?;

    // Include static library
    let sys_out = locate_sys_out_dir(workspace_root, &target)?;
    let lib_path = sys_out.join(expected_lib_name());
    if !lib_path.exists() {
        return Err(format!("Static library not found at {}", lib_path.display()).into());
    }
    let mut f = fs::File::open(&lib_path)?;
    tar.append_file(format!("lib/{}", expected_lib_name()), &mut f)?;
    println!("Added lib: {}", lib_path.display());

    // Add manifest text
    let manifest_txt = compose_manifest_bytes(
        "boxdd",
        &crate_version,
        &target,
        link_type,
        crt,
        if features.is_empty() {
            None
        } else {
            Some(&features)
        },
    );
    let mut hdr = tar::Header::new_gnu();
    hdr.set_size(manifest_txt.len() as u64);
    hdr.set_mode(0o644);
    hdr.set_cksum();
    tar.append_data(&mut hdr, "manifest.txt", manifest_txt.as_slice())?;

    tar.finish()?;
    println!("Package created: {}", out_path.display());
    Ok(())
}