use std::{
fs::OpenOptions,
io::Write,
path::{Path, PathBuf},
process::Command,
time::Instant,
};
use sha2::{Digest, Sha256};
pub const PINNED_MLX_TAG: &str = "v0.32.4";
pub const PINNED_MLX_C_TAG: &str = "v0.7.0";
pub const PINNED_MLX_TARBALL_SHA256: &str =
"0276db8d7bf1af8db6d4ae25c7c550abea4d5b3b6a16207610a5d4a0c19fe1a0";
pub const PINNED_MLX_C_TARBALL_SHA256: &str =
"ee726bb38e191bb3c516a6bae47dc8abad9e5f273873385839019ce46bfceab5";
const PINNED_TARBALL_BASE_URL: &str = "https://github.com";
const PINNED_PAIR_BUILD_RECIPE: &str = "recipe-5-prefix-metallib";
#[path = "native_forward_edits.rs"]
mod native_forward_edits;
use native_forward_edits::apply_forward_source_edits;
pub const NATIVE_BUILD_PROGRESS_FILE_VARIABLE: &str = "MLX_C_RUST_NATIVE_PROGRESS_FILE";
const MLX_C_SHARED_CACHE_DIRECTORY: &str = ".cache/mlx-c-rust/native";
const COMPLETION_MARKER_DIRECTORY: &str = "complete";
pub fn digest_lowercase_hex(digest_bytes: &[u8]) -> String {
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut hex_characters = String::with_capacity(digest_bytes.len() * 2);
for &digest_byte in digest_bytes {
hex_characters.push(HEX_DIGITS[(digest_byte >> 4) as usize] as char);
hex_characters.push(HEX_DIGITS[(digest_byte & 0x0f) as usize] as char);
}
hex_characters
}
pub fn pinned_pair_identity() -> String {
let mut hasher = Sha256::new();
hasher.update(PINNED_MLX_TAG);
hasher.update(PINNED_MLX_TARBALL_SHA256);
hasher.update(PINNED_MLX_C_TAG);
hasher.update(PINNED_MLX_C_TARBALL_SHA256);
hasher.update(PINNED_PAIR_BUILD_RECIPE);
digest_lowercase_hex(hasher.finalize().as_ref())[..16].to_owned()
}
pub fn shared_prefix() -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
let home = PathBuf::from(home);
Some(
home.join(MLX_C_SHARED_CACHE_DIRECTORY)
.join(pinned_pair_identity())
.join("prefix"),
)
}
pub fn prefix_carries_mlxc_library(prefix: &Path) -> bool {
let library_directory = prefix.join("lib");
library_directory.join("libmlxc.dylib").is_file()
|| library_directory.join("libmlxc.a").is_file()
}
pub enum NativeBuildOutcome {
Reused,
Built,
Failed,
}
pub fn ensure_pinned_pair() -> NativeBuildOutcome {
let progress_file_path = std::env::var_os(NATIVE_BUILD_PROGRESS_FILE_VARIABLE)
.map(PathBuf::from)
.filter(|path| path.is_absolute());
if let Some(raw_path) = std::env::var_os(NATIVE_BUILD_PROGRESS_FILE_VARIABLE)
.filter(|path| !Path::new(path).is_absolute())
{
println!(
"cargo:warning={NATIVE_BUILD_PROGRESS_FILE_VARIABLE} must be an absolute path: {}",
raw_path.to_string_lossy()
);
}
let started_at = Instant::now();
emit(
progress_file_path.as_deref(),
&format!(
"status=start identity={} mlx={} mlx_c={}",
pinned_pair_identity(),
PINNED_MLX_TAG,
PINNED_MLX_C_TAG
),
);
let Some(prefix) = shared_prefix() else {
emit(
progress_file_path.as_deref(),
"status=failed reason=no-home",
);
return NativeBuildOutcome::Failed;
};
if prefix_carries_mlxc_library(&prefix) {
emit(
progress_file_path.as_deref(),
&format!(
"status=complete outcome=reused prefix={} elapsed_seconds={:.3}",
prefix.display(),
started_at.elapsed().as_secs_f64()
),
);
return NativeBuildOutcome::Reused;
}
let working_directory = prefix
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let marker_directory = working_directory.join(COMPLETION_MARKER_DIRECTORY);
let _ = std::fs::create_dir_all(&marker_directory);
for required_tool in ["curl", "tar", "cmake"] {
if Command::new(required_tool)
.arg("--version")
.output()
.is_err()
{
println!(
"cargo:warning=automatic MLX build needs `{required_tool}` on PATH \
(brew install {required_tool}); no MLX-C library will be linked"
);
emit(
progress_file_path.as_deref(),
&format!("status=failed reason=missing-tool tool={required_tool}"),
);
return NativeBuildOutcome::Failed;
}
}
let download_operation = begin(progress_file_path.as_deref(), "download-and-verify");
let Some(mlx_source) = download_verified_tarball(
&marker_directory,
&working_directory,
"mlx.tar.gz",
&format!(
"{PINNED_TARBALL_BASE_URL}/ml-explore/mlx/archive/refs/tags/{PINNED_MLX_TAG}.tar.gz"
),
PINNED_MLX_TARBALL_SHA256,
) else {
download_operation.complete("failed");
return NativeBuildOutcome::Failed;
};
let Some(mlx_c_source) = download_verified_tarball(
&marker_directory,
&working_directory,
"mlx-c.tar.gz",
&format!(
"{PINNED_TARBALL_BASE_URL}/ml-explore/mlx-c/archive/refs/tags/{PINNED_MLX_C_TAG}.tar.gz"
),
PINNED_MLX_C_TARBALL_SHA256,
) else {
download_operation.complete("failed");
return NativeBuildOutcome::Failed;
};
download_operation.complete("ok");
let mlx_directory = working_directory.join("mlx");
let mlx_c_directory = working_directory.join("mlx-c");
let extract_operation = begin(progress_file_path.as_deref(), "extract");
if !mlx_directory.is_dir() {
if run_step(
&working_directory,
Command::new("tar").arg("xzf").arg(&mlx_source),
)
.is_none()
{
extract_operation.complete("failed");
return NativeBuildOutcome::Failed;
}
let _ = std::fs::rename(
working_directory.join(format!("mlx-{}", PINNED_MLX_TAG.trim_start_matches('v'))),
&mlx_directory,
);
}
if !mlx_c_directory.is_dir() {
if run_step(
&working_directory,
Command::new("tar").arg("xzf").arg(&mlx_c_source),
)
.is_none()
{
extract_operation.complete("failed");
return NativeBuildOutcome::Failed;
}
let _ = std::fs::rename(
working_directory.join(format!(
"mlx-c-{}",
PINNED_MLX_C_TAG.trim_start_matches('v')
)),
&mlx_c_directory,
);
}
extract_operation.complete("ok");
let forward_edit_operation = begin(progress_file_path.as_deref(), "forward-edits");
if apply_forward_source_edits(&mlx_c_directory).is_none() {
forward_edit_operation.complete("failed");
return NativeBuildOutcome::Failed;
}
forward_edit_operation.complete("ok");
let parallelism = std::env::var("NUM_JOBS").unwrap_or_else(|_| "4".to_owned());
if let Err(create_error) = std::fs::create_dir_all(prefix.join("lib")) {
println!(
"cargo:warning=could not create the prefix library directory {}: {create_error}",
prefix.join("lib").display()
);
return NativeBuildOutcome::Failed;
}
let mlx_build = working_directory.join("mlx-build");
let mlx_marker = marker_directory.join("mlx");
if !mlx_marker.exists() {
let operation = begin(progress_file_path.as_deref(), "compile-mlx");
let configured = run_step(
&working_directory,
Command::new("cmake")
.arg("-S")
.arg(&mlx_directory)
.arg("-B")
.arg(&mlx_build)
.arg("-DCMAKE_BUILD_TYPE=Release")
.arg(format!("-DMLX_METAL_PATH={}", prefix.join("lib").display()))
.arg("-DBUILD_SHARED_LIBS=OFF")
.arg("-DMLX_METAL_JIT=ON")
.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix.display())),
);
if configured.is_none() {
operation.complete("failed");
if let Err(purge_error) = std::fs::remove_dir_all(&mlx_build) {
println!(
"cargo:warning=failed to purge the poisoned CMake build tree at {}: \
{purge_error}",
mlx_build.display()
);
}
return NativeBuildOutcome::Failed;
}
let built_and_installed = configured
.and_then(|()| {
run_step(
&working_directory,
Command::new("cmake")
.arg("--build")
.arg(&mlx_build)
.arg("--parallel")
.arg(¶llelism),
)
})
.and_then(|()| {
run_step(
&working_directory,
Command::new("cmake").arg("--install").arg(&mlx_build),
)
});
if built_and_installed.is_none() {
operation.complete("failed");
return NativeBuildOutcome::Failed;
}
operation.complete("ok");
let _ = std::fs::write(&mlx_marker, b"complete");
}
let jaccl_config_directory = prefix.join("lib/cmake/jaccl");
if jaccl_config_directory.join("jacclTargets.cmake").is_file()
&& !jaccl_config_directory.join("jacclConfig.cmake").is_file()
&& let Err(config_write_error) = std::fs::write(
jaccl_config_directory.join("jacclConfig.cmake"),
"# Generated by the mlx-c-rust native build engine: MLX's install\n\
# ships jaccl's targets without the package config that\n\
# find_package(jaccl) searches for. Completing the packaging.\n\
if(NOT TARGET jaccl::jaccl)\n\
include(\"${CMAKE_CURRENT_LIST_DIR}/jacclTargets.cmake\")\n\
endif()\n",
)
{
println!(
"cargo:warning=could not write the generated jaccl package config at {}: \
{config_write_error}; the MLX-C configure will fail to find jaccl",
jaccl_config_directory.join("jacclConfig.cmake").display()
);
return NativeBuildOutcome::Failed;
}
let mlx_c_build = working_directory.join("mlx-c-build");
let mlx_c_marker = marker_directory.join("mlx-c");
if !mlx_c_marker.exists() {
let operation = begin(progress_file_path.as_deref(), "compile-mlx-c");
let configured = run_step(
&working_directory,
Command::new("cmake")
.arg("-S")
.arg(&mlx_c_directory)
.arg("-B")
.arg(&mlx_c_build)
.arg("-DCMAKE_BUILD_TYPE=Release")
.arg("-DBUILD_SHARED_LIBS=OFF")
.arg("-DMLX_C_BUILD_EXAMPLES=OFF")
.arg("-DMLX_C_USE_SYSTEM_MLX=ON")
.arg(format!("-DCMAKE_PREFIX_PATH={}", prefix.display()))
.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix.display())),
);
if configured.is_none() {
operation.complete("failed");
if let Err(purge_error) = std::fs::remove_dir_all(&mlx_c_build) {
println!(
"cargo:warning=failed to purge the poisoned CMake build tree at {}: \
{purge_error}",
mlx_c_build.display()
);
}
return NativeBuildOutcome::Failed;
}
let built_and_installed = configured
.and_then(|()| {
run_step(
&working_directory,
Command::new("cmake")
.arg("--build")
.arg(&mlx_c_build)
.arg("--parallel")
.arg(¶llelism),
)
})
.and_then(|()| {
run_step(
&working_directory,
Command::new("cmake").arg("--install").arg(&mlx_c_build),
)
});
if built_and_installed.is_none() {
operation.complete("failed");
return NativeBuildOutcome::Failed;
}
operation.complete("ok");
let _ = std::fs::write(&mlx_c_marker, b"complete");
}
if prefix_carries_mlxc_library(&prefix) {
emit(
progress_file_path.as_deref(),
&format!(
"status=complete outcome=built prefix={} elapsed_seconds={:.3}",
prefix.display(),
started_at.elapsed().as_secs_f64()
),
);
NativeBuildOutcome::Built
} else {
emit(
progress_file_path.as_deref(),
"status=failed reason=library-missing-after-build",
);
NativeBuildOutcome::Failed
}
}
fn begin(progress_file_path: Option<&Path>, operation_name: &str) -> OperationTimer {
emit(
progress_file_path,
&format!("operation={operation_name} status=start"),
);
OperationTimer {
started_at: Instant::now(),
progress_file_path: progress_file_path.map(Path::to_path_buf),
operation_name: operation_name.to_owned(),
}
}
struct OperationTimer {
started_at: Instant,
progress_file_path: Option<PathBuf>,
operation_name: String,
}
impl OperationTimer {
fn complete(self, outcome: &str) {
emit(
self.progress_file_path.as_deref(),
&format!(
"operation={} status={outcome} elapsed_seconds={:.3}",
self.operation_name,
self.started_at.elapsed().as_secs_f64()
),
);
}
}
fn emit(progress_file_path: Option<&Path>, line_body: &str) {
println!("[mlx-c-rust-native-build] {line_body}");
if let Some(progress_file_path) = progress_file_path {
if let Ok(mut progress_file) = OpenOptions::new()
.create(true)
.append(true)
.open(progress_file_path)
{
let _ = writeln!(progress_file, "[mlx-c-rust-native-build] {line_body}");
}
}
}
fn download_verified_tarball(
marker_directory: &Path,
working_directory: &Path,
tarball_name: &str,
tarball_url: &str,
expected_sha256: &str,
) -> Option<PathBuf> {
let tarball_path = working_directory.join(tarball_name);
let marker = marker_directory.join(format!("{tarball_name}.verified"));
if !marker.exists() {
if !tarball_path.exists()
&& run_step(
working_directory,
Command::new("curl")
.arg("-sSL")
.arg("-o")
.arg(&tarball_path)
.arg(tarball_url),
)
.is_none()
{
return None;
}
let tarball_bytes = std::fs::read(&tarball_path).ok()?;
let mut hasher = Sha256::new();
hasher.update(&tarball_bytes);
let actual_sha256 = digest_lowercase_hex(hasher.finalize().as_ref());
if actual_sha256 != expected_sha256 {
println!(
"cargo:warning={} has SHA-256 {actual_sha256}, expected {expected_sha256}; \
refusing to build from an unverified source",
tarball_path.display()
);
return None;
}
let _ = std::fs::write(&marker, b"verified");
}
Some(tarball_path)
}
fn run_step(working_directory: &Path, command: &mut Command) -> Option<()> {
let command_status = command
.current_dir(working_directory)
.status()
.map_err(|launch_error| {
println!(
"cargo:warning=failed to run {}: {launch_error}",
command.get_program().to_string_lossy()
);
})
.ok()?;
if command_status.success() {
Some(())
} else {
println!(
"cargo:warning=native build step failed: {}",
command.get_program().to_string_lossy()
);
None
}
}