gglib_core/paths/
models.rs1use std::env;
7use std::path::PathBuf;
8
9use super::error::PathError;
10use super::platform::normalize_user_path;
11
12#[cfg(not(target_os = "windows"))]
14pub const DEFAULT_MODELS_DIR_RELATIVE: &str = ".local/share/llama_models";
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ModelsDirSource {
19 Explicit,
21 EnvVar,
23 Default,
26}
27
28#[derive(Debug, Clone)]
30pub struct ModelsDirResolution {
31 pub path: PathBuf,
33 pub source: ModelsDirSource,
35}
36
37pub 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
54pub 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 #[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 #[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}