use std::env;
use std::path::{Path, PathBuf};
fn feature(name: &str) -> bool {
env::var_os(format!("CARGO_FEATURE_{name}")).is_some()
}
fn split_cmake_args(s: &str) -> Vec<String> {
let mut args = Vec::new();
let mut cur = String::new();
let mut in_quotes = false;
let mut has_token = false;
for c in s.chars() {
match c {
'"' => {
in_quotes = !in_quotes;
has_token = true;
}
c if c.is_whitespace() && !in_quotes => {
if has_token {
args.push(std::mem::take(&mut cur));
has_token = false;
}
}
c => {
cur.push(c);
has_token = true;
}
}
}
if has_token {
args.push(cur);
}
args
}
fn main() {
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
for p in [
"CMakeLists.txt",
"CMakePresets.json",
"include",
"src",
"ggml",
"cmake",
"bindings/rust/sys/build.rs",
"bindings/rust/sys/src",
] {
println!("cargo:rerun-if-changed={}", root.join(p).display());
}
let dynamic_backends = feature("DYNAMIC_BACKENDS");
let shared = feature("SHARED") || dynamic_backends;
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let is_apple = matches!(target_os.as_str(), "macos" | "ios");
let is_x86 = matches!(target_arch.as_str(), "x86" | "x86_64");
let mut cfg = cmake::Config::new(&root);
cfg.profile("Release") .define("TRANSCRIBE_INSTALL", "ON")
.define("TRANSCRIBE_BUILD_TESTS", "OFF")
.define("TRANSCRIBE_BUILD_EXAMPLES", "OFF")
.define("TRANSCRIBE_BUILD_TOOLS", "OFF")
.define("TRANSCRIBE_BUILD_SHARED", if shared { "ON" } else { "OFF" });
if target_env == "msvc" {
for flag in ["/O2", "/Ob2", "/DNDEBUG"] {
cfg.cflag(flag);
cfg.cxxflag(flag);
}
}
if dynamic_backends {
cfg.define("TRANSCRIBE_GGML_BACKEND_DL", "ON");
if is_x86 {
cfg.define("GGML_CPU_ALL_VARIANTS", "ON");
cfg.define("TRANSCRIBE_X86_CONSERVATIVE", "ON");
}
}
if is_apple {
if feature("METAL") {
cfg.define("TRANSCRIBE_METAL", "ON");
cfg.define("GGML_METAL_EMBED_LIBRARY", "ON");
} else {
cfg.define("TRANSCRIBE_METAL", "OFF");
}
}
if feature("VULKAN") {
cfg.define("TRANSCRIBE_VULKAN", "ON");
}
if feature("CUDA") {
cfg.define("TRANSCRIBE_CUDA", "ON");
}
cfg.define(
"TRANSCRIBE_USE_OPENMP",
if feature("OPENMP") { "ON" } else { "OFF" },
);
if target_os == "windows" {
cfg.define("GGML_OPENMP", "ON");
}
for var in ["TRANSCRIBE_CMAKE_ARGS", "CMAKE_ARGS"] {
println!("cargo:rerun-if-env-changed={var}");
if let Ok(extra) = env::var(var) {
for arg in split_cmake_args(&extra) {
cfg.configure_arg(arg);
}
}
}
let prefix = cfg.build();
let manifest = find_manifest(&prefix)
.unwrap_or_else(|| panic!("transcribe-link.json not found under {}", prefix.display()));
emit_link_lines(&prefix, &manifest);
}
fn find_manifest(prefix: &Path) -> Option<PathBuf> {
for libdir in ["lib", "lib64"] {
let p = prefix.join(libdir).join("transcribe-link.json");
if p.is_file() {
return Some(p);
}
}
None
}
fn emit_link_lines(prefix: &Path, manifest_path: &Path) {
let text = std::fs::read_to_string(manifest_path).expect("read transcribe-link.json");
let json: serde_json::Value = serde_json::from_str(&text).expect("parse transcribe-link.json");
let strs = |key: &str| -> Vec<String> {
json[key]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
};
let shared = json["shared"].as_bool().unwrap_or(false);
let lib_dir = prefix.join(json["lib_dir"].as_str().unwrap_or("lib"));
println!("cargo:rustc-link-search=native={}", lib_dir.display());
let kind = if shared { "dylib" } else { "static" };
for name in strs("libraries") {
println!("cargo:rustc-link-lib={kind}={name}");
}
for path in strs("library_paths") {
println!("cargo:rustc-link-arg={path}");
}
for name in strs("system_libs") {
println!("cargo:rustc-link-lib=dylib={name}");
}
for name in strs("frameworks") {
println!("cargo:rustc-link-lib=framework={name}");
}
for flag in strs("link_flags") {
println!("cargo:rustc-link-arg={flag}");
}
let is_windows = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows");
if shared && !is_windows {
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display());
}
if shared && is_windows {
stage_windows_dlls(prefix);
}
println!(
"cargo:include_dir={}",
prefix
.join(json["include_dir"].as_str().unwrap_or("include"))
.display()
);
println!("cargo:lib_dir={}", lib_dir.display());
if shared {
let bin_dir = prefix.join("bin");
let runtime_dir = if is_windows { &bin_dir } else { &lib_dir };
println!("cargo:runtime_dir={}", runtime_dir.display());
println!("cargo:bin_dir={}", bin_dir.display());
if let Some(rel) = json["module_dir"].as_str() {
println!("cargo:module_dir={}", prefix.join(rel).display());
}
}
}
fn stage_windows_dlls(prefix: &Path) {
let bin_dir = prefix.join("bin");
let dlls: Vec<PathBuf> = match std::fs::read_dir(&bin_dir) {
Ok(entries) => entries
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("dll"))
.collect(),
Err(_) => Vec::new(),
};
if dlls.is_empty() {
println!(
"cargo:warning=transcribe-cpp-sys: no DLLs under {} to stage for the shared posture",
bin_dir.display()
);
return;
}
let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
let Some(profile_dir) = out_dir.ancestors().nth(3) else {
return;
};
for sub in ["", "deps", "examples"] {
let dest = if sub.is_empty() {
profile_dir.to_path_buf()
} else {
profile_dir.join(sub)
};
let _ = std::fs::create_dir_all(&dest);
for dll in &dlls {
if let Some(name) = dll.file_name() {
let _ = std::fs::copy(dll, dest.join(name));
}
}
}
println!("cargo:rerun-if-changed={}", bin_dir.display());
}