use std::path::{Path, PathBuf};
use objc2::rc::Retained;
use objc2_core_ml::{MLComputeUnits, MLModel, MLModelConfiguration};
use objc2_foundation::{NSString, NSURL};
use super::ns_error_message;
use crate::runtime::error::RuntimeError;
#[allow(deprecated)]
pub fn compile_and_load(
package: &Path,
cpu_and_ne: bool,
) -> Result<Retained<MLModel>, RuntimeError> {
let config: Retained<MLModelConfiguration> = unsafe { MLModelConfiguration::new() };
let units = if cpu_and_ne {
MLComputeUnits::CPUAndNeuralEngine
} else {
MLComputeUnits::CPUOnly
};
unsafe { config.setComputeUnits(units) };
let cached = cached_model_path(package);
if cached.is_dir() {
match current_source_key(package) {
Ok(key) if meta_matches(&cached_meta_path(package), &key) => {
match load_model_from_dir(&cached, &config) {
Ok(model) => {
tracing::info!(
cache = %cached.display(),
"loaded compiled ANE model from cache"
);
return Ok(model);
}
Err(e) => {
tracing::warn!(
cache = %cached.display(),
error = %e,
"cached ANE model failed to load; recompiling"
);
}
}
}
Ok(_) => {
tracing::info!(
cache = %cached.display(),
"ANE compiled-model cache stale (source or OS changed); recompiling"
);
}
Err(e) => {
tracing::warn!(error = %e, "could not compute ANE cache key; recompiling");
}
}
}
tracing::info!(
package = %package.display(),
cache = %cached.display(),
"compiling ANE model (cold-start ~20s), caching for fast restarts"
);
let compiled_url = compile_package(package)?;
if let Some(temp_dir) = url_to_path(&compiled_url) {
match populate_cache(package, &temp_dir) {
Ok(()) => {
match load_model_from_dir(&cached, &config) {
Ok(model) => return Ok(model),
Err(e) => tracing::warn!(
cache = %cached.display(),
error = %e,
"freshly cached ANE model failed to load; loading from temp"
),
}
}
Err(e) => tracing::warn!(
cache = %cached.display(),
error = %e,
"failed to populate ANE compiled-model cache; loading from temp"
),
}
} else {
tracing::warn!("compiled ANE model URL is not a local path; cache skipped");
}
load_model_from_url(package, &compiled_url, &config)
}
#[allow(deprecated)]
fn compile_package(package: &Path) -> Result<Retained<NSURL>, RuntimeError> {
let path_str = package.to_str().ok_or_else(|| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: "package path is not valid UTF-8".to_string(),
})?;
let ns_path = NSString::from_str(path_str);
let pkg_url: Retained<NSURL> = NSURL::fileURLWithPath(&ns_path);
unsafe { MLModel::compileModelAtURL_error(&pkg_url) }.map_err(|err| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: format!("compileModelAtURL failed: {}", ns_error_message(&err)),
})
}
fn load_model_from_dir(
compiled_dir: &Path,
config: &MLModelConfiguration,
) -> Result<Retained<MLModel>, RuntimeError> {
let path_str = compiled_dir
.to_str()
.ok_or_else(|| RuntimeError::LoadFailed {
path: compiled_dir.to_path_buf(),
message: "compiled model path is not valid UTF-8".to_string(),
})?;
let ns_path = NSString::from_str(path_str);
let url: Retained<NSURL> = NSURL::fileURLWithPath(&ns_path);
load_model_from_url(compiled_dir, &url, config)
}
fn load_model_from_url(
package: &Path,
compiled_url: &NSURL,
config: &MLModelConfiguration,
) -> Result<Retained<MLModel>, RuntimeError> {
unsafe { MLModel::modelWithContentsOfURL_configuration_error(compiled_url, config) }.map_err(
|err| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: format!("modelWithContentsOfURL failed: {}", ns_error_message(&err)),
},
)
}
const COMPILED_CACHE_DIR: &str = "compiled_cache";
pub(super) fn compiled_cache_dir(package: &Path) -> PathBuf {
package
.parent()
.unwrap_or_else(|| Path::new("."))
.join(COMPILED_CACHE_DIR)
}
pub(super) fn cached_model_path(package: &Path) -> PathBuf {
let stem = package
.file_stem()
.map(|s| s.to_os_string())
.unwrap_or_else(|| std::ffi::OsString::from("model"));
let mut name = stem;
name.push(".mlmodelc");
compiled_cache_dir(package).join(name)
}
pub(super) fn cached_meta_path(package: &Path) -> PathBuf {
let mut p = cached_model_path(package).into_os_string();
p.push(".meta");
PathBuf::from(p)
}
fn macos_product_version() -> Result<String, RuntimeError> {
let out = std::process::Command::new("sw_vers")
.arg("-productVersion")
.output()
.map_err(|e| RuntimeError::LoadFailed {
path: PathBuf::from("sw_vers"),
message: format!("failed to run sw_vers: {e}"),
})?;
if !out.status.success() {
return Err(RuntimeError::LoadFailed {
path: PathBuf::from("sw_vers"),
message: format!("sw_vers exited with {}", out.status),
});
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn dir_size_and_newest_mtime(root: &Path) -> std::io::Result<(u64, u128)> {
let mut total: u64 = 0;
let mut newest: u128 = 0;
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let ft = entry.file_type()?;
if ft.is_dir() {
stack.push(entry.path());
} else if ft.is_file() {
let meta = entry.metadata()?;
total = total.saturating_add(meta.len());
if let Ok(mtime) = meta.modified()
&& let Ok(dur) = mtime.duration_since(std::time::UNIX_EPOCH)
{
newest = newest.max(dur.as_nanos());
}
}
}
}
Ok((total, newest))
}
pub(super) fn build_source_key(package: &Path, os_version: &str) -> Result<String, RuntimeError> {
let (size, mtime) =
dir_size_and_newest_mtime(package).map_err(|e| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: format!("failed to stat package for cache key: {e}"),
})?;
Ok(format!("size={size} mtime_ns={mtime} os={os_version}"))
}
pub(super) fn current_source_key(package: &Path) -> Result<String, RuntimeError> {
build_source_key(package, &macos_product_version()?)
}
pub(super) fn meta_matches(meta_path: &Path, key: &str) -> bool {
match std::fs::read_to_string(meta_path) {
Ok(content) => content.trim() == key.trim(),
Err(_) => false,
}
}
pub(super) fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ft = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ft.is_dir() {
copy_dir_recursive(&from, &to)?;
} else {
std::fs::copy(&from, &to)?;
}
}
Ok(())
}
pub(super) fn populate_cache(package: &Path, temp_dir: &Path) -> std::io::Result<()> {
let cache_dir = compiled_cache_dir(package);
std::fs::create_dir_all(&cache_dir)?;
sweep_stale_temp_dirs(&cache_dir);
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id();
let staging = cache_dir.join(format!(".staging.{pid}.{stamp}"));
let cleanup = || {
let _ = std::fs::remove_dir_all(&staging);
};
if let Err(e) = copy_dir_recursive(temp_dir, &staging) {
cleanup();
return Err(e);
}
let final_dir = cached_model_path(package);
let mut trash: Option<PathBuf> = None;
if final_dir.exists() {
let aside = cache_dir.join(format!(".trash.{pid}.{stamp}"));
if let Err(e) = std::fs::rename(&final_dir, &aside) {
cleanup();
return Err(e);
}
trash = Some(aside);
}
if let Err(e) = std::fs::rename(&staging, &final_dir) {
cleanup();
if let Some(aside) = trash {
let _ = std::fs::remove_dir_all(&aside);
}
return Err(e);
}
if let Some(aside) = trash {
let _ = std::fs::remove_dir_all(&aside);
}
let key = current_source_key(package).map_err(std::io::Error::other)?;
std::fs::write(cached_meta_path(package), key)?;
Ok(())
}
fn sweep_stale_temp_dirs(cache_dir: &Path) {
let Ok(entries) = std::fs::read_dir(cache_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with(".staging.") || name.starts_with(".trash.") {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
fn url_to_path(url: &NSURL) -> Option<PathBuf> {
let ns = url.path()?;
Some(PathBuf::from(ns.to_string()))
}