gglib_core/paths/
models.rs

1//! Models directory resolution.
2//!
3//! Provides utilities for resolving the models directory from explicit paths,
4//! environment variables, or platform defaults.
5
6use std::env;
7use std::path::PathBuf;
8
9use super::error::PathError;
10use super::platform::normalize_user_path;
11
12/// Default relative location for downloaded models on non-Windows platforms.
13#[cfg(not(target_os = "windows"))]
14pub const DEFAULT_MODELS_DIR_RELATIVE: &str = ".local/share/llama_models";
15
16/// How the models directory was derived.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ModelsDirSource {
19    /// The user passed an explicit path (e.g., CLI flag or GUI form).
20    Explicit,
21    /// The path came from environment variables / `.env`.
22    EnvVar,
23    /// Fallback default (`~/.local/share/llama_models` on Linux/macOS,
24    /// `%LOCALAPPDATA%\llama_models` on Windows).
25    Default,
26}
27
28/// Resolution result for the models directory.
29#[derive(Debug, Clone)]
30pub struct ModelsDirResolution {
31    /// The resolved path to the models directory.
32    pub path: PathBuf,
33    /// How the path was determined.
34    pub source: ModelsDirSource,
35}
36
37/// Return the platform-specific default models directory.
38///
39/// - **Windows**: `%LOCALAPPDATA%\llama_models` (e.g. `C:\Users\name\AppData\Local\llama_models`)
40/// - **macOS / Linux**: `~/.local/share/llama_models`
41pub fn default_models_dir() -> Result<PathBuf, PathError> {
42    #[cfg(target_os = "windows")]
43    {
44        let local_app_data = dirs::data_local_dir().ok_or(PathError::NoDataDir)?;
45        Ok(local_app_data.join("llama_models"))
46    }
47    #[cfg(not(target_os = "windows"))]
48    {
49        let home = dirs::home_dir().ok_or(PathError::NoHomeDir)?;
50        Ok(home.join(DEFAULT_MODELS_DIR_RELATIVE))
51    }
52}
53
54/// Resolve the models directory from an explicit override, env var, or default.
55///
56/// Resolution order:
57/// 1. Explicit path provided by caller (highest priority)
58/// 2. `GGLIB_MODELS_DIR` environment variable
59/// 3. Default models directory (`~/.local/share/llama_models`)
60pub fn resolve_models_dir(explicit: Option<&str>) -> Result<ModelsDirResolution, PathError> {
61    if let Some(path_str) = explicit {
62        return Ok(ModelsDirResolution {
63            path: normalize_user_path(path_str)?,
64            source: ModelsDirSource::Explicit,
65        });
66    }
67
68    if let Ok(env_path) = env::var("GGLIB_MODELS_DIR") {
69        if !env_path.trim().is_empty() {
70            return Ok(ModelsDirResolution {
71                path: normalize_user_path(&env_path)?,
72                source: ModelsDirSource::EnvVar,
73            });
74        }
75    }
76
77    Ok(ModelsDirResolution {
78        path: default_models_dir()?,
79        source: ModelsDirSource::Default,
80    })
81}
82
83#[cfg(test)]
84#[allow(unsafe_code)]
85mod tests {
86    use super::*;
87    use serial_test::serial;
88
89    #[test]
90    fn test_default_models_dir_platform_path() {
91        let dir = default_models_dir().unwrap();
92        let path_str = dir.to_string_lossy();
93        // On Windows the path should be under %LOCALAPPDATA% and use native
94        // separators throughout — no forward-slash fragments.
95        #[cfg(target_os = "windows")]
96        {
97            assert!(
98                path_str.contains("llama_models"),
99                "Expected 'llama_models' in path: {path_str}"
100            );
101            assert!(
102                !path_str.contains('/'),
103                "Path must not contain forward slashes on Windows: {path_str}"
104            );
105        }
106        // On non-Windows the path should sit under ~/.local/share/llama_models.
107        #[cfg(not(target_os = "windows"))]
108        assert!(
109            path_str.contains(DEFAULT_MODELS_DIR_RELATIVE),
110            "Expected '{DEFAULT_MODELS_DIR_RELATIVE}' in path: {path_str}"
111        );
112    }
113
114    #[test]
115    #[serial]
116    fn test_resolve_models_dir_prefers_explicit() {
117        let prev = env::var("GGLIB_MODELS_DIR").ok();
118        unsafe {
119            env::set_var("GGLIB_MODELS_DIR", "/tmp/env-value");
120        }
121        let resolved = resolve_models_dir(Some("/tmp/explicit")).unwrap();
122        assert_eq!(resolved.source, ModelsDirSource::Explicit);
123        assert!(resolved.path.ends_with("explicit"));
124        restore_env("GGLIB_MODELS_DIR", prev);
125    }
126
127    #[test]
128    #[serial]
129    fn test_resolve_models_dir_env_value() {
130        let prev = env::var("GGLIB_MODELS_DIR").ok();
131        unsafe {
132            env::set_var("GGLIB_MODELS_DIR", "/tmp/from-env");
133        }
134        let resolved = resolve_models_dir(None).unwrap();
135        assert_eq!(resolved.source, ModelsDirSource::EnvVar);
136        assert!(resolved.path.ends_with("from-env"));
137        restore_env("GGLIB_MODELS_DIR", prev);
138    }
139
140    fn restore_env(key: &str, previous: Option<String>) {
141        if let Some(value) = previous {
142            unsafe {
143                env::set_var(key, value);
144            }
145        } else {
146            unsafe {
147                env::remove_var(key);
148            }
149        }
150    }
151}