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(windows)]
165    {
166        env::var(EnvVars::SYSTEMDRIVE)
167            .ok()
168            .and_then(|system_drive| locate_system_config_windows(format!("{system_drive}\\")))
169    }
170
171    #[cfg(not(windows))]
172    {
173        if let Some(path) =
174            locate_system_config_xdg(env::var(EnvVars::XDG_CONFIG_DIRS).ok().as_deref())
175        {
176            return Some(path);
177        }
178
179        // Fallback to `/etc/uv/uv.toml` if `XDG_CONFIG_DIRS` is not set or no valid
180        // path is found.
181        let candidate = Path::new("/etc/uv/uv.toml");
182        match candidate.try_exists() {
183            Ok(true) => Some(candidate.to_path_buf()),
184            Ok(false) => None,
185            Err(err) => {
186                tracing::warn!("Failed to query system configuration file: {err}");
187                None
188            }
189        }
190    }
191}
192
193#[cfg(test)]
194mod test {
195    #[cfg(windows)]
196    use crate::locate_system_config_windows;
197    #[cfg(not(windows))]
198    use crate::locate_system_config_xdg;
199
200    use assert_fs::fixture::FixtureError;
201    use assert_fs::prelude::*;
202    use indoc::indoc;
203
204    #[test]
205    #[cfg(not(windows))]
206    fn test_locate_system_config_xdg() -> Result<(), FixtureError> {
207        // Write a `uv.toml` to a temporary directory.
208        let context = assert_fs::TempDir::new()?;
209        context.child("uv").child("uv.toml").write_str(indoc! {
210            r#"
211            [pip]
212            index-url = "https://test.pypi.org/simple"
213        "#,
214        })?;
215
216        // None
217        assert_eq!(locate_system_config_xdg(None), None);
218
219        // Empty string
220        assert_eq!(locate_system_config_xdg(Some("")), None);
221
222        // Single colon
223        assert_eq!(locate_system_config_xdg(Some(":")), None);
224
225        // Assert that the `system_config_file` function returns the correct path.
226        assert_eq!(
227            locate_system_config_xdg(Some(context.to_str().unwrap())).unwrap(),
228            context.child("uv").child("uv.toml").path()
229        );
230
231        // Write a separate `uv.toml` to a different directory.
232        let first = context.child("first");
233        let first_config = first.child("uv").child("uv.toml");
234        first_config.write_str("")?;
235
236        assert_eq!(
237            locate_system_config_xdg(Some(
238                format!("{}:{}", first.to_string_lossy(), context.to_string_lossy()).as_str()
239            ))
240            .unwrap(),
241            first_config.path()
242        );
243
244        Ok(())
245    }
246
247    #[test]
248    #[cfg(unix)]
249    fn test_locate_system_config_xdg_unix_permissions() -> Result<(), FixtureError> {
250        let context = assert_fs::TempDir::new()?;
251        let config = context.child("uv").child("uv.toml");
252        config.write_str("")?;
253        fs_err::set_permissions(
254            &context,
255            std::os::unix::fs::PermissionsExt::from_mode(0o000),
256        )
257        .unwrap();
258
259        assert_eq!(
260            locate_system_config_xdg(Some(context.to_str().unwrap())),
261            None
262        );
263
264        Ok(())
265    }
266
267    #[test]
268    #[cfg(windows)]
269    fn test_windows_config() -> Result<(), FixtureError> {
270        // Write a `uv.toml` to a temporary directory.
271        let context = assert_fs::TempDir::new()?;
272        context
273            .child("ProgramData")
274            .child("uv")
275            .child("uv.toml")
276            .write_str(indoc! { r#"
277            [pip]
278            index-url = "https://test.pypi.org/simple"
279        "#})?;
280
281        // This is typically only a drive (that is, letter and colon) but we
282        // allow anything, including a path to the test fixtures...
283        assert_eq!(
284            locate_system_config_windows(context.path()).unwrap(),
285            context
286                .child("ProgramData")
287                .child("uv")
288                .child("uv.toml")
289                .path()
290        );
291
292        // This does not have a `ProgramData` child, so contains no config.
293        let context = assert_fs::TempDir::new()?;
294        assert_eq!(locate_system_config_windows(context.path()), None);
295
296        Ok(())
297    }
298}