uv_dirs/
lib.rs

1use std::{
2    env,
3    ffi::OsString,
4    path::{Path, PathBuf},
5};
6
7use etcetera::BaseStrategy;
8
9use uv_static::EnvVars;
10
11/// Returns an appropriate user-level directory for storing executables.
12///
13/// This follows, in order:
14///
15/// - `$OVERRIDE_VARIABLE` (if provided)
16/// - `$XDG_BIN_HOME`
17/// - `$XDG_DATA_HOME/../bin`
18/// - `$HOME/.local/bin`
19///
20/// On all platforms.
21///
22/// Returns `None` if a directory cannot be found, i.e., if `$HOME` cannot be resolved. Does not
23/// check if the directory exists.
24pub fn user_executable_directory(override_variable: Option<&'static str>) -> Option<PathBuf> {
25    override_variable
26        .and_then(std::env::var_os)
27        .and_then(parse_path)
28        .or_else(|| std::env::var_os(EnvVars::XDG_BIN_HOME).and_then(parse_path))
29        .or_else(|| {
30            std::env::var_os(EnvVars::XDG_DATA_HOME)
31                .and_then(parse_path)
32                .map(|path| path.join("../bin"))
33        })
34        .or_else(|| {
35            let home_dir = etcetera::home_dir().ok();
36            home_dir.map(|path| path.join(".local").join("bin"))
37        })
38}
39
40/// Returns an appropriate user-level directory for storing the cache.
41///
42/// Corresponds to `$XDG_CACHE_HOME/uv` on Unix.
43pub fn user_cache_dir() -> Option<PathBuf> {
44    etcetera::base_strategy::choose_base_strategy()
45        .ok()
46        .map(|dirs| dirs.cache_dir().join("uv"))
47}
48
49/// Returns the legacy cache directory path.
50///
51/// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference
52/// for using the XDG directories on all Unix platforms.
53pub fn legacy_user_cache_dir() -> Option<PathBuf> {
54    etcetera::base_strategy::choose_native_strategy()
55        .ok()
56        .map(|dirs| dirs.cache_dir().join("uv"))
57        .map(|dir| {
58            if cfg!(windows) {
59                dir.join("cache")
60            } else {
61                dir
62            }
63        })
64}
65
66/// Returns an appropriate user-level directory for storing application state.
67///
68/// Corresponds to `$XDG_DATA_HOME/uv` on Unix.
69pub fn user_state_dir() -> Option<PathBuf> {
70    etcetera::base_strategy::choose_base_strategy()
71        .ok()
72        .map(|dirs| dirs.data_dir().join("uv"))
73}
74
75/// Returns the legacy state directory path.
76///
77/// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference
78/// for using the XDG directories on all Unix platforms.
79pub fn legacy_user_state_dir() -> Option<PathBuf> {
80    etcetera::base_strategy::choose_native_strategy()
81        .ok()
82        .map(|dirs| dirs.data_dir().join("uv"))
83        .map(|dir| if cfg!(windows) { dir.join("data") } else { dir })
84}
85
86/// Return a [`PathBuf`] if the given [`OsString`] is an absolute path.
87fn parse_path(path: OsString) -> Option<PathBuf> {
88    let path = PathBuf::from(path);
89    if path.is_absolute() { Some(path) } else { None }
90}
91
92/// Returns the path to the user configuration directory.
93///
94/// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming
95/// On Linux and macOS, use `XDG_CONFIG_HOME` or $HOME/.config, e.g., /home/alice/.config.
96pub fn user_config_dir() -> Option<PathBuf> {
97    etcetera::choose_base_strategy()
98        .map(|dirs| dirs.config_dir())
99        .ok()
100}
101
102pub fn user_uv_config_dir() -> Option<PathBuf> {
103    user_config_dir().map(|mut path| {
104        path.push("uv");
105        path
106    })
107}
108
109#[cfg(not(windows))]
110fn locate_system_config_xdg(value: Option<&str>) -> Option<PathBuf> {
111    // On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable.
112
113    use std::path::Path;
114    let default = "/etc/xdg";
115    let config_dirs = value.filter(|s| !s.is_empty()).unwrap_or(default);
116
117    for dir in config_dirs.split(':').take_while(|s| !s.is_empty()) {
118        let uv_toml_path = Path::new(dir).join("uv").join("uv.toml");
119        if uv_toml_path.is_file() {
120            return Some(uv_toml_path);
121        }
122    }
123    None
124}
125
126#[cfg(windows)]
127fn locate_system_config_windows(system_drive: impl AsRef<Path>) -> Option<PathBuf> {
128    // On Windows, use `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` (e.g., `C:\ProgramData`).
129    let candidate = system_drive
130        .as_ref()
131        .join("ProgramData")
132        .join("uv")
133        .join("uv.toml");
134    candidate.as_path().is_file().then_some(candidate)
135}
136
137/// Returns the path to the system configuration file.
138///
139/// On Unix-like systems, uses the `XDG_CONFIG_DIRS` environment variable (falling back to
140/// `/etc/xdg/uv/uv.toml` if unset or empty) and then `/etc/uv/uv.toml`
141///
142/// On Windows, uses `%SYSTEMDRIVE%\ProgramData\uv\uv.toml`.
143pub fn system_config_file() -> Option<PathBuf> {
144    #[cfg(windows)]
145    {
146        env::var(EnvVars::SYSTEMDRIVE)
147            .ok()
148            .and_then(|system_drive| locate_system_config_windows(format!("{system_drive}\\")))
149    }
150
151    #[cfg(not(windows))]
152    {
153        if let Some(path) =
154            locate_system_config_xdg(env::var(EnvVars::XDG_CONFIG_DIRS).ok().as_deref())
155        {
156            return Some(path);
157        }
158
159        // Fallback to `/etc/uv/uv.toml` if `XDG_CONFIG_DIRS` is not set or no valid
160        // path is found.
161        let candidate = Path::new("/etc/uv/uv.toml");
162        match candidate.try_exists() {
163            Ok(true) => Some(candidate.to_path_buf()),
164            Ok(false) => None,
165            Err(err) => {
166                tracing::warn!("Failed to query system configuration file: {err}");
167                None
168            }
169        }
170    }
171}
172
173#[cfg(test)]
174mod test {
175    #[cfg(windows)]
176    use crate::locate_system_config_windows;
177    #[cfg(not(windows))]
178    use crate::locate_system_config_xdg;
179
180    use assert_fs::fixture::FixtureError;
181    use assert_fs::prelude::*;
182    use indoc::indoc;
183
184    #[test]
185    #[cfg(not(windows))]
186    fn test_locate_system_config_xdg() -> Result<(), FixtureError> {
187        // Write a `uv.toml` to a temporary directory.
188        let context = assert_fs::TempDir::new()?;
189        context.child("uv").child("uv.toml").write_str(indoc! {
190            r#"
191            [pip]
192            index-url = "https://test.pypi.org/simple"
193        "#,
194        })?;
195
196        // None
197        assert_eq!(locate_system_config_xdg(None), None);
198
199        // Empty string
200        assert_eq!(locate_system_config_xdg(Some("")), None);
201
202        // Single colon
203        assert_eq!(locate_system_config_xdg(Some(":")), None);
204
205        // Assert that the `system_config_file` function returns the correct path.
206        assert_eq!(
207            locate_system_config_xdg(Some(context.to_str().unwrap())).unwrap(),
208            context.child("uv").child("uv.toml").path()
209        );
210
211        // Write a separate `uv.toml` to a different directory.
212        let first = context.child("first");
213        let first_config = first.child("uv").child("uv.toml");
214        first_config.write_str("")?;
215
216        assert_eq!(
217            locate_system_config_xdg(Some(
218                format!("{}:{}", first.to_string_lossy(), context.to_string_lossy()).as_str()
219            ))
220            .unwrap(),
221            first_config.path()
222        );
223
224        Ok(())
225    }
226
227    #[test]
228    #[cfg(unix)]
229    fn test_locate_system_config_xdg_unix_permissions() -> Result<(), FixtureError> {
230        let context = assert_fs::TempDir::new()?;
231        let config = context.child("uv").child("uv.toml");
232        config.write_str("")?;
233        fs_err::set_permissions(
234            &context,
235            std::os::unix::fs::PermissionsExt::from_mode(0o000),
236        )
237        .unwrap();
238
239        assert_eq!(
240            locate_system_config_xdg(Some(context.to_str().unwrap())),
241            None
242        );
243
244        Ok(())
245    }
246
247    #[test]
248    #[cfg(windows)]
249    fn test_windows_config() -> Result<(), FixtureError> {
250        // Write a `uv.toml` to a temporary directory.
251        let context = assert_fs::TempDir::new()?;
252        context
253            .child("ProgramData")
254            .child("uv")
255            .child("uv.toml")
256            .write_str(indoc! { r#"
257            [pip]
258            index-url = "https://test.pypi.org/simple"
259        "#})?;
260
261        // This is typically only a drive (that is, letter and colon) but we
262        // allow anything, including a path to the test fixtures...
263        assert_eq!(
264            locate_system_config_windows(context.path()).unwrap(),
265            context
266                .child("ProgramData")
267                .child("uv")
268                .child("uv.toml")
269                .path()
270        );
271
272        // This does not have a `ProgramData` child, so contains no config.
273        let context = assert_fs::TempDir::new()?;
274        assert_eq!(locate_system_config_windows(context.path()), None);
275
276        Ok(())
277    }
278}