car-inference 0.18.0

Local model inference for CAR — Candle backend with Qwen3 models
Documentation
// Compiles the FoundationModels Swift shim into a static library and
// emits the link directives that pull it (plus the framework itself
// and the Swift runtime) into car-inference.
//
// Runs on Apple Silicon macOS AND on the two iOS targets that ship in
// the XCFramework — `aarch64-apple-ios` (device) and
// `aarch64-apple-ios-sim` (simulator on Apple Silicon Macs). On every
// other host we emit nothing — `backend::foundation_models` is
// cfg-gated to the same set of targets so its `extern "C"`
// declarations never get linked.
//
// The framework is weak-linked: the produced binary still loads on
// pre-26 macOS / pre-26 iOS, it just can't invoke FM symbols (the
// runtime check in `car_fm_is_available` returns 0 there).

use std::env;
use std::path::PathBuf;
use std::process::Command;

/// Soft-warn by default; hard-error when CAR_REQUIRE_APPLE_SHIMS=1
/// is set. See car-vision/build.rs for rationale.
fn shim_skipped(reason: String) {
    if std::env::var("CAR_REQUIRE_APPLE_SHIMS").as_deref() == Ok("1") {
        println!("cargo::error={reason}");
    } else {
        println!("cargo:warning={reason}");
        println!(
            "cargo:warning=FoundationModels shim skipped — `apple/foundation:default` model \
             route will be unavailable; the adaptive router will fall through to other \
             backends. Set CAR_REQUIRE_APPLE_SHIMS=1 to fail the build instead."
        );
    }
}

/// Per-target shim build configuration. Encapsulates the moving
/// pieces that differ between macOS, iOS device, and iOS simulator
/// so the rest of `main` stays uniform.
struct AppleTarget {
    /// Argument to `xcrun --sdk` (e.g. `macosx`, `iphoneos`).
    sdk: &'static str,
    /// Env var holding the deployment-target version (e.g.
    /// `MACOSX_DEPLOYMENT_TARGET`).
    deployment_env: &'static str,
    /// Default deployment-target version when the env var is unset.
    default_version: &'static str,
    /// Triple prefix consumed by `swiftc -target` — version + suffix
    /// are appended at use site.
    target_prefix: &'static str,
    /// Triple suffix appended after the version (empty for device
    /// builds, `-simulator` for the iOS-sim slice).
    target_suffix: &'static str,
    /// Platform directory under the Xcode toolchain Swift runtime
    /// root, e.g. `iphoneos` or `iphonesimulator`.
    swift_platform: &'static str,
    /// Linker deployment-minimum flag consumed by Apple's clang
    /// driver.
    min_version_flag: &'static str,
}

fn detect_apple_target(target: &str) -> Option<AppleTarget> {
    match target {
        "aarch64-apple-darwin" => Some(AppleTarget {
            sdk: "macosx",
            deployment_env: "MACOSX_DEPLOYMENT_TARGET",
            default_version: "15.0",
            target_prefix: "arm64-apple-macosx",
            target_suffix: "",
            swift_platform: "macosx",
            min_version_flag: "-mmacosx-version-min",
        }),
        "aarch64-apple-ios" => Some(AppleTarget {
            sdk: "iphoneos",
            deployment_env: "IPHONEOS_DEPLOYMENT_TARGET",
            default_version: "15.0",
            target_prefix: "arm64-apple-ios",
            target_suffix: "",
            swift_platform: "iphoneos",
            min_version_flag: "-miphoneos-version-min",
        }),
        // Apple Silicon iOS simulator slice. Intel-Mac iOS sim
        // (`x86_64-apple-ios`) isn't in the XCFramework matrix so
        // it's intentionally absent here.
        "aarch64-apple-ios-sim" => Some(AppleTarget {
            sdk: "iphonesimulator",
            deployment_env: "IPHONEOS_DEPLOYMENT_TARGET",
            default_version: "15.0",
            target_prefix: "arm64-apple-ios",
            target_suffix: "-simulator",
            swift_platform: "iphonesimulator",
            min_version_flag: "-mios-simulator-version-min",
        }),
        _ => None,
    }
}

fn main() {
    let target = env::var("TARGET").unwrap_or_default();
    let apple = detect_apple_target(&target);

    println!("cargo:rerun-if-changed=swift/CarFoundationModels.swift");
    println!("cargo:rerun-if-changed=build.rs");

    // Declare `car_fm_swift_built` to cargo's check-cfg machinery so
    // newer toolchains don't warn that it's unrecognized. Set later
    // only when swiftc succeeds; backend module gates its extern "C"
    // surface on it (with stubs otherwise).
    println!("cargo:rustc-check-cfg=cfg(car_fm_swift_built)");

    let Some(apple) = apple else {
        return;
    };

    // Xcode-version landmines we know about. Surface them as
    // cargo:warning so a developer on a known-bad combo sees the
    // pointer to the workaround during build instead of debugging
    // mysterious link / Metal-kernel failures.
    if apple.sdk == "macosx" {
        warn_known_xcode_landmines();
    }

    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let swift_src = manifest_dir.join("swift").join("CarFoundationModels.swift");
    if !swift_src.exists() {
        // Source missing — nothing to compile, leave the backend
        // module to fail at link time with a clear error if anyone
        // tries to call it.
        shim_skipped(format!(
            "FoundationModels Swift shim not found at {}",
            swift_src.display()
        ));
        return;
    }

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let obj_path = out_dir.join("CarFoundationModels.o");
    let lib_path = out_dir.join("libCarFoundationModels.a");

    // Read the deployment target from the standard toolchain env var
    // for this Apple target (`MACOSX_DEPLOYMENT_TARGET` /
    // `IPHONEOS_DEPLOYMENT_TARGET`), falling back to a sensible floor.
    // Hardcoding the version would silently disagree if the workspace
    // ever bumps the floor.
    println!("cargo:rerun-if-env-changed={}", apple.deployment_env);
    let deployment_target =
        env::var(apple.deployment_env).unwrap_or_else(|_| apple.default_version.to_string());
    let target_arg = format!(
        "{}{}{}",
        apple.target_prefix, deployment_target, apple.target_suffix
    );

    // Compile Swift -> object file.
    //
    // -parse-as-library: source has no top-level statements.
    // -O: release optimisation; matches Cargo's release profile.
    // -target: deployment floor for the active Apple target. Swift
    //    `@available` annotations gate FM symbols to {macOS,iOS} 26+
    //    at runtime regardless of how low the floor is.
    // -runtime-compatibility-version none: tell swiftc not to emit
    //    `__swift_FORCE_LOAD_$_swiftCompatibility{50,51,56,Concurrency}`
    //    references. Those reference back-deployment shim static libs
    //    (`libswiftCompatibility56.a`, etc.) that aren't in the
    //    iPhoneOS SDK's `usr/lib/swift` and aren't on the rustc link
    //    line — the linker fails with `Undefined symbols:
    //    __swift_FORCE_LOAD_$_swiftCompatibility56`. The shim's
    //    Concurrency usage works fine without the back-compat library
    //    on iOS 15+ / macOS 12+ (native Concurrency runtime), and the
    //    framework's `@available(iOS 26)` gate already prevents
    //    invocation on older OSes.
    let status = Command::new("xcrun")
        .arg("--sdk")
        .arg(apple.sdk)
        .arg("swiftc")
        .arg("-parse-as-library")
        .arg("-O")
        .arg("-target")
        .arg(&target_arg)
        .arg("-runtime-compatibility-version")
        .arg("none")
        .arg("-emit-object")
        .arg("-o")
        .arg(&obj_path)
        .arg(&swift_src)
        .status();

    let status = match status {
        Ok(s) => s,
        Err(e) => {
            shim_skipped(format!(
                "swiftc invocation failed ({e}); FoundationModels backend unavailable. \
                 Install full Xcode (not just Command Line Tools) to enable it."
            ));
            return;
        }
    };
    if !status.success() {
        shim_skipped(format!(
            "swiftc returned non-zero status ({:?}); FoundationModels backend unavailable",
            status.code()
        ));
        return;
    }

    // Pack the object into a static library so cargo's linker can pick
    // it up via -l.
    let ar_status = Command::new("ar")
        .arg("crus")
        .arg(&lib_path)
        .arg(&obj_path)
        .status();
    match ar_status {
        Ok(s) if s.success() => {}
        _ => {
            shim_skipped(format!(
                "ar failed to pack {} into {}; FoundationModels backend unavailable",
                obj_path.display(),
                lib_path.display()
            ));
            return;
        }
    }

    println!("cargo:rustc-link-search=native={}", out_dir.display());
    println!("cargo:rustc-link-lib=static=CarFoundationModels");

    // Weak-link the framework so the binary still loads on pre-26
    // OSes. The runtime availability check inside the Swift shim is
    // what actually gates calls.
    println!(
        "cargo:rustc-link-arg={}={}",
        apple.min_version_flag, deployment_target
    );
    println!("cargo:rustc-link-arg=-Wl,-weak_framework,FoundationModels");

    // Swift standard library + concurrency runtime. The shim uses
    // `Task` / `async`, so libswift_Concurrency must be linked too.
    // `-rpath` lets the binary find the matching dylibs in the OS's
    // built-in Swift runtime location (present on macOS 10.14+ and
    // iOS 12.2+).
    println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
    if let Ok(swift_lib) = swift_runtime_path(apple.sdk) {
        println!("cargo:rustc-link-search=native={}", swift_lib);
    }
    if let Ok(swift_lib) = swift_toolchain_runtime_path(apple.swift_platform) {
        println!("cargo:rustc-link-search=native={}", swift_lib);
    }
    println!("cargo:rustc-link-lib=dylib=swiftCore");
    println!("cargo:rustc-link-lib=dylib=swift_Concurrency");
    println!("cargo:rustc-link-lib=dylib=swiftFoundation");

    // Tell our backend module that the library is available so it can
    // unlock the extern "C" declarations.
    println!("cargo:rustc-cfg=car_fm_swift_built");
}

fn swift_runtime_path(sdk: &str) -> Result<String, std::io::Error> {
    let out = Command::new("xcrun")
        .arg("--sdk")
        .arg(sdk)
        .arg("--show-sdk-path")
        .output()?;
    if !out.status.success() {
        return Err(std::io::Error::other("xcrun --show-sdk-path failed"));
    }
    let sdk_path = String::from_utf8_lossy(&out.stdout).trim().to_string();
    // Toolchain Swift libs live next to the SDK in the developer dir.
    Ok(format!("{}/usr/lib/swift", sdk_path))
}

fn swift_toolchain_runtime_path(platform: &str) -> Result<String, std::io::Error> {
    let out = Command::new("xcrun").arg("--find").arg("swiftc").output()?;
    if !out.status.success() {
        return Err(std::io::Error::other("xcrun --find swiftc failed"));
    }
    let swiftc = PathBuf::from(String::from_utf8_lossy(&out.stdout).trim().to_string());
    let Some(toolchain_root) = swiftc
        .parent()
        .and_then(|bin| bin.parent())
        .map(PathBuf::from)
    else {
        return Err(std::io::Error::other(
            "could not resolve Swift toolchain root",
        ));
    };
    Ok(toolchain_root
        .join("lib")
        .join("swift")
        .join(platform)
        .display()
        .to_string())
}

/// Warn about known Xcode/Apple Clang version combos that break
/// car-inference's transitive dep graph. Cheap probe via
/// `xcodebuild -version`. Failures (xcodebuild missing, parse
/// error, etc.) are silent — this is best-effort hint surfacing,
/// not a hard gate.
fn warn_known_xcode_landmines() {
    let Ok(out) = Command::new("xcodebuild").arg("-version").output() else {
        return;
    };
    if !out.status.success() {
        return;
    }
    let stdout = String::from_utf8_lossy(&out.stdout);
    // First line: `Xcode 16.4` or `Xcode 17.0` etc.
    let Some(line) = stdout.lines().next() else {
        return;
    };
    let Some(version_str) = line.split_whitespace().nth(1) else {
        return;
    };
    // Parse "16.4" → (16, 4); "17" → (17, 0).
    let (major, minor) = {
        let mut parts = version_str.split('.');
        let major: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
        let minor: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
        (major, minor)
    };

    // Known landmines as of 2026-05. Update this list whenever a
    // new Xcode release breaks the build in a way that has a
    // documented workaround.
    if major == 16 && minor >= 3 {
        println!(
            "cargo:warning=Xcode {major}.{minor} bundles Apple Clang 17, which miscompiles \
             MLX's bf16 truncation. Build may fail or produce incorrect MLX inference. \
             Pin to Xcode 16.2 (Clang 16) via `sudo xcode-select -s /Applications/Xcode_16.2.app`. \
             See .github/workflows/ci.yml `check-macos-server` for the CI pin."
        );
    }
    if major == 26 && minor >= 4 {
        println!(
            "cargo:warning=Xcode {major}.{minor} ships a Metal Toolchain version that conflicts \
             with mlx-sys's compiled Metal kernels. See \
             docs/mlx-sys-xcode-26-4-metal-incompat.md for the documented workaround."
        );
    }
}