use cmake::Config; use glob::glob; use std::env; use std::path::{Path, PathBuf}; use std::process::Command; use walkdir::DirEntry; enum WindowsVariant { Msvc, Other, } enum AppleVariant { MacOS, Other, } enum TargetOs { Windows(WindowsVariant), Apple(AppleVariant), Linux, Android, } macro_rules! debug_log { ($($arg:tt)*) => { if std::env::var("BUILD_DEBUG").is_ok() { println!("cargo:warning=[DEBUG] {}", format!($($arg)*)); } }; } fn parse_target_os() -> Result<(TargetOs, String), String> { let target = env::var("TARGET").unwrap(); if target.contains("windows") { if target.ends_with("-windows-msvc") { Ok((TargetOs::Windows(WindowsVariant::Msvc), target)) } else { Ok((TargetOs::Windows(WindowsVariant::Other), target)) } } else if target.contains("apple") { if target.ends_with("-apple-darwin") { Ok((TargetOs::Apple(AppleVariant::MacOS), target)) } else { Ok((TargetOs::Apple(AppleVariant::Other), target)) } } else if target.contains("android") { Ok((TargetOs::Android, target)) } else if target.contains("linux") { Ok((TargetOs::Linux, target)) } else { Err(target) } } fn get_cargo_target_dir() -> Result> { let out_dir = env::var("OUT_DIR")?; let path = PathBuf::from(out_dir); let target_dir = path .ancestors() .nth(3) .ok_or("OUT_DIR is not deep enough")?; Ok(target_dir.to_path_buf()) } fn extract_lib_names(out_dir: &Path, build_shared_libs: bool) -> Vec { let lib_pattern = if cfg!(windows) { "*.lib" } else if cfg!(target_os = "macos") { if build_shared_libs { "*.dylib" } else { "*.a" } } else if build_shared_libs { "*.so" } else { "*.a" }; let libs_dir = out_dir.join("lib*"); let pattern = libs_dir.join(lib_pattern); debug_log!("Extract libs {}", pattern.display()); let mut lib_names: Vec = Vec::new(); // Process the libraries based on the pattern for entry in glob(pattern.to_str().unwrap()).unwrap() { match entry { Ok(path) => { let stem = path.file_stem().unwrap(); let stem_str = stem.to_str().unwrap(); // Remove the "lib" prefix if present let lib_name = if stem_str.starts_with("lib") { stem_str.strip_prefix("lib").unwrap_or(stem_str) } else { if path.extension() == Some(std::ffi::OsStr::new("a")) { let target = path.parent().unwrap().join(format!("lib{}.a", stem_str)); std::fs::rename(&path, &target).unwrap_or_else(|e| { panic!("Failed to rename {path:?} to {target:?}: {e:?}"); }) } stem_str }; lib_names.push(lib_name.to_string()); } Err(e) => println!("cargo:warning=error={}", e), } } lib_names } fn extract_lib_assets(out_dir: &Path) -> Vec { let shared_lib_pattern = if cfg!(windows) { "*.dll" } else if cfg!(target_os = "macos") { "*.dylib" } else { "*.so" }; let shared_libs_dir = if cfg!(windows) { "bin" } else { "lib" }; let libs_dir = out_dir.join(shared_libs_dir); let pattern = libs_dir.join(shared_lib_pattern); debug_log!("Extract lib assets {}", pattern.display()); let mut files = Vec::new(); for entry in glob(pattern.to_str().unwrap()).unwrap() { match entry { Ok(path) => { files.push(path); } Err(e) => eprintln!("cargo:warning=error={}", e), } } files } fn macos_link_search_path() -> Option { let output = Command::new("clang") .arg("--print-search-dirs") .output() .ok()?; if !output.status.success() { println!( "failed to run 'clang --print-search-dirs', continuing without a link search path" ); return None; } let stdout = String::from_utf8_lossy(&output.stdout); for line in stdout.lines() { if line.contains("libraries: =") { let path = line.split('=').nth(1)?; return Some(format!("{}/lib/darwin", path)); } } println!("failed to determine link search path, continuing without it"); None } fn is_hidden(e: &DirEntry) -> bool { e.file_name() .to_str() .map(|s| s.starts_with('.')) .unwrap_or_default() } fn main() { println!("cargo:rerun-if-changed=build.rs"); let (target_os, target_triple) = parse_target_os().unwrap_or_else(|t| panic!("Failed to parse target os {t}")); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let target_dir = get_cargo_target_dir().unwrap(); let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("Failed to get CARGO_MANIFEST_DIR"); let llama_src = Path::new(&manifest_dir).join("llama.cpp"); let build_shared_libs = cfg!(feature = "dynamic-link"); let build_shared_libs = std::env::var("LLAMA_BUILD_SHARED_LIBS") .map(|v| v == "1") .unwrap_or(build_shared_libs); let profile = env::var("LLAMA_LIB_PROFILE").unwrap_or("Release".to_string()); let static_crt = env::var("LLAMA_STATIC_CRT") .map(|v| v == "1") .unwrap_or(false); println!("cargo:rerun-if-env-changed=LLAMA_LIB_PROFILE"); println!("cargo:rerun-if-env-changed=LLAMA_BUILD_SHARED_LIBS"); println!("cargo:rerun-if-env-changed=LLAMA_STATIC_CRT"); debug_log!("TARGET: {}", target_triple); debug_log!("CARGO_MANIFEST_DIR: {}", manifest_dir); debug_log!("TARGET_DIR: {}", target_dir.display()); debug_log!("OUT_DIR: {}", out_dir.display()); debug_log!("BUILD_SHARED: {}", build_shared_libs); // Make sure that changes to the llama.cpp project trigger a rebuild. let rebuild_on_children_of = [ llama_src.join("src"), llama_src.join("ggml/src"), llama_src.join("common"), ]; for entry in walkdir::WalkDir::new(&llama_src) .into_iter() .filter_entry(|e| !is_hidden(e)) { let entry = entry.expect("Failed to obtain entry"); let rebuild = entry .file_name() .to_str() .map(|f| f.starts_with("CMake")) .unwrap_or_default() || rebuild_on_children_of .iter() .any(|src_folder| entry.path().starts_with(src_folder)); if rebuild { println!("cargo:rerun-if-changed={}", entry.path().display()); } } // Speed up build env::set_var( "CMAKE_BUILD_PARALLEL_LEVEL", std::thread::available_parallelism() .unwrap() .get() .to_string(), ); // Bindings let bindings = bindgen::Builder::default() .header("wrapper.h") .clang_arg(format!("-I{}", llama_src.join("include").display())) .clang_arg(format!("-I{}", llama_src.join("ggml/include").display())) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .derive_partialeq(true) .allowlist_function("ggml_.*") .allowlist_type("ggml_.*") .allowlist_function("llama_.*") .allowlist_type("llama_.*") .prepend_enum_name(false) .generate() .expect("Failed to generate bindings"); // Write the generated bindings to an output file let bindings_path = out_dir.join("bindings.rs"); bindings .write_to_file(bindings_path) .expect("Failed to write bindings"); println!("cargo:rerun-if-changed=wrapper.h"); debug_log!("Bindings Created"); // Build with Cmake let mut config = Config::new(&llama_src); // Would require extra source files to pointlessly // be included in what's uploaded to and downloaded from // crates.io, so deactivating these instead config.define("LLAMA_BUILD_TESTS", "OFF"); config.define("LLAMA_BUILD_EXAMPLES", "OFF"); config.define("LLAMA_BUILD_SERVER", "OFF"); config.define("LLAMA_CURL", "OFF"); config.define( "BUILD_SHARED_LIBS", if build_shared_libs { "ON" } else { "OFF" }, ); if matches!(target_os, TargetOs::Apple(_)) { config.define("GGML_BLAS", "OFF"); } if (matches!(target_os, TargetOs::Windows(WindowsVariant::Msvc)) && matches!(profile.as_str(), "Release" | "RelWithDebInfo" | "MinSizeRel")) { // Debug Rust builds under MSVC turn off optimization even though we're ideally building the release profile of llama.cpp. // Looks like an upstream bug: // https://github.com/rust-lang/cmake-rs/issues/240 // For now explicitly reinject the optimization flags that a CMake Release build is expected to have on in this scenario. // This fixes CPU inference performance when part of a Rust debug build. for flag in &["/O2", "/DNDEBUG", "/Ob2"] { config.cflag(flag); config.cxxflag(flag); } } config.static_crt(static_crt); if matches!(target_os, TargetOs::Android) { // build flags for android taken from this doc // https://github.com/ggerganov/llama.cpp/blob/master/docs/android.md let android_ndk = env::var("ANDROID_NDK") .expect("Please install Android NDK and ensure that ANDROID_NDK env variable is set"); println!("cargo::rerun-if-env-changed=ANDROID_NDK"); config.define( "CMAKE_TOOLCHAIN_FILE", format!("{android_ndk}/build/cmake/android.toolchain.cmake"), ); if env::var("ANDROID_PLATFORM").is_ok() { println!("cargo::rerun-if-env-changed=ANDROID_PLATFORM"); } else { config.define("ANDROID_PLATFORM", "android-28"); } if target_triple.contains("aarch64") { config.cflag("-march=armv8.7a"); config.cxxflag("-march=armv8.7a"); } else if target_triple.contains("armv7") { config.cflag("-march=armv8.7a"); config.cxxflag("-march=armv8.7a"); } else if target_triple.contains("x86_64") { config.cflag("-march=x86-64"); config.cxxflag("-march=x86-64"); } else if target_triple.contains("i686") { config.cflag("-march=i686"); config.cxxflag("-march=i686"); } else { // Rather than guessing just fail. panic!("Unsupported Android target {target_triple}"); } config.define("GGML_LLAMAFILE", "OFF"); if cfg!(feature = "shared-stdcxx") { println!("cargo:rustc-link-lib=dylib=stdc++"); println!("cargo:rustc-link-lib=c++_shared"); } } if cfg!(feature = "vulkan") { config.define("GGML_VULKAN", "ON"); match target_os { TargetOs::Windows(_) => { let vulkan_path = env::var("VULKAN_SDK").expect( "Please install Vulkan SDK and ensure that VULKAN_SDK env variable is set", ); let vulkan_lib_path = Path::new(&vulkan_path).join("Lib"); println!("cargo:rustc-link-search={}", vulkan_lib_path.display()); println!("cargo:rustc-link-lib=vulkan-1"); } TargetOs::Linux => { println!("cargo:rustc-link-lib=vulkan"); } _ => (), } } if cfg!(feature = "cuda") { config.define("GGML_CUDA", "ON"); if cfg!(feature = "cuda-no-vmm") { config.define("GGML_CUDA_NO_VMM", "ON"); } } // Android doesn't have OpenMP support AFAICT and openmp is a default feature. Do this here // rather than modifying the defaults in Cargo.toml just in case someone enables the OpenMP feature // and tries to build for Android anyway. if cfg!(feature = "openmp") && !matches!(target_os, TargetOs::Android) { config.define("GGML_OPENMP", "ON"); } else { config.define("GGML_OPENMP", "OFF"); } // General config .profile(&profile) .very_verbose(std::env::var("CMAKE_VERBOSE").is_ok()) // Not verbose by default .always_configure(false); let build_dir = config.build(); let build_info_src = llama_src.join("common/build-info.cpp"); let build_info_target = build_dir.join("build-info.cpp"); std::fs::rename(&build_info_src,&build_info_target).unwrap_or_else(|move_e| { // Rename may fail if the target directory is on a different filesystem/disk from the source. // Fall back to copy + delete to achieve the same effect in this case. std::fs::copy(&build_info_src, &build_info_target).unwrap_or_else(|copy_e| { panic!("Failed to rename {build_info_src:?} to {build_info_target:?}. Move failed with {move_e:?} and copy failed with {copy_e:?}"); }); std::fs::remove_file(&build_info_src).unwrap_or_else(|e| { panic!("Failed to delete {build_info_src:?} after copying to {build_info_target:?}: {e:?} (move failed because {move_e:?})"); }); }); // Search paths println!("cargo:rustc-link-search={}", out_dir.join("lib").display()); println!( "cargo:rustc-link-search={}", out_dir.join("lib64").display() ); println!("cargo:rustc-link-search={}", build_dir.display()); if cfg!(feature = "cuda") && !build_shared_libs { println!("cargo:rerun-if-env-changed=CUDA_PATH"); for lib_dir in find_cuda_helper::find_cuda_lib_dirs() { println!("cargo:rustc-link-search=native={}", lib_dir.display()); } // Logic from ggml-cuda/CMakeLists.txt println!("cargo:rustc-link-lib=static=cudart_static"); if matches!(target_os, TargetOs::Windows(_)) { println!("cargo:rustc-link-lib=static=cublas"); println!("cargo:rustc-link-lib=static=cublasLt"); } else { println!("cargo:rustc-link-lib=static=cublas_static"); println!("cargo:rustc-link-lib=static=cublasLt_static"); } // Need to link against libcuda.so unless GGML_CUDA_NO_VMM is defined. if !cfg!(feature = "cuda-no-vmm") { println!("cargo:rustc-link-lib=cuda"); } println!("cargo:rustc-link-lib=static=culibos"); } // Link libraries let llama_libs_kind = if build_shared_libs { "dylib" } else { "static" }; let llama_libs = extract_lib_names(&out_dir, build_shared_libs); assert_ne!(llama_libs.len(), 0); for lib in llama_libs { let link = format!("cargo:rustc-link-lib={}={}", llama_libs_kind, lib); debug_log!("LINK {link}",); println!("{link}",); } // OpenMP if cfg!(feature = "openmp") && target_triple.contains("gnu") { println!("cargo:rustc-link-lib=gomp"); } match target_os { TargetOs::Windows(WindowsVariant::Msvc) => { if cfg!(debug_assertions) { println!("cargo:rustc-link-lib=dylib=msvcrtd"); } } TargetOs::Linux => { println!("cargo:rustc-link-lib=dylib=stdc++"); } TargetOs::Apple(variant) => { println!("cargo:rustc-link-lib=framework=Foundation"); println!("cargo:rustc-link-lib=framework=Metal"); println!("cargo:rustc-link-lib=framework=MetalKit"); println!("cargo:rustc-link-lib=framework=Accelerate"); println!("cargo:rustc-link-lib=c++"); match variant { AppleVariant::MacOS => { // On (older) OSX we need to link against the clang runtime, // which is hidden in some non-default path. // // More details at https://github.com/alexcrichton/curl-rust/issues/279. if let Some(path) = macos_link_search_path() { println!("cargo:rustc-link-lib=clang_rt.osx"); println!("cargo:rustc-link-search={}", path); } } AppleVariant::Other => (), } } _ => (), } // copy DLLs to target if build_shared_libs { let libs_assets = extract_lib_assets(&out_dir); for asset in libs_assets { let asset_clone = asset.clone(); let filename = asset_clone.file_name().unwrap(); let filename = filename.to_str().unwrap(); let dst = target_dir.join(filename); debug_log!("HARD LINK {} TO {}", asset.display(), dst.display()); if !dst.exists() { std::fs::hard_link(asset.clone(), dst).unwrap(); } // Copy DLLs to examples as well if target_dir.join("examples").exists() { let dst = target_dir.join("examples").join(filename); debug_log!("HARD LINK {} TO {}", asset.display(), dst.display()); if !dst.exists() { std::fs::hard_link(asset.clone(), dst).unwrap(); } } // Copy DLLs to target/profile/deps as well for tests let dst = target_dir.join("deps").join(filename); debug_log!("HARD LINK {} TO {}", asset.display(), dst.display()); if !dst.exists() { std::fs::hard_link(asset.clone(), dst).unwrap(); } } } }