use std::{
env,
path::{Path, PathBuf},
process::Command,
};
const EXPECTED_MLX_REV: &str = "68cf2fddd8de5edd8ab3d926391772b2e2cedad8"; const EXPECTED_GGUFLIB_REV: &str = "8fa6eb65236618e28fd7710a0fba565f7faa1848";
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rerun-if-changed=vendor/mlx-c");
println!("cargo:rerun-if-changed=vendor/mlx");
println!("cargo:rerun-if-changed=vendor/gguflib");
println!("cargo:rerun-if-changed=shim/mlxrs_shim.cpp");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
if target_os != "macos" || target_arch != "aarch64" {
panic!(
"mlxrs-sys M1 supports aarch64-apple-darwin only; \
got target_os={target_os}, target_arch={target_arch}"
);
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let vendor_dir = manifest_dir.join("vendor");
let mlx_c_root = vendor_dir.join("mlx-c");
let mlx_root = vendor_dir.join("mlx");
let gguflib_root = vendor_dir.join("gguflib");
for (name, root, sentinel) in [
("mlx-c", &mlx_c_root, "CMakeLists.txt"),
("mlx", &mlx_root, "CMakeLists.txt"),
("gguflib", &gguflib_root, "gguflib.c"),
] {
if !root.join(sentinel).exists() {
panic!(
"vendor/{name}/{sentinel} missing. Run:\n\
\tgit submodule update --init --recursive"
);
}
}
check_submodule_rev(&mlx_root, EXPECTED_MLX_REV, "mlx").unwrap_or_else(|msg| panic!("{msg}"));
check_submodule_rev(&gguflib_root, EXPECTED_GGUFLIB_REV, "gguflib")
.unwrap_or_else(|msg| panic!("{msg}"));
let dst = cmake::Config::new(&mlx_c_root)
.define("BUILD_SHARED_LIBS", "OFF")
.define("MLX_C_BUILD_EXAMPLES", "OFF")
.define("MLX_BUILD_TESTS", "OFF")
.define("MLX_BUILD_BENCHMARKS", "OFF")
.define("MLX_BUILD_PYTHON_BINDINGS", "OFF")
.define("CMAKE_CXX_STANDARD", "20")
.define("CMAKE_BUILD_TYPE", "Release")
.define(
"FETCHCONTENT_SOURCE_DIR_MLX",
mlx_root.to_str().expect("mlx vendor path is valid UTF-8"),
)
.define(
"FETCHCONTENT_SOURCE_DIR_GGUFLIB",
gguflib_root
.to_str()
.expect("gguflib vendor path is valid UTF-8"),
)
.build();
let lib_dir = dst.join("lib");
let build_dir = dst.join("build");
let libmlx_src = find_libmlx(&build_dir)
.unwrap_or_else(|| panic!("could not find libmlx.a under {}", build_dir.display()));
let libmlx_dst = lib_dir.join("libmlx.a");
std::fs::copy(&libmlx_src, &libmlx_dst).unwrap_or_else(|e| {
panic!(
"failed to copy {} -> {}: {e}",
libmlx_src.display(),
libmlx_dst.display()
)
});
let libgguflib_src = find_libgguflib(&build_dir).unwrap_or_else(|| {
panic!(
"could not find libgguflib.a under {} (MLX core builds it from \
FetchContent'd gguf-tools when MLX_BUILD_GGUF is ON, its default)",
build_dir.display()
)
});
let libgguflib_dst = lib_dir.join("libgguflib.a");
std::fs::copy(&libgguflib_src, &libgguflib_dst).unwrap_or_else(|e| {
panic!(
"failed to copy {} -> {}: {e}",
libgguflib_src.display(),
libgguflib_dst.display()
)
});
if !mlx_root.join("mlx/stream.h").exists() {
panic!(
"mlx C++ headers not found at {} (expected mlx/stream.h). The shim \
needs the vendored mlx submodule.",
mlx_root.display()
);
}
cc::Build::new()
.cpp(true)
.std("c++20")
.file("shim/mlxrs_shim.cpp")
.include(&mlx_root)
.compile("mlxrs_shim");
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=static=mlxc");
println!("cargo:rustc-link-lib=static=mlx");
println!("cargo:rustc-link-lib=static=gguflib");
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=MetalPerformanceShaders");
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=QuartzCore");
println!("cargo:rustc-link-lib=framework=Accelerate");
println!("cargo:rustc-link-lib=dylib=c++");
}
fn check_submodule_rev(
submodule_path: &Path,
expected: &str,
friendly_name: &str,
) -> Result<(), String> {
if !submodule_path.join(".git").exists() {
return Ok(());
}
let path_str = match submodule_path.to_str() {
Some(s) => s,
None => return Ok(()), };
let output = match Command::new("git")
.args(["-C", path_str, "rev-parse", "HEAD"])
.output()
{
Ok(o) => o,
Err(_) => return Ok(()), };
if !output.status.success() {
return Err(format!(
"vendored submodule `{friendly_name}` at {} has git metadata but \
`git rev-parse HEAD` failed (exit {:?}, stderr: {}). Cannot verify \
pinned revision. Re-initialize the submodule:\n\
\tgit submodule deinit -f {}\n\
\tgit submodule update --init --recursive",
submodule_path.display(),
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim_end(),
submodule_path.display(),
));
}
let actual = String::from_utf8_lossy(&output.stdout).trim().to_string();
if actual != expected {
let name_upper = friendly_name.to_uppercase();
return Err(format!(
"vendored submodule `{friendly_name}` at {} is at revision {actual} \
but mlxrs-sys expects {expected}.\n\
\n\
If you intentionally updated the submodule, also update \
EXPECTED_{name_upper}_REV in mlxrs-sys/build.rs (both must commit \
in the same change so CI catches drift).\n\
\n\
Otherwise restore the pinned revision:\n\
\tgit -C {} checkout {expected}\n\
\tgit add {}",
submodule_path.display(),
submodule_path.display(),
submodule_path.display(),
));
}
Ok(())
}
fn find_libmlx(start: &Path) -> Option<PathBuf> {
let matches: Vec<PathBuf> = walkdir::WalkDir::new(start)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_name() == "libmlx.a")
.map(|e| e.into_path())
.collect();
match matches.len() {
0 => None,
1 => matches.into_iter().next(),
_ => panic!(
"found {} libmlx.a candidates under {}; expected exactly one. \
Either MLX upstream changed its build layout, or a stale archive \
was left behind. Candidates:\n {}",
matches.len(),
start.display(),
matches
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n "),
),
}
}
fn find_libgguflib(start: &Path) -> Option<PathBuf> {
let matches: Vec<PathBuf> = walkdir::WalkDir::new(start)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_name() == "libgguflib.a")
.map(|e| e.into_path())
.collect();
match matches.len() {
0 => None,
1 => matches.into_iter().next(),
_ => panic!(
"found {} libgguflib.a candidates under {}; expected exactly one. \
Either MLX upstream changed its gguf build layout, or a stale \
archive was left behind. Candidates:\n {}",
matches.len(),
start.display(),
matches
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n "),
),
}
}