libxaac-sys 0.1.0

Rust FFI bindings to the libxaac AAC/xHE-AAC encoder and decoder library
Documentation
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=wrapper.h");
    println!("cargo:rerun-if-changed=libxaac");

    let bundled = env::var_os("CARGO_FEATURE_BUNDLED").is_some();
    let prefer_static = env::var_os("CARGO_FEATURE_STATIC").is_some();
    let prefer_dynamic = env::var_os("CARGO_FEATURE_DYNAMIC").is_some();

    assert!(
        !(prefer_static && prefer_dynamic),
        "`static` and `dynamic` features are mutually exclusive"
    );

    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir"));
    let source_dir = manifest_dir.join("libxaac");
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));

    if bundled {
        let processor = cmake_processor();
        let build_dir = out_dir.join("cmake-build");

        if build_dir.exists() {
            std::fs::remove_dir_all(&build_dir)
                .unwrap_or_else(|err| panic!("failed to clean {}: {err}", build_dir.display()));
        }

        run(
            Command::new("cmake")
                .arg("-S")
                .arg(&source_dir)
                .arg("-B")
                .arg(&build_dir)
                .arg(format!("-DCMAKE_SYSTEM_PROCESSOR={processor}"))
                .arg("-DCMAKE_POSITION_INDEPENDENT_CODE=ON"),
        );
        run(
            Command::new("cmake")
                .arg("--build")
                .arg(&build_dir)
                .arg("--target")
                .arg("libxaacenc")
                .arg("libxaacdec"),
        );

        for lib_name in ["libxaacenc.a", "libxaacdec.a"] {
            let lib_path = find_file(&build_dir, lib_name).unwrap_or_else(|| {
                panic!("failed to locate {lib_name} under {}", build_dir.display())
            });
            let lib_dir = lib_path
                .parent()
                .expect("static library missing parent directory");
            println!("cargo:rustc-link-search=native={}", lib_dir.display());
        }
    }

    if bundled && prefer_dynamic {
        println!("cargo:warning=`dynamic` requested with `bundled`, but vendored libxaac only builds static libraries; using static linking");
    }

    let link_kind = if bundled || prefer_static {
        "static"
    } else if prefer_dynamic {
        "dylib"
    } else {
        "dylib"
    };

    println!("cargo:rustc-link-lib={link_kind}=xaacenc");
    println!("cargo:rustc-link-lib={link_kind}=xaacdec");
    println!("cargo:rustc-link-lib=m");

    let bindings = bindgen::Builder::default()
        .header(manifest_dir.join("wrapper.h").display().to_string())
        .clang_arg(format!("-I{}", manifest_dir.display()))
        .clang_arg(format!("-I{}", source_dir.join("common").display()))
        .clang_arg(format!("-I{}", source_dir.join("decoder").display()))
        .clang_arg(format!("-I{}", source_dir.join("decoder/drc_src").display()))
        .clang_arg(format!("-I{}", source_dir.join("encoder").display()))
        .clang_arg(format!("-I{}", source_dir.join("encoder/drc_src").display()))
        .allowlist_function("ixheaace_(get_lib_id_strings|create|process|delete)")
        .allowlist_function("ixheaacd_(get_lib_id_strings|dec_api|dec_main)")
        .allowlist_function("ia_drc_dec_api")
        .allowlist_type("ixheaace_.*")
        .allowlist_type("ia_(mem_info_struct|lib_info_struct)")
        .allowlist_var("(IA|IXHEAACE|AOT|DEFAULT_MEM_ALIGN_8).*")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("failed to generate bindings");

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

fn cmake_processor() -> &'static str {
    match env::var("CARGO_CFG_TARGET_ARCH").as_deref() {
        Ok("x86_64") => "x86_64",
        Ok("x86") => "i686",
        Ok("aarch64") => "aarch64",
        Ok("arm") => "aarch32",
        Ok(other) => panic!("unsupported target arch: {other}"),
        Err(_) => panic!("missing CARGO_CFG_TARGET_ARCH"),
    }
}

fn find_file(dir: &Path, file_name: &str) -> Option<PathBuf> {
    if !dir.exists() {
        return None;
    }

    let entries = std::fs::read_dir(dir).ok()?;
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            if let Some(found) = find_file(&path, file_name) {
                return Some(found);
            }
        } else if path.file_name().and_then(|name| name.to_str()) == Some(file_name) {
            return Some(path);
        }
    }

    None
}

fn run(command: &mut Command) {
    let status = command.status().expect("failed to spawn command");
    assert!(status.success(), "command failed with status {status}");
}