fdk-aac-rust 0.2.2

Third-party Rust port of the Fraunhofer FDK AAC Codec Library for Android
use std::{env, fs, path::PathBuf, process::Command};

const UPSTREAM_URL: &str = "https://github.com/mstorsjo/fdk-aac.git";
const DEFAULT_UPSTREAM_REVISION: &str = "d8e6b1a3aa606c450241632b64b703f21ea31ce3";

fn main() {
    println!("cargo:rerun-if-env-changed=FDK_AAC_SOURCE_DIR");
    println!("cargo:rerun-if-env-changed=FDK_AAC_REVISION");
    let source = upstream_source();
    println!("cargo:rustc-env=FDK_AAC_UPSTREAM_DIR={}", source.display());
}

fn upstream_source() -> PathBuf {
    if let Some(path) = env::var_os("FDK_AAC_SOURCE_DIR") {
        let path = PathBuf::from(path);
        require_source_tree(&path);
        return path
            .canonicalize()
            .expect("canonicalize FDK_AAC_SOURCE_DIR");
    }

    let revision = upstream_revision();
    let checkout = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"))
        .join(format!("fdk-aac-upstream-{revision}"));
    if !is_real_directory(&checkout) || !is_real_directory(&checkout.join(".git")) {
        if fs::symlink_metadata(&checkout).is_ok() {
            remove_checkout(&checkout);
        }
        fs::create_dir_all(&checkout).expect("create upstream checkout directory");
        run(Command::new("git")
            .arg("init")
            .arg("--quiet")
            .arg(&checkout));
        run(Command::new("git").args(["-C"]).arg(&checkout).args([
            "remote",
            "add",
            "origin",
            UPSTREAM_URL,
        ]));
        run(Command::new("git").args(["-C"]).arg(&checkout).args([
            "fetch",
            "--quiet",
            "--depth=1",
            "origin",
            &revision,
        ]));
        run(Command::new("git").args(["-C"]).arg(&checkout).args([
            "checkout",
            "--quiet",
            "--detach",
            "FETCH_HEAD",
        ]));
    }
    if !command_succeeds(Command::new("git").args(["-C"]).arg(&checkout).args([
        "cat-file",
        "-e",
        &format!("{revision}^{{commit}}"),
    ])) {
        run(Command::new("git").args(["-C"]).arg(&checkout).args([
            "fetch",
            "--quiet",
            "--depth=1",
            "origin",
            &revision,
        ]));
    }
    run(Command::new("git")
        .args(["-C"])
        .arg(&checkout)
        .args(["reset", "--hard", "--quiet", &revision]));
    run(Command::new("git")
        .args(["-C"])
        .arg(&checkout)
        .args(["clean", "-fdx", "--quiet"]));
    let actual_revision = run_output(
        Command::new("git")
            .args(["-C"])
            .arg(&checkout)
            .args(["rev-parse", "HEAD"]),
    );
    assert_eq!(
        actual_revision, revision,
        "upstream checkout revision mismatch"
    );
    require_source_tree(&checkout);
    checkout
}

fn upstream_revision() -> String {
    let revision =
        env::var("FDK_AAC_REVISION").unwrap_or_else(|_| DEFAULT_UPSTREAM_REVISION.to_owned());
    assert!(
        revision.len() == 40 && revision.bytes().all(|byte| byte.is_ascii_hexdigit()),
        "FDK_AAC_REVISION must be a full 40-character Git commit SHA"
    );
    revision
}

fn require_source_tree(path: &std::path::Path) {
    if !path.join("CMakeLists.txt").is_file()
        || !path.join("libFDK/src/FDK_tools_rom.cpp").is_file()
    {
        panic!("{} is not an fdk-aac source tree", path.display());
    }
}

fn is_real_directory(path: &std::path::Path) -> bool {
    fs::symlink_metadata(path)
        .is_ok_and(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
}

fn remove_checkout(path: &std::path::Path) {
    let metadata = fs::symlink_metadata(path).expect("inspect incomplete upstream checkout");
    if metadata.is_dir() && !metadata.file_type().is_symlink() {
        fs::remove_dir_all(path).expect("remove incomplete upstream checkout directory");
    } else {
        fs::remove_file(path).expect("remove incomplete upstream checkout entry");
    }
}

fn run(command: &mut Command) {
    let display = format!("{command:?}");
    let status = command
        .status()
        .unwrap_or_else(|error| panic!("failed to run {display}: {error}"));
    assert!(status.success(), "command failed: {display}");
}

fn command_succeeds(command: &mut Command) -> bool {
    command.status().is_ok_and(|status| status.success())
}

fn run_output(command: &mut Command) -> String {
    let display = format!("{command:?}");
    let output = command
        .output()
        .unwrap_or_else(|error| panic!("failed to run {display}: {error}"));
    assert!(output.status.success(), "command failed: {display}");
    String::from_utf8(output.stdout)
        .expect("command output is not UTF-8")
        .trim()
        .to_owned()
}