//! Builds the macOS helper processes the app drives (`resources/native/*.swift`) //! and the vendored MediaRemoteAdapter framework into `resources/bin`. //! //! Whisper ships for Apple silicon only, so the helpers are built for arm64. //! A missing toolchain only produces a cargo warning: the Rust code degrades //! gracefully when a helper is absent. Set `WHISPER_SKIP_NATIVE_BUILD=1` to //! skip this step entirely. use std::path::{Path, PathBuf}; use std::process::Command; use std::time::SystemTime; const CPU_TYPE_ARM64: i32 = 0x0100_000c; const MH_MAGIC_64: u32 = 0xfeed_facf; const SWIFT_TARGET: &str = "arm64-apple-macosx11.0"; fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=resources/native"); println!("cargo:rerun-if-changed=resources/mediaremote-adapter"); println!("cargo:rerun-if-changed=scripts/build-mediaremote-adapter.sh"); println!("cargo:rerun-if-env-changed=WHISPER_SKIP_NATIVE_BUILD"); if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") { return; } if std::env::var_os("WHISPER_SKIP_NATIVE_BUILD").is_some_and(|v| v != "0") { return; } if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() != Ok("aarch64") { warn("native helpers are built for arm64 only; skipping"); return; } let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into())); let native_dir = root.join("resources").join("native"); let out_dir = root.join("resources").join("bin"); if let Err(error) = std::fs::create_dir_all(&out_dir) { warn(&format!("cannot create {}: {error}", out_dir.display())); return; } let mut sources: Vec = std::fs::read_dir(&native_dir) .map(|entries| { entries .filter_map(Result::ok) .map(|entry| entry.path()) .filter(|path| { path.extension().is_some_and(|ext| ext == "swift") && path .file_name() .and_then(|name| name.to_str()) .is_some_and(|name| name.starts_with("macos-")) }) .collect() }) .unwrap_or_default(); sources.sort(); for source in &sources { build_swift_helper(source, &out_dir); } build_media_remote_adapter(&root); } fn warn(message: &str) { println!("cargo:warning=[native] {message}"); } fn mtime(path: &Path) -> Option { std::fs::metadata(path).and_then(|m| m.modified()).ok() } fn is_arm64_binary(path: &Path) -> bool { let Ok(bytes) = std::fs::read(path) else { return false; }; if bytes.len() < 8 { return false; } let magic = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); let cpu = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); magic == MH_MAGIC_64 && cpu == CPU_TYPE_ARM64 } /// Frameworks each helper links explicitly. fn frameworks_for(name: &str) -> &'static [&'static str] { match name { "macos-mic-listener" => &["Foundation", "IOKit"], _ => &[], } } fn build_swift_helper(source: &Path, out_dir: &Path) { let Some(name) = source.file_stem().and_then(|s| s.to_str()) else { return; }; let output = out_dir.join(name); let up_to_date = match (mtime(&output), mtime(source)) { (Some(built), Some(src)) => built >= src && is_arm64_binary(&output), _ => false, }; if up_to_date { return; } let module_cache = out_dir.join(".swift-module-cache"); let _ = std::fs::create_dir_all(&module_cache); let mut args: Vec = vec![ source.display().to_string(), "-O".into(), "-target".into(), SWIFT_TARGET.into(), "-module-cache-path".into(), module_cache.display().to_string(), "-o".into(), output.display().to_string(), ]; for framework in frameworks_for(name) { args.push("-framework".into()); args.push((*framework).into()); } let compile = |program: &str, prefix: &[&str]| { Command::new(program) .args(prefix) .args(&args) .env("SWIFT_MODULE_CACHE_PATH", &module_cache) .output() }; let mut result = compile("xcrun", &["swiftc"]); if !matches!(&result, Ok(out) if out.status.success()) { result = compile("swiftc", &[]); } match result { Ok(out) if out.status.success() => { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions(&output, std::fs::Permissions::from_mode(0o755)); } if !is_arm64_binary(&output) { warn(&format!( "{name}: compiled binary is not arm64 - helper may not run" )); } } Ok(out) => { let stderr = String::from_utf8_lossy(&out.stderr); let first = stderr.lines().find(|l| l.contains("error")).unwrap_or(""); warn(&format!("failed to compile {name} with swiftc: {first}")); } Err(error) => warn(&format!("swiftc unavailable, skipping {name}: {error}")), } } fn build_media_remote_adapter(root: &Path) { let script = root.join("scripts").join("build-mediaremote-adapter.sh"); if !script.is_file() { return; } match Command::new("/bin/bash") .arg(&script) .current_dir(root) .output() { Ok(out) if out.status.success() => {} Ok(out) => { let stderr = String::from_utf8_lossy(&out.stderr); let last = stderr.lines().last().unwrap_or(""); warn(&format!( "MediaRemoteAdapter.framework build failed: {last}" )); } Err(error) => warn(&format!("cannot run {}: {error}", script.display())), } }