audio-engine-core 0.1.0

Reusable decoder, DSP, loudness, resampling, and streaming pipeline primitives
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

#[allow(dead_code)]
fn main() {
    run_build_script();
}

pub fn run_build_script() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
    println!("cargo:rerun-if-env-changed=VCPKG_ROOT");

    if env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() != "windows" {
        return;
    }

    if vcpkg::find_package("soxr").is_ok() {
        return;
    }

    match pkg_config::probe_library("soxr") {
        Ok(library) => {
            if env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "msvc" {
                if let Err(error) = ensure_msvc_import_lib(&library) {
                    println!(
                        "cargo:warning=Found soxr via pkg-config but failed to prepare an MSVC import library: {error}"
                    );
                }
            }
        }
        Err(error) => {
            println!("cargo:warning=Both vcpkg and pkg-config failed to find soxr: {error}");
        }
    }
}

fn ensure_msvc_import_lib(library: &pkg_config::Library) -> Result<(), String> {
    let dll_path = find_soxr_dll(library).ok_or_else(|| {
        "unable to locate libsoxr.dll next to the pkg-config library path".to_string()
    })?;

    let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|error| error.to_string())?);
    let def_file_name = format!(
        "{}.def",
        dll_path
            .file_stem()
            .and_then(|stem| stem.to_str())
            .ok_or_else(|| "soxr dll path is missing a valid file stem".to_string())?
    );
    let def_path = out_dir.join(def_file_name);
    let import_lib_path = out_dir.join("soxr.lib");

    if !import_lib_path.exists() {
        let gendef_path = find_gendef(&dll_path)
            .ok_or_else(|| "unable to find gendef.exe to generate soxr.def".to_string())?;
        let lib_exe_path = find_lib_exe().ok_or_else(|| {
            "unable to find Visual Studio lib.exe to generate soxr.lib".to_string()
        })?;

        run(
            Command::new(gendef_path)
                .arg(&dll_path)
                .current_dir(&out_dir),
            "gendef",
        )?;
        run(
            Command::new(lib_exe_path)
                .arg(format!("/DEF:{}", def_path.display()))
                .arg(format!("/OUT:{}", import_lib_path.display()))
                .arg("/MACHINE:X64"),
            "lib.exe",
        )?;
    }

    println!("cargo:rustc-link-search=native={}", out_dir.display());
    copy_runtime_dll(&dll_path, &out_dir)?;

    Ok(())
}

fn find_soxr_dll(library: &pkg_config::Library) -> Option<PathBuf> {
    for link_path in &library.link_paths {
        let candidates = [
            link_path.join("soxr.lib"),
            link_path.join("libsoxr.lib"),
            link_path.join("libsoxr.dll.a"),
            link_path.join("libsoxr.a"),
        ];

        if candidates.iter().any(|path| path.exists()) {
            let bin_dir = link_path.parent()?.join("bin");
            for dll_name in ["libsoxr.dll", "soxr.dll"] {
                let dll_path = bin_dir.join(dll_name);
                if dll_path.exists() {
                    return Some(dll_path);
                }
            }
        }
    }

    if let Some(dll_path) = env::var_os("PKG_CONFIG_PATH").and_then(|paths| {
        env::split_paths(&paths)
            .flat_map(|dir| {
                let mut candidates = vec![dir.join("..").join("bin").join("libsoxr.dll")];
                candidates.push(dir.join("..").join("bin").join("soxr.dll"));
                candidates
            })
            .find(|path| path.exists())
    }) {
        return Some(dll_path);
    }

    if let Some(dll_path) = path_candidates("libsoxr.dll")
        .into_iter()
        .chain(path_candidates("soxr.dll"))
        .find(|path| path.exists())
    {
        return Some(dll_path);
    }

    if let Some(user_profile) = env::var_os("USERPROFILE") {
        let scoop_msys2 = PathBuf::from(user_profile)
            .join("scoop")
            .join("apps")
            .join("msys2");
        if let Ok(entries) = fs::read_dir(scoop_msys2) {
            for entry in entries.filter_map(Result::ok) {
                let bin_dir = entry.path().join("mingw64").join("bin");
                for dll_name in ["libsoxr.dll", "soxr.dll"] {
                    let dll_path = bin_dir.join(dll_name);
                    if dll_path.exists() {
                        return Some(dll_path);
                    }
                }
            }
        }
    }

    None
}

fn find_gendef(dll_path: &Path) -> Option<PathBuf> {
    let mut candidates = Vec::new();
    if let Some(bin_dir) = dll_path.parent() {
        candidates.push(bin_dir.join("gendef.exe"));
    }
    candidates.extend(path_candidates("gendef.exe"));
    candidates.into_iter().find(|path| path.exists())
}

fn find_lib_exe() -> Option<PathBuf> {
    let mut candidates = path_candidates("lib.exe");

    if let Some(vswhere) = find_vswhere() {
        let output = Command::new(vswhere)
            .args([
                "-latest",
                "-products",
                "*",
                "-requires",
                "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
                "-property",
                "installationPath",
            ])
            .output()
            .ok()?;

        if output.status.success() {
            let install_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !install_path.is_empty() {
                let tools_root = Path::new(&install_path)
                    .join("VC")
                    .join("Tools")
                    .join("MSVC");
                if let Ok(entries) = fs::read_dir(tools_root) {
                    let mut versions = entries
                        .filter_map(Result::ok)
                        .map(|entry| entry.path())
                        .filter(|path| path.is_dir())
                        .collect::<Vec<_>>();
                    versions.sort();
                    versions.reverse();
                    for version_dir in versions {
                        candidates.push(
                            version_dir
                                .join("bin")
                                .join("HostX64")
                                .join("x64")
                                .join("lib.exe"),
                        );
                    }
                }
            }
        }
    }

    candidates.into_iter().find(|path| path.exists())
}

fn find_vswhere() -> Option<PathBuf> {
    let explicit =
        PathBuf::from(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe");
    if explicit.exists() {
        return Some(explicit);
    }
    path_candidates("vswhere.exe")
        .into_iter()
        .find(|path| path.exists())
}

fn path_candidates(name: &str) -> Vec<PathBuf> {
    env::var_os("PATH")
        .map(|path| env::split_paths(&path).map(|dir| dir.join(name)).collect())
        .unwrap_or_default()
}

fn copy_runtime_dll(dll_path: &Path, out_dir: &Path) -> Result<(), String> {
    let profile_dir = out_dir
        .ancestors()
        .nth(3)
        .ok_or_else(|| "unable to resolve Cargo profile output directory".to_string())?;
    let destination = profile_dir.join(
        dll_path
            .file_name()
            .ok_or_else(|| "soxr dll path is missing a file name".to_string())?,
    );

    if destination.exists() {
        return Ok(());
    }

    fs::copy(dll_path, &destination).map_err(|error| {
        format!(
            "failed to copy {} to {}: {error}",
            dll_path.display(),
            destination.display()
        )
    })?;

    Ok(())
}

fn run(command: &mut Command, name: &str) -> Result<(), String> {
    let output = command
        .output()
        .map_err(|error| format!("failed to run {name}: {error}"))?;
    if output.status.success() {
        return Ok(());
    }

    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    Err(format!(
        "{name} exited with {}. stdout: {} stderr: {}",
        output.status, stdout, stderr
    ))
}