Skip to main content

model_hf/
cache_paths.rs

1use anyhow::{Result, bail};
2use std::collections::HashSet;
3use std::ffi::OsString;
4use std::fmt;
5use std::fs::OpenOptions;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, Ordering};
9
10const HUB_CACHE_ENV: &str = "HF_HUB_CACHE";
11const HUB_CACHE_ALIAS_ENV: &str = "HUGGINGFACE_HUB_CACHE";
12const HF_HOME_ENV: &str = "HF_HOME";
13const XET_CACHE_ENV: &str = "HF_XET_CACHE";
14const XDG_CACHE_HOME_ENV: &str = "XDG_CACHE_HOME";
15const MESH_LLM_DATA_DIR_ENV: &str = "MESH_LLM_DATA_DIR";
16const WRITE_PROBE_PREFIX: &str = ".mesh-llm-write-probe";
17
18static WRITE_PROBE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum DownloadDirectoryKind {
22    HuggingFaceHub,
23    HuggingFaceXet,
24}
25
26impl fmt::Display for DownloadDirectoryKind {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::HuggingFaceHub => formatter.write_str("Hugging Face Hub cache"),
30            Self::HuggingFaceXet => formatter.write_str("Hugging Face Xet cache"),
31        }
32    }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct DownloadDirectoryFallback {
37    pub kind: DownloadDirectoryKind,
38    pub requested: PathBuf,
39    pub selected: PathBuf,
40    pub error: String,
41    pub raw_os_error: Option<i32>,
42}
43
44impl fmt::Display for DownloadDirectoryFallback {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(
47            formatter,
48            "{} {} is not writable ({}); using {}. Set {} or {} to choose a different writable location.",
49            self.kind,
50            self.requested.display(),
51            self.error,
52            self.selected.display(),
53            match self.kind {
54                DownloadDirectoryKind::HuggingFaceHub => HUB_CACHE_ENV,
55                DownloadDirectoryKind::HuggingFaceXet => XET_CACHE_ENV,
56            },
57            MESH_LLM_DATA_DIR_ENV,
58        )
59    }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct PreparedDownloadDirectories {
64    pub hub_cache: PathBuf,
65    pub xet_cache: PathBuf,
66    pub fallbacks: Vec<DownloadDirectoryFallback>,
67}
68
69impl PreparedDownloadDirectories {
70    /// Apply the validated directories to the process environment.
71    ///
72    /// # Safety
73    ///
74    /// The caller must guarantee that no other threads are reading or writing
75    /// process environment variables. The shipped binary calls this before it
76    /// constructs its Tokio runtime.
77    pub unsafe fn apply_to_process_environment(&self) {
78        // SAFETY: Upheld by the caller as documented above.
79        unsafe {
80            std::env::set_var(HUB_CACHE_ENV, &self.hub_cache);
81            std::env::set_var(XET_CACHE_ENV, &self.xet_cache);
82        }
83    }
84}
85
86pub fn huggingface_hub_cache_dir() -> PathBuf {
87    requested_hub_cache_dir()
88}
89
90pub fn huggingface_xet_cache_dir() -> PathBuf {
91    requested_xet_cache_dir()
92}
93
94pub fn mesh_llm_cache_dir() -> PathBuf {
95    dirs::cache_dir()
96        .or_else(|| dirs::home_dir().map(|home| home.join(".cache")))
97        .unwrap_or_else(|| std::env::temp_dir().join("mesh-llm-cache"))
98        .join("mesh-llm")
99}
100
101pub fn prepare_download_directories() -> Result<PreparedDownloadDirectories> {
102    prepare_download_directories_in(
103        requested_hub_cache_dir(),
104        requested_xet_cache_dir(),
105        fallback_data_roots(),
106    )
107}
108
109pub fn download_cache_diagnostic() -> String {
110    download_cache_diagnostic_for(&huggingface_hub_cache_dir())
111}
112
113pub fn download_cache_diagnostic_for(hub_cache: &Path) -> String {
114    format!(
115        "Hub cache: {}; Xet cache: {}. Ensure both paths are writable, or set {} to a writable application-data directory",
116        hub_cache.display(),
117        huggingface_xet_cache_dir().display(),
118        MESH_LLM_DATA_DIR_ENV,
119    )
120}
121
122fn requested_hub_cache_dir() -> PathBuf {
123    env_path(HUB_CACHE_ENV)
124        .or_else(|| env_path(HUB_CACHE_ALIAS_ENV))
125        .or_else(|| env_path(HF_HOME_ENV).map(|path| path.join("hub")))
126        .or_else(|| env_path(XDG_CACHE_HOME_ENV).map(|path| path.join("huggingface").join("hub")))
127        .or_else(|| dirs::cache_dir().map(|path| path.join("huggingface").join("hub")))
128        .unwrap_or_else(|| default_data_root().join("huggingface").join("hub"))
129}
130
131fn requested_xet_cache_dir() -> PathBuf {
132    env_path(XET_CACHE_ENV)
133        .or_else(|| env_path(HF_HOME_ENV).map(|path| path.join("xet")))
134        .or_else(|| env_path(XDG_CACHE_HOME_ENV).map(|path| path.join("huggingface").join("xet")))
135        .or_else(|| dirs::cache_dir().map(|path| path.join("huggingface").join("xet")))
136        .unwrap_or_else(|| default_data_root().join("huggingface").join("xet"))
137}
138
139fn env_path(key: &str) -> Option<PathBuf> {
140    let value = std::env::var_os(key)?;
141    nonempty_path(value)
142}
143
144fn nonempty_path(value: OsString) -> Option<PathBuf> {
145    if value.to_str().is_some_and(|value| value.trim().is_empty()) {
146        return None;
147    }
148    let path = PathBuf::from(value);
149    (!path.as_os_str().is_empty()).then_some(path)
150}
151
152fn default_data_root() -> PathBuf {
153    env_path(MESH_LLM_DATA_DIR_ENV)
154        .or_else(|| dirs::data_local_dir().map(|path| path.join("mesh-llm")))
155        .or_else(|| dirs::home_dir().map(|path| path.join(".mesh-llm").join("data")))
156        .unwrap_or_else(|| std::env::temp_dir().join("mesh-llm-data"))
157}
158
159fn fallback_data_roots() -> Vec<PathBuf> {
160    let mut roots = Vec::new();
161    if let Some(path) = env_path(MESH_LLM_DATA_DIR_ENV) {
162        roots.push(path);
163    }
164    if let Some(path) = dirs::data_local_dir() {
165        roots.push(path.join("mesh-llm"));
166    }
167    if let Some(path) = dirs::home_dir() {
168        roots.push(path.join(".mesh-llm").join("data"));
169    }
170    roots.push(std::env::temp_dir().join("mesh-llm-data"));
171    deduplicate_paths(roots)
172}
173
174fn deduplicate_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
175    let mut seen = HashSet::new();
176    paths
177        .into_iter()
178        .filter(|path| seen.insert(path.clone()))
179        .collect()
180}
181
182fn prepare_download_directories_in(
183    requested_hub: PathBuf,
184    requested_xet: PathBuf,
185    fallback_roots: Vec<PathBuf>,
186) -> Result<PreparedDownloadDirectories> {
187    let (hub_cache, hub_fallback) = prepare_directory(
188        DownloadDirectoryKind::HuggingFaceHub,
189        requested_hub,
190        fallback_roots
191            .iter()
192            .map(|root| root.join("huggingface").join("hub")),
193    )?;
194    let (xet_cache, xet_fallback) = prepare_directory(
195        DownloadDirectoryKind::HuggingFaceXet,
196        requested_xet,
197        fallback_roots
198            .iter()
199            .map(|root| root.join("huggingface").join("xet")),
200    )?;
201    let fallbacks = hub_fallback.into_iter().chain(xet_fallback).collect();
202    Ok(PreparedDownloadDirectories {
203        hub_cache,
204        xet_cache,
205        fallbacks,
206    })
207}
208
209fn prepare_directory(
210    kind: DownloadDirectoryKind,
211    requested: PathBuf,
212    fallbacks: impl IntoIterator<Item = PathBuf>,
213) -> Result<(PathBuf, Option<DownloadDirectoryFallback>)> {
214    let requested_error = match probe_writable_directory(&requested) {
215        Ok(()) => return Ok((requested, None)),
216        Err(error) => error,
217    };
218    let mut attempts = vec![format!("{}: {}", requested.display(), requested_error)];
219    for fallback in deduplicate_paths(fallbacks.into_iter().collect()) {
220        if fallback == requested {
221            continue;
222        }
223        match probe_writable_directory(&fallback) {
224            Ok(()) => {
225                return Ok((
226                    fallback.clone(),
227                    Some(DownloadDirectoryFallback {
228                        kind,
229                        requested,
230                        selected: fallback,
231                        error: requested_error.to_string(),
232                        raw_os_error: requested_error.raw_os_error(),
233                    }),
234                ));
235            }
236            Err(error) => attempts.push(format!("{}: {}", fallback.display(), error)),
237        }
238    }
239    bail!(
240        "no writable {kind} directory was available; tried {}",
241        attempts.join("; ")
242    )
243}
244
245fn probe_writable_directory(path: &Path) -> std::io::Result<()> {
246    std::fs::create_dir_all(path)?;
247    let sequence = WRITE_PROBE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
248    let probe = path.join(format!(
249        "{WRITE_PROBE_PREFIX}-{}-{sequence}",
250        std::process::id()
251    ));
252    let mut file = OpenOptions::new()
253        .write(true)
254        .create_new(true)
255        .open(&probe)?;
256    file.write_all(b"mesh-llm")?;
257    drop(file);
258    std::fs::remove_file(&probe)
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn writable_requested_directories_are_preserved() {
267        let temp = tempfile::tempdir().unwrap();
268        let hub = temp.path().join("requested-hub");
269        let xet = temp.path().join("requested-xet");
270
271        let prepared = prepare_download_directories_in(
272            hub.clone(),
273            xet.clone(),
274            vec![temp.path().join("fallback")],
275        )
276        .unwrap();
277
278        assert_eq!(prepared.hub_cache, hub);
279        assert_eq!(prepared.xet_cache, xet);
280        assert!(prepared.fallbacks.is_empty());
281    }
282
283    #[test]
284    fn non_directory_cache_path_uses_writable_data_fallback() {
285        let temp = tempfile::tempdir().unwrap();
286        let blocked_parent = temp.path().join("not-a-directory");
287        std::fs::write(&blocked_parent, b"file").unwrap();
288        let fallback = temp.path().join("data");
289
290        let prepared = prepare_download_directories_in(
291            blocked_parent.join("hub"),
292            blocked_parent.join("xet"),
293            vec![fallback.clone()],
294        )
295        .unwrap();
296
297        assert_eq!(prepared.hub_cache, fallback.join("huggingface").join("hub"));
298        assert_eq!(prepared.xet_cache, fallback.join("huggingface").join("xet"));
299        assert_eq!(prepared.fallbacks.len(), 2);
300        assert!(
301            prepared
302                .fallbacks
303                .iter()
304                .all(|warning| !warning.error.is_empty())
305        );
306    }
307
308    #[cfg(any(target_os = "linux", target_os = "macos"))]
309    #[test]
310    fn operating_system_read_only_directory_uses_writable_fallback() {
311        let temp = tempfile::tempdir().unwrap();
312        let read_only_root = if cfg!(target_os = "macos") {
313            PathBuf::from("/System")
314        } else {
315            PathBuf::from("/sys")
316        };
317        let requested_hub = read_only_root.join("mesh-llm-issue-980").join("hub");
318        let writable_xet = temp.path().join("xet");
319
320        let prepared = prepare_download_directories_in(
321            requested_hub.clone(),
322            writable_xet.clone(),
323            vec![temp.path().join("data")],
324        )
325        .unwrap();
326
327        assert_ne!(prepared.hub_cache, requested_hub);
328        assert_eq!(prepared.xet_cache, writable_xet);
329        assert_eq!(prepared.fallbacks.len(), 1);
330        let warning = &prepared.fallbacks[0];
331        assert!(matches!(
332            warning.raw_os_error,
333            Some(30) | Some(13) | Some(1)
334        ));
335    }
336
337    #[test]
338    fn empty_environment_path_is_ignored() {
339        assert_eq!(nonempty_path(OsString::new()), None);
340        assert_eq!(nonempty_path(OsString::from("   ")), None);
341    }
342
343    #[test]
344    fn read_only_filesystem_warning_includes_os_error_and_recovery_path() {
345        let warning = DownloadDirectoryFallback {
346            kind: DownloadDirectoryKind::HuggingFaceXet,
347            requested: PathBuf::from("/"),
348            selected: PathBuf::from("/writable/data/huggingface/xet"),
349            error: std::io::Error::from_raw_os_error(30).to_string(),
350            raw_os_error: Some(30),
351        };
352
353        let message = warning.to_string();
354        assert!(message.contains("Read-only file system"));
355        assert!(message.contains("/writable/data/huggingface/xet"));
356        assert!(message.contains("HF_XET_CACHE"));
357        assert!(message.contains("MESH_LLM_DATA_DIR"));
358    }
359}