|
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
|
} |