#[path = "native_build.rs"]
mod native_build;
use std::{
env,
error::Error,
path::{Path, PathBuf},
};
use pkg_config::Config as PkgConfig;
const VENDORED_HEADERS_DIRECTORY: &str = "vendor/mlx-c";
const MLX_C_PREFIX_VARIABLE: &str = "MLX_C_RUST_MLX_C_PREFIX";
const BINDGEN_FUNCTION_ALLOWLIST: &str = "mlx_.*";
const BINDGEN_TYPE_ALLOWLIST: &str = "mlx_.*";
const BRIDGED_INVENTORY_FILE_NAME: &str = "bridged_inventory.rs";
const FREESTANDING_HEADERS_DIRECTORY: &str = "vendor/freestanding";
fn self_contained_parse_arguments(freestanding_directory: &Path) -> Vec<String> {
vec![
"--target=aarch64-apple-darwin".to_owned(),
"-Xclang".to_owned(),
"-nostdsysteminc".to_owned(),
format!("-isystem{}", freestanding_directory.display()),
]
}
fn main() -> Result<(), Box<dyn Error>> {
let manifest_directory =
PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").ok_or("CARGO_MANIFEST_DIR is unset")?);
let output_directory = PathBuf::from(env::var_os("OUT_DIR").ok_or("OUT_DIR is unset")?);
let include_directory = manifest_directory.join(VENDORED_HEADERS_DIRECTORY);
let freestanding_directory = manifest_directory.join(FREESTANDING_HEADERS_DIRECTORY);
let mlx_c_header = include_directory.join("mlx/c/mlx.h");
if !mlx_c_header.is_file() {
return Err(format!(
"missing vendored MLX C umbrella header at {}",
mlx_c_header.display()
)
.into());
}
println!("cargo:rerun-if-changed={}", include_directory.display());
if link_feature_is_enabled() {
println!("cargo:rerun-if-env-changed={MLX_C_PREFIX_VARIABLE}");
}
let bindings = bindgen::Builder::default()
.header(mlx_c_header.to_string_lossy())
.clang_arg(format!("-I{}", include_directory.display()))
.clang_args(self_contained_parse_arguments(&freestanding_directory))
.allowlist_function(BINDGEN_FUNCTION_ALLOWLIST)
.allowlist_type(BINDGEN_TYPE_ALLOWLIST)
.generate_comments(false)
.generate()?;
let bindings_content = bindings.to_string();
write_bridged_inventory(&bindings_content, &output_directory)?;
bindings.write_to_file(output_directory.join("mlx_c_bindings.rs"))?;
if link_feature_is_enabled() {
emit_link_instructions();
}
Ok(())
}
fn link_feature_is_enabled() -> bool {
env::var_os("CARGO_FEATURE_LINK").is_some_and(|feature_value| feature_value != "0")
}
fn emit_link_instructions() {
if !cfg!(target_os = "macos") {
println!(
"cargo:warning=no MLX-C library can be built on this host operating system; \
the crate compiles and documents, but linking a binary that uses it needs \
an Apple silicon Mac"
);
return;
}
if let Some(prefix) = prefix_from_environment() {
emit_prefix_link_flags(&prefix);
return;
}
if let Some(prefix) = homebrew_prefix_with_mlxc() {
emit_prefix_link_flags(&prefix);
return;
}
if let Ok(mlx_c) = PkgConfig::new().atleast_version("0.7.0").probe("mlx-c") {
for library_name in &mlx_c.libs {
println!("cargo:rustc-link-lib={library_name}");
}
for search_path in &mlx_c.link_paths {
println!("cargo:rustc-link-search=native={}", search_path.display());
}
for framework in &mlx_c.frameworks {
println!("cargo:rustc-link-lib=framework={framework}");
}
for framework_path in &mlx_c.framework_paths {
println!(
"cargo:rustc-link-search=framework={}",
framework_path.display()
);
}
return;
}
match native_build::ensure_pinned_pair() {
native_build::NativeBuildOutcome::Built | native_build::NativeBuildOutcome::Reused => {
if let Some(prefix) = native_build::shared_prefix()
.filter(|candidate| native_build::prefix_carries_mlxc_library(candidate))
{
emit_prefix_link_flags(&prefix);
return;
}
println!(
"cargo:warning=the native build reported success but the shared prefix \
holds no MLX-C library; continuing without link flags"
);
}
native_build::NativeBuildOutcome::Failed => println!(
"cargo:warning=no MLX-C library could be located or built; the crate compiles, \
but linking a binary that uses it will report undefined MLX symbols. Set \
{MLX_C_PREFIX_VARIABLE}, run `cargo xtask build-native`, install mlx-c \
through Homebrew, or install CMake (brew install cmake) so the pinned \
sources can be built automatically."
),
}
}
fn prefix_from_environment() -> Option<PathBuf> {
let prefix = PathBuf::from(env::var_os(MLX_C_PREFIX_VARIABLE)?);
if native_build::prefix_carries_mlxc_library(&prefix) {
return Some(prefix);
}
println!(
"cargo:warning=MLX_C_RUST_MLX_C_PREFIX is set but {} holds no MLX-C library; \
continuing down the discovery ladder",
prefix.join("lib").display()
);
None
}
fn homebrew_prefix_with_mlxc() -> Option<PathBuf> {
let mut candidates = vec![PathBuf::from("/opt/homebrew")];
if let Some(home) = env::var_os("HOME") {
candidates.push(PathBuf::from(home).join("homebrew"));
}
candidates
.into_iter()
.find(|prefix| native_build::prefix_carries_mlxc_library(prefix))
}
fn emit_prefix_link_flags(prefix: &Path) {
let library_directory = prefix.join("lib");
println!(
"cargo:rustc-link-search=native={}",
library_directory.display()
);
if library_directory.join("libmlxc.dylib").is_file() {
println!("cargo:rustc-link-lib=dylib=mlxc");
println!("cargo:rustc-link-lib=dylib=mlx");
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
library_directory.display()
);
} else {
println!("cargo:rustc-link-lib=static=mlxc");
println!("cargo:rustc-link-lib=static=mlx");
println!("cargo:rustc-link-lib=static=jaccl");
for framework in ["Metal", "Foundation", "QuartzCore", "Accelerate"] {
println!("cargo:rustc-link-lib=framework={framework}");
}
println!("cargo:rustc-link-lib=dylib=c++");
}
}
fn write_bridged_inventory(
bindings_content: &str,
output_directory: &Path,
) -> Result<(), Box<dyn Error>> {
let identifier_end = |segment: &str| {
segment
.find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
.unwrap_or(segment.len())
};
let mut bridged_functions: Vec<String> = bindings_content
.split("pub fn ")
.skip(1)
.map(|segment| &segment[..identifier_end(segment)])
.filter(|name| name.starts_with("mlx_"))
.map(str::to_owned)
.collect();
bridged_functions.sort();
bridged_functions.dedup();
bridged_functions.push("mlx_error".to_owned());
let mut bridged_types: Vec<String> = ["pub struct ", "pub enum ", "pub union ", "pub type "]
.iter()
.flat_map(|marker| {
bindings_content
.split(marker)
.skip(1)
.map(|segment| &segment[..identifier_end(segment)])
.filter(|name| name.starts_with("mlx_"))
.map(str::to_owned)
.collect::<Vec<String>>()
})
.collect();
for alias_segment in bindings_content.split("pub use self::").skip(1) {
let Some((_tag, alias_with_rest)) = alias_segment.split_once(" as ") else {
continue;
};
let alias = &alias_with_rest[..identifier_end(alias_with_rest)];
if alias.starts_with("mlx_") {
bridged_types.push(alias.to_owned());
}
}
bridged_types.sort();
bridged_types.dedup();
let inventory_source = format!(
"// Generated by this crate's build script from the bindgen output.\n\
// Every listed symbol exists in the compiled bridge; the coverage\n\
// contract compares this inventory against the vendored headers.\n\n\
/// Every MLX-C function bridged by the generated raw declarations.\n\
pub const BRIDGED_FUNCTIONS: [&str; {}] = {:?};\n\n\
/// Every MLX-C type bridged by the generated raw declarations.\n\
pub const BRIDGED_TYPES: [&str; {}] = {:?};\n",
bridged_functions.len(),
bridged_functions,
bridged_types.len(),
bridged_types,
);
std::fs::write(
output_directory.join(BRIDGED_INVENTORY_FILE_NAME),
inventory_source,
)?;
Ok(())
}