use std::env;
use std::path::PathBuf;
use std::process::Command;
fn main() -> Result<(), Box<dyn std::error::Error>> {
if env::var("DOCS_RS").is_ok() {
println!("cargo:warning=Skipping Swift compilation on docs.rs");
return Ok(());
}
if !is_apple_platform() {
println!(
"cargo:warning=fm-rs only supports Apple Intelligence platforms (macOS, iOS/iPadOS). Skipping build."
);
return Ok(());
}
let target = env::var("TARGET")?;
if !is_supported_target(&target) {
return Err(format!(
"fm-rs requires Apple Intelligence hardware; unsupported target '{target}'. Use Apple Silicon (aarch64) on macOS/iOS/iPadOS."
)
.into());
}
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let swift_src = PathBuf::from("src/swift/ffi.swift");
let module_name = "fm_ffi";
let lib_name = format!("lib{module_name}.a");
println!("cargo:rerun-if-changed={}", swift_src.display());
let swiftc = env::var("SWIFTC").unwrap_or_else(|_| "swiftc".to_string());
let swift_output = out_dir.join(&lib_name);
let swift_target = get_swift_target(&target)?;
let sdk_path = get_sdk_path(&swift_target);
let swift_output_str = swift_output.to_str().ok_or("Invalid output path")?;
let swift_src_str = swift_src.to_str().ok_or("Invalid Swift source path")?;
let mut swift_args: Vec<String> = vec![
"-emit-library".to_string(),
"-static".to_string(),
"-module-name".to_string(),
module_name.to_string(),
"-o".to_string(),
swift_output_str.to_string(),
"-target".to_string(),
swift_target.clone(),
];
if let Some(ref sdk) = sdk_path {
swift_args.push("-sdk".to_string());
swift_args.push(sdk.clone());
}
swift_args.push(swift_src_str.to_string());
println!("Compiling Swift code with: swiftc {}", swift_args.join(" "));
let status = Command::new(&swiftc).args(&swift_args).status()?;
if !status.success() {
return Err(format!("Swift compilation failed with status: {status}").into());
}
println!("cargo:rustc-link-lib=static={module_name}");
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=FoundationModels");
if let Some(swift_lib_path) = get_swift_lib_path() {
println!("cargo:rustc-link-search=native={swift_lib_path}");
println!("cargo:rustc-link-arg=-Wl,-rpath,{swift_lib_path}");
}
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
Ok(())
}
fn is_apple_platform() -> bool {
env::var("CARGO_CFG_TARGET_OS")
.map(|os| os == "macos" || os == "ios")
.unwrap_or(false)
}
fn is_supported_target(target: &str) -> bool {
matches!(
target,
"aarch64-apple-darwin" | "aarch64-apple-ios" | "aarch64-apple-ios-sim"
)
}
fn get_swift_target(target: &str) -> Result<String, Box<dyn std::error::Error>> {
let swift_target = match target {
"aarch64-apple-darwin" => "arm64-apple-macosx26.0",
"aarch64-apple-ios" => "arm64-apple-ios26.0",
"aarch64-apple-ios-sim" => "arm64-apple-ios26.0-simulator",
_ => {
return Err(
format!("Unsupported Apple target '{target}' for FoundationModels.").into(),
);
}
};
Ok(swift_target.to_string())
}
fn get_swift_lib_path() -> Option<String> {
let output = Command::new("xcrun")
.args(["--toolchain", "default", "--find", "swift"])
.output()
.ok()?;
let swift_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if swift_path.is_empty() {
return None;
}
let toolchain_path = std::path::Path::new(&swift_path)
.parent()? .parent()?;
let lib_path = toolchain_path.join("lib/swift/macosx");
if lib_path.exists() {
return Some(lib_path.to_string_lossy().into_owned());
}
None
}
fn get_sdk_path(swift_target: &str) -> Option<String> {
let sdk_name = if swift_target.contains("macosx") {
"macosx"
} else if swift_target.contains("ios") && swift_target.contains("simulator") {
"iphonesimulator"
} else if swift_target.contains("ios") {
"iphoneos"
} else if swift_target.contains("xros") {
"xros"
} else if swift_target.contains("tvos") {
"appletvos"
} else if swift_target.contains("watchos") {
"watchos"
} else {
return None;
};
let output = Command::new("xcrun")
.args(["--show-sdk-path", "--sdk", sdk_name])
.output()
.ok()?;
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path.is_empty() || path.contains("cannot be found") {
None
} else {
Some(path)
}
}