use std::env;
use std::path::{Path, PathBuf};
use cmake::Config;
use glob::glob;
macro_rules! debug_log {
($($arg:tt)*) => {
if std::env::var("BUILD_DEBUG").is_ok() {
println!("cargo:warning=[DEBUG] {}", format!($($arg)*));
}
};
}
fn audio_src_dir() -> PathBuf {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR 未设置");
Path::new(&manifest_dir).join("audio.cpp")
}
const AUDIO_CPP_URL: &str = "https://github.com/0xShug0/audio.cpp.git";
fn ensure_audio_src() -> PathBuf {
let manifest_src = audio_src_dir();
if manifest_src.join("CMakeLists.txt").exists() {
return manifest_src;
}
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| manifest_src.clone());
let mut dir = manifest_dir.clone();
let mut in_git_repo = false;
loop {
if dir.join(".git").exists() {
in_git_repo = true;
break;
}
if !dir.pop() {
break;
}
}
if in_git_repo {
debug_log!("audio.cpp 缺失,执行 git submodule update --init ...");
let status = std::process::Command::new("git")
.args(["submodule", "update", "--init", "--recursive"])
.current_dir(&manifest_dir)
.status()
.expect("failed to run git submodule update");
if status.success() && manifest_src.join("CMakeLists.txt").exists() {
return manifest_src;
}
debug_log!("submodule 更新失败,回退到 git clone");
}
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR 未设置"));
let src_dir = out_dir.join("audio.cpp");
if !src_dir.join("CMakeLists.txt").exists() {
debug_log!("audio.cpp 缺失,执行 git clone --depth 1 {AUDIO_CPP_URL} 到 OUT_DIR ...");
let status = std::process::Command::new("git")
.args(["clone", "--depth", "1", AUDIO_CPP_URL])
.arg(&src_dir)
.status()
.expect("failed to run git clone");
assert!(
status.success() && src_dir.join("CMakeLists.txt").exists(),
"无法自动获取 audio.cpp 源码。请确认网络可用,或手动把源码放到 {}",
manifest_src.display()
);
}
src_dir
}
fn target_os() -> String {
let target = env::var("TARGET").unwrap_or_default();
if target.contains("windows") {
"windows".to_string()
} else if target.contains("apple") {
"apple".to_string()
} else if target.contains("android") {
"android".to_string()
} else if target.contains("linux") {
"linux".to_string()
} else {
target
}
}
fn extract_static_lib_names(search_dirs: &[PathBuf], os: &str) -> Vec<String> {
let ext = match os {
"windows" => "*.lib",
_ => "*.a",
};
let mut names: Vec<String> = Vec::new();
for dir in search_dirs {
let pattern = dir.join("**").join(ext).to_string_lossy().into_owned();
for entry in glob(&pattern).expect("构建 lib glob 失败") {
let Ok(path) = entry else { continue };
let Some(stem) = path.file_stem() else { continue };
let mut name = stem.to_string_lossy().into_owned();
if !name.starts_with("lib") && path.extension().map(|e| e == "a").unwrap_or(false) {
}
if name.starts_with("lib") {
name = name.strip_prefix("lib").unwrap_or(&name).to_string();
}
if name.ends_with("-static") {
name = name.strip_suffix("-static").unwrap_or(&name).to_string();
}
if !names.contains(&name) {
names.push(name);
}
}
}
names
}
fn link_static_libs(names: &[String]) {
for name in names {
println!("cargo:rustc-link-lib=static={}", name);
}
}
fn enabled_model_features() -> Vec<String> {
let mut names = Vec::new();
for (key, _) in env::vars() {
if let Some(suffix) = key.strip_prefix("CARGO_FEATURE_MODEL_") {
names.push(suffix.to_lowercase());
}
}
names.sort();
names
}
fn merge_custom_models(feature_names: Vec<String>) -> String {
let mut all: Vec<String> = feature_names;
if let Ok(env_models) = env::var("AUDIOCPP_MODELS") {
for m in env_models.split(',').map(str::trim).filter(|s| !s.is_empty()) {
if !all.iter().any(|s| s == m) {
all.push(m.to_string());
}
}
}
all.join(",")
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=capi.h");
println!("cargo:rerun-if-changed=capi.cpp");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR 未设置");
let manifest_dir = PathBuf::from(&manifest_dir);
let src_dir = ensure_audio_src();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let os = target_os();
let mut config = Config::new(&src_dir);
config.generator("Ninja");
if os == "windows" {
config.cxxflag("/utf-8").cxxflag("/EHsc");
config.cflag("/utf-8");
}
config.define("ENGINE_BUILD_EXAMPLES", "OFF");
config.define("ENGINE_BUILD_TESTS", "OFF");
config.define("ENGINE_BUILD_WARMBENCH", "OFF");
config.define("AUDIOCPP_DEPLOYMENT_BUILD", "OFF");
config.define("SPM_BUILD_TEST", "OFF");
config.define("SPM_ENABLE_SHARED", "OFF");
config.define("ENGINE_ENABLE_CUDA", if cfg!(feature = "cuda") { "ON" } else { "OFF" });
config.define("ENGINE_ENABLE_HIP", if cfg!(feature = "hip") { "ON" } else { "OFF" });
config.define("ENGINE_ENABLE_VULKAN", if cfg!(feature = "vulkan") { "ON" } else { "OFF" });
let metal_on = cfg!(feature = "metal") || (os == "apple");
config.define("ENGINE_ENABLE_METAL", if metal_on { "ON" } else { "OFF" });
config.define("ENGINE_ENABLE_OPENMP", if cfg!(feature = "openmp") { "ON" } else { "OFF" });
config.define("ENGINE_ENABLE_NATIVE_CPU", if cfg!(feature = "native") { "ON" } else { "OFF" });
config.define(
"CMAKE_ARCHIVE_OUTPUT_DIRECTORY",
out_dir.join("lib").to_string_lossy().into_owned(),
);
let model_set = if cfg!(feature = "full-models") {
"full"
} else if cfg!(feature = "custom-models") {
let requested = merge_custom_models(enabled_model_features());
if requested.is_empty() {
panic!(
"feature `custom-models` 未指定任何模型族。请至少先启用一个 \
`model-<族>` feature(如 --features model-qwen3-asr),或设置 \
环境变量 AUDIOCPP_MODELS(逗号分隔的模型族目标,如 \
AUDIOCPP_MODELS=qwen3_asr,citrinet_asr)"
);
}
println!("cargo:rerun-if-env-changed=AUDIOCPP_MODELS");
config.define("AUDIOCPP_MODELS", &requested);
debug_log!("AUDIOCPP_MODELS(合并后)={}", requested);
"custom"
} else {
"core"
};
config.define("AUDIOCPP_MODEL_SET", model_set);
for (key, value) in env::vars() {
if key.starts_with("GGML_") || key.starts_with("CMAKE_") {
println!("cargo:rerun-if-env-changed={key}");
config.define(&key, &value);
}
}
if os == "windows" {
let cc = cc::Build::new();
let compiler = cc.try_get_compiler().expect("探测 C 编译器失败");
for (key, value) in compiler.env().iter().filter(|(k, _)| {
k.eq_ignore_ascii_case("INCLUDE")
|| k.eq_ignore_ascii_case("LIB")
|| k.eq_ignore_ascii_case("PATH")
}) {
debug_log!(
"注入 MSVC 环境变量 {}={}",
key.to_string_lossy(),
value.to_string_lossy()
);
config.env(key, value);
}
}
let profile = env::var("AUDIOCPP_LIB_PROFILE").unwrap_or_else(|_| "Release".to_string());
let build_dir = config
.profile(&profile)
.build_target("engine_runtime")
.very_verbose(env::var("CMAKE_VERBOSE").is_ok())
.always_configure(true)
.build();
println!("cargo:rerun-if-env-changed=AUDIOCPP_LIB_PROFILE");
let mut search_dirs = vec![
out_dir.join("lib"),
out_dir.join("lib64"),
build_dir.clone(),
];
debug_log!("out_dir={} build_dir={}", out_dir.display(), build_dir.display());
search_dirs.retain(|d| d.is_dir());
for cfg in ["Release", "RelWithDebInfo", "Debug"] {
for base in [&out_dir, &build_dir] {
let d = base.join("lib").join(cfg);
if d.is_dir() {
search_dirs.push(d);
}
}
}
let mut seen: Vec<PathBuf> = Vec::new();
for d in &search_dirs {
if !seen.contains(d) {
println!("cargo:rustc-link-search=native={}", d.display());
seen.push(d.clone());
}
}
let lib_names = extract_static_lib_names(&search_dirs, &os);
assert!(
lib_names.iter().any(|n| n == "engine_runtime"),
"在 OUT_DIR 下未找到 engine_runtime 静态库(找到: {:?})",
lib_names
);
link_static_libs(&lib_names);
debug_log!("发现的静态库: {:?}", lib_names);
if os == "windows" {
println!("cargo:rustc-link-lib=advapi32");
}
let mut cpp = cc::Build::new();
cpp.cpp(true)
.file(manifest_dir.join("capi.cpp"))
.include(src_dir.join("include"))
.include(src_dir.join("external/ggml/include"))
.include(src_dir.join("external/sentencepiece/src"))
.include(src_dir.join("external/llama_tokenizer"))
.include(src_dir.join("external/cJSON"))
.include(src_dir.join("external/libyaml/include"))
.include(build_dir.join("generated"))
.pic(true);
if os == "windows" {
cpp.flag("/std:c++17").flag("/utf-8").flag("/EHsc");
} else {
cpp.flag_if_supported("-std=c++17");
}
if !cfg!(feature = "openmp") {
cpp.flag_if_supported("-fno-openmp");
}
cpp.compile("audio_cpp_capi");
let mut bindings_builder = bindgen::Builder::default()
.header(manifest_dir.join("capi.h").to_str().unwrap())
.allowlist_function("audiocpp_.*")
.allowlist_type("audiocpp_.*")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.derive_partialeq(true);
if os == "windows" {
let cc = cc::Build::new();
let compiler = cc.try_get_compiler().expect("探测 C 编译器失败");
if let Some((_, include_env)) = compiler
.env()
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("INCLUDE"))
{
for inc in include_env.to_string_lossy().split(';').filter(|s| !s.is_empty()) {
bindings_builder = bindings_builder.clang_arg("-isystem").clang_arg(inc);
}
}
let target = env::var("TARGET").unwrap_or_default();
bindings_builder = bindings_builder
.clang_arg(format!("--target={}", target))
.clang_arg("-fms-compatibility")
.clang_arg("-fms-extensions");
}
let bindings = bindings_builder
.generate()
.expect("生成 capi 绑定失败");
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("写入 bindings.rs 失败");
}