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 }