#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::collections::hash_map::DefaultHasher;
use std::env;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use cmake::Config;
use glob::glob;
#[cfg(feature = "prebuilt")]
mod prebuilt_download;
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 crt_static_enabled() -> bool {
env::var("CARGO_CFG_TARGET_FEATURE")
.map(|f| f.split(',').any(|s| s.trim() == "crt-static"))
.unwrap_or(false)
}
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 };
if path.components().any(|c| c.as_os_str() == "CMakeFiles") {
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 resolve_prebuilt_directory(use_shared_libs: bool) -> Option<PathBuf> {
if let Ok(raw) = env::var("AUDIOCPP_PREBUILT_DIR")
&& !raw.is_empty()
{
let dir = PathBuf::from(&raw);
if !dir.is_dir() {
panic!("AUDIOCPP_PREBUILT_DIR 指向的目录不存在:{}", dir.display());
}
return Some(dir);
}
#[cfg(feature = "prebuilt")]
{
let target = env::var("TARGET").unwrap_or_default();
prebuilt_download::ensure_prebuilt(&target, use_shared_libs)
}
#[cfg(not(feature = "prebuilt"))]
{
let _ = use_shared_libs;
None
}
}
fn cuda_toolkit_lib_dir(os: &str) -> Option<PathBuf> {
let env_root: Option<PathBuf> = env::var("CUDA_PATH")
.ok()
.or_else(|| {
let mut versions: Vec<String> = env::vars()
.filter_map(|(k, v)| k.strip_prefix("CUDA_PATH_V").map(|_| v))
.collect();
versions.sort();
versions.pop()
})
.map(PathBuf::from);
let from_nvcc = env::var("PATH").ok().and_then(|path| {
let nvcc_name = if os == "windows" { "nvcc.exe" } else { "nvcc" };
env::split_paths(&path)
.map(|d| d.join(nvcc_name))
.find(|p| p.is_file())
.and_then(|p| {
p.parent()
.and_then(|d| d.parent())
.map(std::path::Path::to_path_buf)
})
});
let common = || -> Option<PathBuf> {
let candidates: Vec<PathBuf> = match os {
"windows" => glob("C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*")
.ok()?
.filter_map(std::result::Result::ok)
.collect(),
"linux" => vec![PathBuf::from("/usr/local/cuda")],
_ => vec![],
};
candidates.into_iter().max()
};
let root = env_root.or(from_nvcc).or_else(common)?;
let lib = if os == "windows" {
root.join("lib").join("x64")
} else {
root.join("lib64")
};
lib.is_dir().then_some(lib)
}
fn emit_cuda_links(os: &str) {
if !cfg!(feature = "cuda") {
return;
}
let lib_dir = cuda_toolkit_lib_dir(os).unwrap_or_else(|| {
panic!(
"cuda feature 已启用但未找到 CUDA Toolkit。请安装 CUDA Toolkit >= 12.0,\
并确保 CUDA_PATH 环境变量可用(如 C:\\Program Files\\NVIDIA GPU \
Computing Toolkit\\CUDA\\v12.4)"
)
});
println!("cargo:rustc-link-search=native={}", lib_dir.display());
for lib in ["cudart", "cublas", "cublasLt", "cufft", "cuda"] {
println!("cargo:rustc-link-lib={}", lib);
}
debug_log!("CUDA 链接库目录: {}", lib_dir.display());
}
fn vulkan_sdk_lib_dir(os: &str) -> Option<PathBuf> {
let from_env = env::var("VULKAN_SDK").ok().map(PathBuf::from);
let common = || -> Option<PathBuf> {
let candidates: Vec<PathBuf> = match os {
"windows" => glob("C:/VulkanSDK/v*")
.ok()?
.filter_map(std::result::Result::ok)
.collect(),
_ => vec![],
};
candidates.into_iter().max()
};
let root = from_env.or_else(common)?;
let lib = if os == "windows" {
root.join("Lib")
} else {
root.join("lib")
};
lib.is_dir().then_some(lib)
}
fn emit_vulkan_links(os: &str) {
if !cfg!(feature = "vulkan") {
return;
}
println!("cargo:rerun-if-env-changed=VULKAN_SDK");
let lib_name = if os == "windows" {
"vulkan-1"
} else {
"vulkan"
};
if let Some(lib_dir) = vulkan_sdk_lib_dir(os) {
println!("cargo:rustc-link-search=native={}", lib_dir.display());
debug_log!("Vulkan loader 库目录: {}", lib_dir.display());
} else {
println!(
"cargo:warning=vulkan feature 已启用但未找到 Vulkan SDK。请安装 LunarG Vulkan SDK,\
或设置 VULKAN_SDK 环境变量(如 C:\\VulkanSDK\\1.4.328.1),否则最终链接会因缺 \
vk* 符号失败(LNK2019)"
);
}
println!("cargo:rustc-link-lib={}", lib_name);
debug_log!("Vulkan loader 链接: {}", lib_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 emit_platform_links(os: &str) {
if os == "windows" {
println!("cargo:rustc-link-lib=advapi32");
}
emit_cuda_links(os);
emit_vulkan_links(os);
}
fn compile_capi_shim(manifest_dir: &Path, src_dir: &Path, build_dir: &Path, os: &str) {
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");
}
fn generate_bindings(manifest_dir: &Path, out_dir: &Path, os: &str) {
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 失败");
}
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");
println!("cargo:rerun-if-env-changed=AUDIOCPP_PREBUILT_DIR");
println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FEATURE"); println!("cargo:rerun-if-env-changed=AUDIOCPP_PREBUILT_TAG");
println!("cargo:rerun-if-env-changed=AUDIOCPP_PREBUILT_REPO");
println!("cargo:rerun-if-env-changed=AUDIOCPP_PREBUILT_URL");
println!("cargo:rerun-if-env-changed=AUDIOCPP_PREBUILT_OFF");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR 未设置");
let manifest_dir = PathBuf::from(&manifest_dir);
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let os = target_os();
let crt_static = crt_static_enabled();
if let Some(prebuilt_dir) = resolve_prebuilt_directory(false) {
println!(
"cargo:warning=使用预编译 audio.cpp 静态库:{}(跳过 CMake 构建)",
prebuilt_dir.display()
);
let src_dir = ensure_audio_src();
let mut search_dirs = vec![
prebuilt_dir.clone(),
prebuilt_dir.join("lib"),
prebuilt_dir.join("lib64"),
prebuilt_dir.join("bin"),
];
search_dirs.retain(|d| d.is_dir());
for d in &search_dirs {
println!("cargo:rustc-link-search=native={}", d.display());
}
debug_log!("prebuilt 链接搜索目录: {:?}", search_dirs);
let lib_names = extract_static_lib_names(&search_dirs, &os);
assert!(
lib_names.iter().any(|n| n == "engine_runtime"),
"AUDIOCPP_PREBUILT_DIR 下未找到 engine_runtime 静态库(找到: {:?})",
lib_names
);
link_static_libs(&lib_names);
debug_log!("prebuilt 静态库: {:?}", lib_names);
emit_platform_links(&os);
compile_capi_shim(&manifest_dir, &src_dir, &out_dir, &os);
generate_bindings(&manifest_dir, &out_dir, &os);
return;
}
let src_dir = ensure_audio_src();
const MAX_SAFE_PATH: usize = 240; const VULKAN_EXTRA_PATH: usize = 161; let build_root = out_dir.join("build");
let projected_len = build_root.to_string_lossy().len()
+ if cfg!(feature = "vulkan") {
VULKAN_EXTRA_PATH
} else {
0
};
let cmake_dir = if projected_len > MAX_SAFE_PATH {
let mut h = DefaultHasher::new();
out_dir.to_string_lossy().hash(&mut h);
let short = std::env::temp_dir().join(format!("acb{:012x}", h.finish()));
println!(
"cargo:warning=MSVC 路径过长(预计 {} 字符,上限约 250),CMake 构建目录重定向到 {}",
projected_len,
short.display()
);
short
} else {
out_dir.clone()
};
let mut config = Config::new(&src_dir);
config.generator("Ninja");
if cmake_dir != out_dir {
config.out_dir(&cmake_dir);
}
if os == "windows" {
config.cxxflag("/utf-8").cxxflag("/EHsc");
config.cflag("/utf-8");
if crt_static {
let profile =
env::var("AUDIOCPP_LIB_PROFILE").unwrap_or_else(|_| "Release".to_string());
let runtime = if profile.contains("Debug") {
"MultiThreadedDebug"
} else {
"MultiThreaded"
};
config.define("CMAKE_POLICY_DEFAULT_CMP0091", "NEW");
config.define("CMAKE_MSVC_RUNTIME_LIBRARY", runtime);
println!(
"cargo:warning=crt-static 已启用:CMake 全目标强制 /MT({})",
runtime
);
}
}
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" });
let openmp_on = cfg!(feature = "openmp") && !(crt_static && os == "windows");
config.define("ENGINE_ENABLE_OPENMP", if openmp_on { "ON" } else { "OFF" });
config.define("GGML_OPENMP", if openmp_on { "ON" } else { "OFF" });
if crt_static && os == "windows" && cfg!(feature = "openmp") {
println!(
"cargo:warning=crt-static 已启用:MSVC OpenMP 运行时无静态版(vcomp140.dll),强制关闭 OpenMP"
);
}
config.define(
"ENGINE_ENABLE_NATIVE_CPU",
if cfg!(feature = "native") {
"ON"
} else {
"OFF"
},
);
config.define(
"CMAKE_ARCHIVE_OUTPUT_DIRECTORY",
cmake_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![
cmake_dir.join("lib"),
cmake_dir.join("lib64"),
build_dir.clone(),
];
debug_log!(
"out_dir={} cmake_dir={} build_dir={}",
out_dir.display(),
cmake_dir.display(),
build_dir.display()
);
search_dirs.retain(|d| d.is_dir());
for cfg in ["Release", "RelWithDebInfo", "Debug"] {
for base in [&cmake_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);
emit_platform_links(&os);
compile_capi_shim(&manifest_dir, &src_dir, &build_dir, &os);
generate_bindings(&manifest_dir, &out_dir, &os);
}