Skip to main content

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_xdg_path))
29        .or_else(|| {
30            std::env::var_os(EnvVars::XDG_DATA_HOME)
31                .and_then(parse_xdg_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`] from the given [`OsString`], if non-empty.
87///
88/// Unlike [`parse_xdg_path`], this function accepts both relative and absolute paths,
89/// for use with uv-specific override variables that are not subject to the XDG specification.
90fn parse_path(path: OsString) -> Option<PathBuf> {
91    if path.is_empty() {
92        None
93    } else {
94        Some(PathBuf::from(path))
95    }
96}
97
98/// Return a [`PathBuf`] if the given [`OsString`] is an absolute path.
99///
100/// Relative paths are considered invalid per the [XDG Base Directory Specification]:
101///
102/// > All paths set in these environment variables must be absolute. If an implementation
103/// > encounters a relative path in any of these variables it should consider the path invalid
104/// > and ignore it.
105///
106/// [XDG Base Directory Specification]: https://specifications.freedesktop.org/basedir-spec/latest/
107fn parse_xdg_path(path: OsString) -> Option<PathBuf> {
108    let path = PathBuf::from(path);
109    if path.is_absolute() { Some(path) } else { None }
110}
111
112/// Returns the path to the user configuration directory.
113///
114/// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming
115/// On Linux and macOS, use `XDG_CONFIG_HOME` or $HOME/.config, e.g., /home/alice/.config.
116pub fn user_config_dir() -> Option<PathBuf> {
117    etcetera::choose_base_strategy()
118        .map(|dirs| dirs.config_dir())
119        .ok()
120}
121
122pub fn user_uv_config_dir() -> Option<PathBuf> {
123    user_config_dir().map(|mut path| {
124        path.push("uv");
125        path
126    })
127}
128
129#[cfg(not(windows))]
130fn locate_system_config_xdg(value: Option<&str>) -> Option<PathBuf> {
131    // On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable.
132
133    use std::path::Path;
134    let default = "/etc/xdg";
135    let config_dirs = value.filter(|s| !s.is_empty()).unwrap_or(default);
136
137    for dir in config_dirs.split(':').take_while(|s| !s.is_empty()) {
138        let uv_toml_path = Path::new(dir).join("uv").join("uv.toml");
139        if uv_toml_path.is_file() {
140            return Some(uv_toml_path);
141        }
142    }
143    None
144}
145
146#[cfg(windows)]
147fn locate_system_config_windows(system_drive: impl AsRef<Path>) -> Option<PathBuf> {
148    // On Windows, use `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` (e.g., `C:\ProgramData`).
149    let candidate = system_drive
150        .as_ref()
151        .join("ProgramData")
152        .join("uv")
153        .join("uv.toml");
154    candidate.as_path().is_file().then_some(candidate)
155}
156
157/// Returns the path to the system configuration file.
158///
159/// On Unix-like systems, uses the `XDG_CONFIG_DIRS` environment variable (falling back to
160/// `/etc/xdg/uv/uv.toml` if unset or empty) and then `/etc/uv/uv.toml`
161///
162/// On Windows, uses `%SYSTEMDRIVE%\ProgramData\uv\uv.toml`.
163pub fn system_config_file() -> Option<PathBuf> {
164    cfg_select! {
165        windows => {
166            env::var(EnvVars::SYSTEMDRIVE)
167                .ok()
168                .and_then(|system_drive| locate_system_config_windows(format!("{system_drive}\\")))
169        },
170        _ => {
171            if let Some(path) =
172                locate_system_config_xdg(env::var(EnvVars::XDG_CONFIG_DIRS).ok().as_deref())
173            {
174                return Some(path);
175            }
176
177            // Fallback to `/etc/uv/uv.toml` if `XDG_CONFIG_DIRS` is not set or no valid
178            // path is found.
179            let candidate = Path::new("/etc/uv/uv.toml");
180            match candidate.try_exists() {
181                Ok(true) => Some(candidate.to_path_buf()),
182                Ok(false) => None,
183                Err(err) => {
184                    tracing::warn!("Failed to query system configuration file: {err}");
185                    None
186                }
187            }
188        },
189    }
190}
191
192#[cfg(test)]
193mod test {
194    #[cfg(windows)]
195    use crate::locate_system_config_windows;
196    #[cfg(not(windows))]
197    use crate::locate_system_config_xdg;
198
199    use assert_fs::fixture::FixtureError;
200    use assert_fs::prelude::*;
201    use indoc::indoc;
202
203    #[test]
204    #[cfg(not(windows))]
205    fn test_locate_system_config_xdg() -> Result<(), FixtureError> {
206        // Write a `uv.toml` to a temporary directory.
207        let context = assert_fs::TempDir::new()?;
208        context.child("uv").child("uv.toml").write_str(indoc! {
209            r#"
210            [pip]
211            index-url = "https://test.pypi.org/simple"
212        "#,
213        })?;
214
215        // None
216        assert_eq!(locate_system_config_xdg(None), None);
217
218        // Empty string
219        assert_eq!(locate_system_config_xdg(Some("")), None);
220
221        // Single colon
222        assert_eq!(locate_system_config_xdg(Some(":")), None);
223
224        // Assert that the `system_config_file` function returns the correct path.
225        assert_eq!(
226            locate_system_config_xdg(Some(context.to_str().unwrap())).unwrap(),
227            context.child("uv").child("uv.toml").path()
228        );
229
230        // Write a separate `uv.toml` to a different directory.
231        let first = context.child("first");
232        let first_config = first.child("uv").child("uv.toml");
233        first_config.write_str("")?;
234
235        assert_eq!(
236            locate_system_config_xdg(Some(
237                format!("{}:{}", first.to_string_lossy(), context.to_string_lossy()).as_str()
238            ))
239            .unwrap(),
240            first_config.path()
241        );
242
243        Ok(())
244    }
245
246    #[test]
247    #[cfg(unix)]
248    fn test_locate_system_config_xdg_unix_permissions() -> Result<(), FixtureError> {
249        let context = assert_fs::TempDir::new()?;
250        let config = context.child("uv").child("uv.toml");
251        config.write_str("")?;
252        fs_err::set_permissions(
253            &context,
254            std::os::unix::fs::PermissionsExt::from_mode(0o000),
255        )
256        .unwrap();
257
258        assert_eq!(
259            locate_system_config_xdg(Some(context.to_str().unwrap())),
260            None
261        );
262
263        Ok(())
264    }
265
266    #[test]
267    #[cfg(windows)]
268    fn test_windows_config() -> Result<(), FixtureError> {
269        // Write a `uv.toml` to a temporary directory.
270        let context = assert_fs::TempDir::new()?;
271        context
272            .child("ProgramData")
273            .child("uv")
274            .child("uv.toml")
275            .write_str(indoc! { r#"
276            [pip]
277            index-url = "https://test.pypi.org/simple"
278        "#})?;
279
280        // This is typically only a drive (that is, letter and colon) but we
281        // allow anything, including a path to the test fixtures...
282        assert_eq!(
283            locate_system_config_windows(context.path()).unwrap(),
284            context
285                .child("ProgramData")
286                .child("uv")
287                .child("uv.toml")
288                .path()
289        );
290
291        // This does not have a `ProgramData` child, so contains no config.
292        let context = assert_fs::TempDir::new()?;
293        assert_eq!(locate_system_config_windows(context.path()), None);
294
295        Ok(())
296    }
297}