use std::env;
use std::path::{Path, PathBuf};
fn feat(name: &str) -> bool {
env::var(format!("CARGO_FEATURE_{name}")).is_ok()
}
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let src = env::var("IK_LLAMA_CPP_SRC")
.map(PathBuf::from)
.unwrap_or_else(|_| manifest_dir.join("ik_llama.cpp"));
assert!(
src.join("include/llama.h").exists(),
"ik_llama.cpp source not found at {src:?} — set IK_LLAMA_CPP_SRC or run `git submodule update --init`"
);
let want_common = feat("COMMON");
let want_mtmd = feat("MTMD");
let want_cuda = feat("CUDA");
let want_vulkan = feat("VULKAN");
let want_openmp = feat("OPENMP");
let want_native = feat("NATIVE");
let dynamic_link = feat("DYNAMIC_LINK");
let static_stdcxx = feat("STATIC_STDCXX");
for f in [
"wrapper.h",
"wrapper_common.h",
"wrapper_common.cpp",
"wrapper_grammar.h",
"wrapper_grammar.cpp",
"wrapper_utils.h",
"wrapper_mtmd.h",
"build.rs",
] {
println!("cargo:rerun-if-changed={f}");
}
println!("cargo:rerun-if-env-changed=IK_LLAMA_CPP_SRC");
println!("cargo:rerun-if-env-changed=IK_LLAMA_CPP_LIB_DIR");
generate_bindings(&src, &out_dir, want_common);
if want_mtmd {
generate_mtmd_bindings(&src, &out_dir);
}
compile_grammar_glue(&src, &manifest_dir);
if want_common {
compile_common_glue(&src, &manifest_dir);
}
if want_mtmd {
compile_mtmd(&src);
}
let backend = if let Some(lib_dir) = env::var("IK_LLAMA_CPP_LIB_DIR").ok().map(PathBuf::from) {
link_prebuilt(&lib_dir, want_common, want_cuda);
format!("prebuilt:{}", lib_dir.display())
} else {
let dst = cmake_build(
&src,
want_common,
want_cuda,
want_vulkan,
want_openmp,
want_native,
dynamic_link,
);
link_built(&dst, want_common, dynamic_link);
"cmake".to_string()
};
if want_cuda {
link_cuda();
}
link_system(static_stdcxx, want_openmp);
write_manifest(
&src,
&out_dir,
want_cuda,
want_vulkan,
want_openmp,
want_common,
&backend,
);
}
fn generate_bindings(src: &Path, out_dir: &Path, want_common: bool) {
let mut builder = bindgen::Builder::default()
.header("wrapper.h")
.clang_arg(format!("-I{}", src.join("include").display()))
.clang_arg(format!("-I{}", src.join("ggml/include").display()))
.allowlist_function("ggml_.*")
.allowlist_type("ggml_.*")
.allowlist_var("ggml_.*")
.allowlist_function("gguf_.*")
.allowlist_type("gguf_.*")
.allowlist_var("gguf_.*")
.allowlist_function("llama_.*")
.allowlist_type("llama_.*")
.allowlist_var("llama_.*")
.allowlist_function("ik_llama_rs_grammar_.*")
.prepend_enum_name(false)
.derive_partialeq(true)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
if want_common {
builder = builder
.clang_arg("-DLLAMA_RS_BUILD_COMMON")
.clang_arg(format!("-I{}", src.join("common").display()))
.allowlist_function("ik_llama_rs_.*")
.allowlist_type("ik_llama_rs_.*");
}
builder
.generate()
.expect("bindgen failed to generate ik_llama.cpp bindings")
.write_to_file(out_dir.join("bindings.rs"))
.expect("failed to write bindings.rs");
}
fn generate_mtmd_bindings(src: &Path, out_dir: &Path) {
let mtmd_dir = src.join("examples/mtmd");
assert!(
mtmd_dir.join("mtmd.h").exists(),
"mtmd feature set but {mtmd_dir:?}/mtmd.h not found"
);
bindgen::Builder::default()
.header("wrapper_mtmd.h")
.clang_arg("-x")
.clang_arg("c++")
.clang_arg("-std=c++17")
.clang_arg(format!("-I{}", mtmd_dir.display()))
.clang_arg(format!("-I{}", src.join("include").display()))
.clang_arg(format!("-I{}", src.join("ggml/include").display()))
.clang_arg(format!("-I{}", src.join("vendor").display()))
.allowlist_function("mtmd_.*")
.allowlist_type("mtmd_.*")
.blocklist_function("mtmd_input_chunk_from_json")
.blocklist_function("mtmd_input_chunk_to_json")
.blocklist_type("llama_.*")
.blocklist_type("ggml_.*")
.blocklist_type("gguf_.*")
.blocklist_function("llama_.*")
.blocklist_function("ggml_.*")
.blocklist_function("gguf_.*")
.blocklist_type("nlohmann.*")
.blocklist_type("json")
.blocklist_item("json")
.opaque_type("std_.*")
.opaque_type("std::.*")
.opaque_type("nlohmann.*")
.opaque_type("__gnu_cxx.*")
.layout_tests(false)
.prepend_enum_name(false)
.derive_partialeq(true)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("bindgen failed to generate mtmd bindings")
.write_to_file(out_dir.join("mtmd_bindings.rs"))
.expect("failed to write mtmd_bindings.rs");
}
fn dirs_with(root: &Path, pred: &dyn Fn(&str) -> bool) -> Vec<PathBuf> {
let mut out = Vec::new();
for entry in walkdir::WalkDir::new(root).into_iter().flatten() {
if entry.file_type().is_file() {
if let Some(name) = entry.file_name().to_str() {
if pred(name) {
if let Some(parent) = entry.path().parent() {
let p = parent.to_path_buf();
if !out.contains(&p) {
out.push(p);
}
}
}
}
}
}
out
}
fn link_prebuilt(lib_dir: &Path, want_common: bool, _want_cuda: bool) {
assert!(
lib_dir.exists(),
"IK_LLAMA_CPP_LIB_DIR does not exist: {lib_dir:?}"
);
let so_dirs = dirs_with(lib_dir, &|n| {
n.starts_with("libllama.so") || n.starts_with("libggml") && n.contains(".so")
});
assert!(
!so_dirs.is_empty(),
"no libllama.so/libggml*.so found under {lib_dir:?}"
);
for d in &so_dirs {
println!("cargo:rustc-link-search=native={}", d.display());
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", d.display());
}
if want_common {
let common_dirs = dirs_with(lib_dir, &|n| n == "libcommon.a");
assert!(
!common_dirs.is_empty(),
"`common` feature set but no libcommon.a found under {lib_dir:?} (expected {lib_dir:?}/common)"
);
for d in &common_dirs {
println!("cargo:rustc-link-search=native={}", d.display());
}
println!("cargo:rustc-link-lib=static=common");
}
println!("cargo:rustc-link-lib=dylib=llama");
println!("cargo:rustc-link-lib=dylib=ggml");
}
fn cmake_build(
src: &Path,
_want_common: bool,
want_cuda: bool,
want_vulkan: bool,
want_openmp: bool,
want_native: bool,
dynamic_link: bool,
) -> PathBuf {
let mut cfg = cmake::Config::new(src);
cfg.define("GGML_MAX_CONTEXTS", "2048") .define("LLAMA_CURL", "OFF")
.define("LLAMA_BUILD_TESTS", "OFF")
.define("LLAMA_BUILD_EXAMPLES", "OFF")
.define("LLAMA_BUILD_SERVER", "OFF")
.define("BUILD_SHARED_LIBS", if dynamic_link { "ON" } else { "OFF" })
.define("GGML_NATIVE", if want_native { "ON" } else { "OFF" })
.define("GGML_OPENMP", if want_openmp { "ON" } else { "OFF" });
if want_cuda {
cfg.define("GGML_CUDA", "ON");
cfg.define("GGML_NCCL", "OFF");
}
if want_vulkan {
cfg.define("GGML_VULKAN", "ON");
}
cfg.build()
}
fn link_built(dst: &Path, want_common: bool, dynamic_link: bool) {
let build = dst.join("build");
let search_root = if build.exists() {
build
} else {
dst.to_path_buf()
};
let kind = if dynamic_link { "dylib" } else { "static" };
let ext_ok = |n: &str| {
if dynamic_link {
n.contains(".so")
} else {
n.ends_with(".a")
}
};
let mut wanted: Vec<(&str, &str)> = Vec::new();
if want_common {
wanted.push(("common", "libcommon"));
}
wanted.push(("llama", "libllama"));
wanted.push(("ggml", "libggml"));
for (link_name, file_prefix) in wanted {
let dirs = dirs_with(&search_root, &|n| n.starts_with(file_prefix) && ext_ok(n));
assert!(
!dirs.is_empty(),
"could not find {file_prefix}.* under {search_root:?} after CMake build"
);
for d in &dirs {
println!("cargo:rustc-link-search=native={}", d.display());
if dynamic_link {
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", d.display());
}
}
println!("cargo:rustc-link-lib={kind}={link_name}");
}
}
fn compile_grammar_glue(src: &Path, manifest_dir: &Path) {
let mut build = cc::Build::new();
build
.cpp(true)
.std("c++17")
.file(manifest_dir.join("wrapper_grammar.cpp"))
.include(manifest_dir)
.include(src.join("include"))
.include(src.join("ggml/include"))
.include(src.join("src")) .flag_if_supported("-fPIC")
.warnings(false);
build.compile("ik_llama_rs_grammar");
}
fn compile_common_glue(src: &Path, manifest_dir: &Path) {
let mut build = cc::Build::new();
build
.cpp(true)
.std("c++17")
.file(manifest_dir.join("wrapper_common.cpp"))
.include(manifest_dir)
.include(src.join("include"))
.include(src.join("ggml/include"))
.include(src.join("common")) .include(src.join("src")) .include(src.join("vendor")) .define("LLAMA_RS_BUILD_COMMON", None)
.flag_if_supported("-fPIC")
.warnings(false);
build.compile("ik_llama_rs_common");
}
fn compile_mtmd(src: &Path) {
let mtmd_dir = src.join("examples/mtmd");
let mut build = cc::Build::new();
build
.cpp(true)
.std("c++17")
.file(mtmd_dir.join("mtmd.cpp"))
.file(mtmd_dir.join("mtmd-audio.cpp"))
.file(mtmd_dir.join("clip.cpp"))
.file(mtmd_dir.join("mtmd-helper.cpp"))
.include(&mtmd_dir)
.include(src)
.include(src.join("include"))
.include(src.join("ggml/include"))
.include(src.join("vendor"))
.flag_if_supported("-Wno-cast-qual")
.pic(true)
.warnings(false);
build.compile("mtmd");
}
fn link_cuda() {
let candidates = [
env::var("CUDA_PATH").ok(),
Some("/opt/cuda".to_string()),
Some("/usr/local/cuda".to_string()),
];
for c in candidates.into_iter().flatten() {
let lib64 = PathBuf::from(&c).join("lib64");
if lib64.exists() {
println!("cargo:rustc-link-search=native={}", lib64.display());
let stubs = lib64.join("stubs");
if stubs.exists() {
println!("cargo:rustc-link-search=native={}", stubs.display());
}
}
}
println!("cargo:rustc-link-lib=dylib=cudart");
println!("cargo:rustc-link-lib=dylib=cublas");
println!("cargo:rustc-link-lib=dylib=cuda");
}
fn link_system(static_stdcxx: bool, want_openmp: bool) {
if static_stdcxx {
println!("cargo:rustc-link-lib=static=stdc++");
} else {
println!("cargo:rustc-link-lib=dylib=stdc++");
}
if want_openmp {
println!("cargo:rustc-link-lib=dylib=gomp");
}
println!("cargo:rustc-link-lib=dylib=m");
println!("cargo:rustc-link-lib=dylib=pthread");
println!("cargo:rustc-link-lib=dylib=dl");
}
fn write_manifest(
src: &Path,
out_dir: &Path,
cuda: bool,
vulkan: bool,
openmp: bool,
common: bool,
backend: &str,
) {
let sha = std::process::Command::new("git")
.args(["-C", &src.display().to_string(), "rev-parse", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
let target = env::var("TARGET").unwrap_or_default();
let mut backends = vec!["cpu"];
if cuda {
backends.push("cuda");
}
if vulkan {
backends.push("vulkan");
}
if openmp {
backends.push("openmp");
}
if common {
backends.push("common");
}
let manifest = format!(
"ik_sha={sha}\ntarget={target}\nGGML_MAX_CONTEXTS=2048\nbackends={}\nlink_backend={backend}\n",
backends.join("+")
);
let _ = std::fs::write(out_dir.join("ik-build.txt"), &manifest);
println!(
"cargo:warning=ik-llama-cpp-sys: ik={} target={} backends={} ({})",
&sha[..sha.len().min(8)],
target,
backends.join("+"),
backend
);
}