| 1 | //! Application filesystem setup: redirects model/temp paths into the shared |
| 2 | //! Apple App Group container so that all apps in `group.com.ondeinference.apps` |
| 3 | //! share a single HuggingFace model cache. |
| 4 | |
| 5 | use log::debug; |
| 6 | #[cfg(any(target_os = "ios", target_os = "macos"))] |
| 7 | use objc2_foundation::ns_string; |
| 8 | #[cfg(any(target_os = "ios", target_os = "macos"))] |
| 9 | use objc2_foundation::NSFileManager; |
| 10 | use std::path::{Path, PathBuf}; |
| 11 | use tauri::{App, Manager}; |
| 12 | |
| 13 | /// Application filesystem setup. |
| 14 | /// |
| 15 | /// On sandboxed platforms (iOS, macOS App Store / TestFlight) several default |
| 16 | /// paths used by Rust crates (`~/.cache/…`, `/tmp`, etc.) are **not** writable. |
| 17 | /// We must redirect all file I/O into directories inside the app container |
| 18 | /// *before* any downstream code (hf-hub, mistral.rs, candle, …) runs. |
| 19 | /// |
| 20 | /// 1. `HF_HOME` → controls where `hf-hub` stores downloaded models |
| 21 | /// 2. `HF_HUB_CACHE` → explicit cache dir read by some mistral.rs code paths |
| 22 | /// 3. `TMPDIR` → controls where `std::env::temp_dir()` points; |
| 23 | /// mistral.rs writes Metal `.metallib` files there |
| 24 | /// |
| 25 | /// The Apple App Group container (`group.com.ondeinference.apps`) is used as the |
| 26 | /// base directory so that models are shared across all apps in the group, |
| 27 | /// avoiding duplicate downloads. |
| 28 | /// |
| 29 | /// On non-sandboxed macOS all of this is harmless; the directories are just |
| 30 | /// as writable as their default counterparts, and if the group container is |
| 31 | /// unavailable we fall back to `app_data_dir`. |
| 32 | pub fn setup_application_filesystem(app: &App) -> Result<(), Box<dyn std::error::Error>> { |
| 33 | let app_data_dir = app.path().app_data_dir()?; |
| 34 | |
| 35 | #[cfg(any(target_os = "ios", target_os = "macos"))] |
| 36 | let container_dir: PathBuf = { |
| 37 | let fs = NSFileManager::defaultManager(); |
| 38 | let group_identifier = ns_string!("group.com.ondeinference.apps"); |
| 39 | let group_url = fs.containerURLForSecurityApplicationGroupIdentifier(group_identifier); |
| 40 | |
| 41 | match group_url { |
| 42 | Some(url) => { |
| 43 | let path_str = url.path().map(|p| p.to_string()).unwrap_or_default(); |
| 44 | |
| 45 | if path_str.is_empty() { |
| 46 | log::warn!("App Group container URL had no path, falling back to app_data_dir"); |
| 47 | app_data_dir.clone() |
| 48 | } else { |
| 49 | let p = PathBuf::from(&path_str); |
| 50 | debug!("App Group container resolved: {}", p.display()); |
| 51 | p |
| 52 | } |
| 53 | } |
| 54 | None => { |
| 55 | log::warn!( |
| 56 | "App Group container not available, falling back to app_data_dir: {}", |
| 57 | app_data_dir.display() |
| 58 | ); |
| 59 | app_data_dir.clone() |
| 60 | } |
| 61 | } |
| 62 | }; |
| 63 | |
| 64 | #[cfg(not(any(target_os = "ios", target_os = "macos")))] |
| 65 | let container_dir: PathBuf = app_data_dir; |
| 66 | |
| 67 | // ── Redirect HF_HOME, HF_HUB_CACHE, and TMPDIR ─────────────────────── |
| 68 | setup_hf_home(&container_dir)?; |
| 69 | setup_tmpdir(&container_dir)?; |
| 70 | |
| 71 | Ok(()) |
| 72 | } |
| 73 | |
| 74 | /// Point `HF_HOME` and `HF_HUB_CACHE` at `<container_dir>/models/` and |
| 75 | /// ensure the directory (and its `hub/` child) exist. |
| 76 | /// |
| 77 | /// This must run **before** any `hf-hub` API call so the crate picks up the |
| 78 | /// environment variables instead of falling back to `~/.cache/huggingface/`. |
| 79 | /// |
| 80 | /// `HF_HUB_CACHE` is read by `hf_hub_cache_dir()` inside mistral.rs's |
| 81 | /// `get_paths_gguf!` macro. Some code paths check this env var directly |
| 82 | /// instead of deriving it from `HF_HOME`, so we set both to be safe. |
| 83 | /// |
| 84 | /// Using the App Group container means the model cache is shared across all |
| 85 | /// apps in the `group.com.ondeinference.apps` group, avoiding duplicate downloads. |
| 86 | fn setup_hf_home(container_dir: &Path) -> Result<(), Box<dyn std::error::Error>> { |
| 87 | let models_home = container_dir.join("models"); |
| 88 | let model_hub = models_home.join("hub"); |
| 89 | |
| 90 | // Create both directories (idempotent). |
| 91 | std::fs::create_dir_all(&model_hub).map_err(|e| { |
| 92 | log::error!( |
| 93 | "Failed to create model hub cache directory at {}: {}", |
| 94 | model_hub.display(), |
| 95 | e |
| 96 | ); |
| 97 | e |
| 98 | })?; |
| 99 | |
| 100 | // Only set if the user hasn't explicitly overridden it already. |
| 101 | if std::env::var("HF_HOME").is_err() { |
| 102 | std::env::set_var("HF_HOME", &models_home); |
| 103 | log::info!("HF_HOME set to: {}", models_home.display()); |
| 104 | } else { |
| 105 | log::debug!( |
| 106 | "HF_HOME already set to: {}", |
| 107 | std::env::var("HF_HOME").unwrap_or_default() |
| 108 | ); |
| 109 | } |
| 110 | |
| 111 | if std::env::var("HF_HUB_CACHE").is_err() { |
| 112 | std::env::set_var("HF_HUB_CACHE", &model_hub); |
| 113 | log::info!("HF_HUB_CACHE set to: {}", model_hub.display()); |
| 114 | } |
| 115 | |
| 116 | Ok(()) |
| 117 | } |
| 118 | |
| 119 | /// Ensure `TMPDIR` points to a writable directory inside the app container. |
| 120 | /// |
| 121 | /// On iOS, `std::env::temp_dir()` falls back to `/tmp` when the `TMPDIR` |
| 122 | /// environment variable is not set. `/tmp` is **outside** the app sandbox |
| 123 | /// and writes there fail with "Operation not permitted". |
| 124 | /// |
| 125 | /// mistral.rs writes precompiled Metal shader libraries |
| 126 | /// (`mistralrs_quant.metallib`, `mistralrs_paged_attention.metallib`) to |
| 127 | /// `std::env::temp_dir()`, so this must be a writable location. |
| 128 | /// |
| 129 | /// On non-sandboxed macOS this is harmless; the system temp directory is |
| 130 | /// always writable, and if `TMPDIR` is already set we leave it alone. |
| 131 | fn setup_tmpdir(container_dir: &Path) -> Result<(), Box<dyn std::error::Error>> { |
| 132 | // Check whether the current temp_dir is actually writable. |
| 133 | let current_tmp = std::env::temp_dir(); |
| 134 | let probe_file = current_tmp.join(".siti_tmp_probe"); |
| 135 | let tmp_is_writable = std::fs::write(&probe_file, b"probe") |
| 136 | .and_then(|_| std::fs::remove_file(&probe_file)) |
| 137 | .is_ok(); |
| 138 | |
| 139 | if tmp_is_writable { |
| 140 | log::debug!("TMPDIR is writable at: {}", current_tmp.display()); |
| 141 | return Ok(()); |
| 142 | } |
| 143 | |
| 144 | // The current temp directory is not writable (typical on iOS / sandboxed macOS). |
| 145 | // Redirect to a `tmp/` folder inside the App Group container. |
| 146 | let app_tmp = container_dir.join("tmp"); |
| 147 | std::fs::create_dir_all(&app_tmp).map_err(|e| { |
| 148 | log::error!( |
| 149 | "Failed to create app-local tmp directory at {}: {}", |
| 150 | app_tmp.display(), |
| 151 | e |
| 152 | ); |
| 153 | e |
| 154 | })?; |
| 155 | |
| 156 | std::env::set_var("TMPDIR", &app_tmp); |
| 157 | log::info!( |
| 158 | "TMPDIR redirected from {} to {} (original was not writable)", |
| 159 | current_tmp.display(), |
| 160 | app_tmp.display() |
| 161 | ); |
| 162 | |
| 163 | Ok(()) |
| 164 | } |