use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
println!("cargo:rerun-if-env-changed=DEVELOPER_DIR");
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rerun-if-env-changed=TESSL_SKIP_AOT");
println!("cargo:rerun-if-env-changed=METAL_RUNTIME_SKIP_AOT");
println!("cargo:rerun-if-env-changed=TESSL_PREBUILT_METALLIB");
println!("cargo:rerun-if-env-changed=TESSL_GEMM_TUNE");
println!("cargo:rerun-if-env-changed=METAL_NATIVE_GEMM_TUNE");
println!("cargo:rustc-link-lib=framework=CoreGraphics");
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=Foundation");
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let kernels_dir = manifest_dir.join("kernels");
println!("cargo:kernels={}", kernels_dir.display());
if env::var_os("DOCS_RS").is_some() {
println!("cargo:warning=DOCS_RS set; skipping metallib AOT (docs only, no GPU)");
println!("cargo:metallib=");
println!("cargo:rustc-env=TESSL_METALLIB=");
return;
}
if env::var_os("TESSL_SKIP_AOT").is_some() || env::var_os("METAL_RUNTIME_SKIP_AOT").is_some() {
println!("cargo:warning=TESSL_SKIP_AOT set; skipping metallib AOT");
let configured = env::var_os("TESSL_PREBUILT_METALLIB").unwrap_or_else(|| {
panic!(
"TESSL_SKIP_AOT is set but TESSL_PREBUILT_METALLIB is not. \
Name an existing absolute metallib path explicitly, or unset \
TESSL_SKIP_AOT and build the shaders"
)
});
let configured = PathBuf::from(configured);
if !configured.is_absolute() {
panic!(
"TESSL_PREBUILT_METALLIB must be an absolute path, got {}",
configured.display()
);
}
let prebuilt = configured.canonicalize().unwrap_or_else(|e| {
panic!(
"TESSL_PREBUILT_METALLIB={} is not accessible: {e}",
configured.display()
)
});
if !prebuilt.is_file() {
panic!(
"TESSL_PREBUILT_METALLIB={} is not a file",
prebuilt.display()
);
}
println!("cargo:rerun-if-changed={}", prebuilt.display());
println!("cargo:metallib={}", prebuilt.display());
println!("cargo:rustc-env=TESSL_METALLIB={}", prebuilt.display());
return;
}
ensure_developer_dir();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
track_kernel_sources(&kernels_dir);
let sdk = xcrun_stdout(&["--sdk", "macosx", "--show-sdk-path"]);
let metal = resolve_metal();
let metallib = resolve_metallib();
let mut air_files: Vec<PathBuf> = Vec::new();
let want_tune =
env::var_os("TESSL_GEMM_TUNE").is_some() || env::var_os("METAL_NATIVE_GEMM_TUNE").is_some();
let mut tensorops_sources: Vec<PathBuf> = vec![kernels_dir.join("matmul_tensorops.metal")];
if want_tune {
tensorops_sources.push(kernels_dir.join("tune/matmul_tensorops_tune.metal"));
}
for src in &tensorops_sources {
let name = src
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("<unnamed>");
if !src.exists() {
panic!(
"required TensorOps source missing: {}; Metal 4 / macOS 26 toolchain required",
src.display()
);
}
let air = out_dir.join(format!(
"{}.air",
src.file_stem().unwrap().to_string_lossy()
));
let status = Command::new(&metal)
.args([
"-std=metal4.0",
"-O2",
"-isysroot",
&sdk,
"-mmacosx-version-min=26.0",
"-c",
])
.arg(src)
.arg("-o")
.arg(&air)
.status()
.unwrap_or_else(|e| panic!("failed to spawn metal for {name}: {e}"));
if !status.success() {
panic!(
"{name} failed to compile (need Metal 4 / macOS 26 SDK + MetalToolchain); \
refusing simdgroup-only metallib"
);
}
air_files.push(air);
}
println!("cargo:rustc-cfg=metal_runtime_tensorops");
let skip: &[&str] = &["matmul_tensorops.metal"];
let mut others: Vec<PathBuf> = fs::read_dir(&kernels_dir)
.unwrap_or_else(|e| panic!("read kernels/: {e}"))
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension().and_then(|s| s.to_str()) == Some("metal")
&& !skip
.iter()
.any(|s| p.file_name().and_then(|n| n.to_str()) == Some(*s))
})
.collect();
others.sort();
for src in &others {
let stem = src.file_stem().unwrap().to_string_lossy();
let air = out_dir.join(format!("{stem}.air"));
if let Err(metal4_diag) = try_metal_compile(&metal, &sdk, src, &air, "metal4.0") {
let name = src.file_name().and_then(|n| n.to_str()).unwrap_or(&stem);
println!(
"cargo:warning={name} failed under -std=metal4.0; falling back to -std=metal3.2 \
(shader dialect only; encode remains Metal 4). metal4.0 diagnostic follows."
);
for line in metal4_diag.lines().take(40) {
println!("cargo:warning= {line}");
}
if let Err(metal32_diag) = try_metal_compile(&metal, &sdk, src, &air, "metal3.2") {
panic!(
"{name} failed under both -std=metal4.0 and -std=metal3.2.\n\
--- metal4.0 ---\n{metal4_diag}\n--- metal3.2 ---\n{metal32_diag}"
);
}
}
air_files.push(air);
}
sweep_previous_metallibs(&out_dir);
let build_id = format!(
"{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before Unix epoch")
.as_nanos()
);
let metallib_out = out_dir.join(format!("default-{build_id}.metallib"));
fs::File::create_new(&metallib_out).expect("reserve unique metallib output");
let mut link = Command::new(&metallib);
for air in &air_files {
link.arg(air);
}
link.arg("-o").arg(&metallib_out);
run(&mut link, "metallib link");
println!("cargo:metallib={}", metallib_out.display());
println!("cargo:rustc-env=TESSL_METALLIB={}", metallib_out.display());
}
fn sweep_previous_metallibs(out_dir: &Path) {
let Ok(entries) = fs::read_dir(out_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("default-") && name.ends_with(".metallib") {
let _ = fs::remove_file(entry.path());
}
}
}
fn try_metal_compile(
metal: &Path,
sdk: &str,
src: &Path,
air: &Path,
metal_std: &str,
) -> Result<(), String> {
let std_flag = format!("-std={metal_std}");
let out = Command::new(metal)
.args([
std_flag.as_str(),
"-O2",
"-isysroot",
sdk,
"-mmacosx-version-min=26.0",
"-c",
])
.arg(src)
.arg("-o")
.arg(air)
.output()
.map_err(|e| format!("failed to spawn {}: {e}", metal.display()))?;
if out.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&out.stderr);
let stderr = stderr.trim();
Err(if stderr.is_empty() {
format!("metal exited with {}", out.status)
} else {
stderr.to_string()
})
}
fn ensure_developer_dir() {
if env::var_os("DEVELOPER_DIR").is_some() {
return;
}
let xcode = Path::new("/Applications/Xcode.app/Contents/Developer");
if xcode.is_dir() {
unsafe { env::set_var("DEVELOPER_DIR", xcode) };
}
}
fn resolve_metal() -> PathBuf {
if let Ok(p) = xcrun_try(&["-f", "metal"]) {
return PathBuf::from(p);
}
panic!(
"metal compiler not found. Install Xcode and run:\n \
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n \
xcodebuild -downloadComponent MetalToolchain"
);
}
fn resolve_metallib() -> PathBuf {
PathBuf::from(xcrun_stdout(&["-f", "metallib"]))
}
fn xcrun_stdout(args: &[&str]) -> String {
let out = Command::new("xcrun")
.args(args)
.output()
.unwrap_or_else(|e| panic!("xcrun {:?} failed to spawn: {e}", args));
if !out.status.success() {
panic!(
"xcrun {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn xcrun_try(args: &[&str]) -> Result<String, ()> {
let out = Command::new("xcrun").args(args).output().map_err(|_| ())?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(())
}
}
fn run(cmd: &mut Command, label: &str) {
let status = cmd
.status()
.unwrap_or_else(|e| panic!("{label}: failed to spawn: {e}"));
if !status.success() {
panic!("{label}: exited with {status}");
}
}
fn track_kernel_sources(dir: &Path) {
println!("cargo:rerun-if-changed={}", dir.display());
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(e) => panic!("read {} for change tracking: {e}", dir.display()),
};
for entry in entries.filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_dir() {
track_kernel_sources(&p);
} else if matches!(
p.extension().and_then(|s| s.to_str()),
Some("metal") | Some("h")
) {
println!("cargo:rerun-if-changed={}", p.display());
}
}
}