diff --git a/Cargo.lock b/Cargo.lock index 2126000c4..b8c50ccff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4335,8 +4335,6 @@ name = "uv-cache" version = "0.0.1" dependencies = [ "clap", - "directories", - "etcetera", "fs-err", "nanoid", "rmp-serde", @@ -4347,6 +4345,7 @@ dependencies = [ "url", "uv-cache-info", "uv-cache-key", + "uv-dirs", "uv-distribution-types", "uv-fs", "uv-normalize", @@ -4533,6 +4532,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "uv-dirs" +version = "0.0.1" +dependencies = [ + "directories", + "dirs-sys", + "etcetera", + "uv-static", +] + [[package]] name = "uv-dispatch" version = "0.0.1" @@ -5212,10 +5221,9 @@ dependencies = [ name = "uv-state" version = "0.0.1" dependencies = [ - "directories", - "etcetera", "fs-err", "tempfile", + "uv-dirs", ] [[package]] @@ -5226,7 +5234,6 @@ version = "0.0.1" name = "uv-tool" version = "0.0.1" dependencies = [ - "dirs-sys", "fs-err", "pathdiff", "serde", @@ -5235,6 +5242,7 @@ dependencies = [ "toml_edit", "tracing", "uv-cache", + "uv-dirs", "uv-fs", "uv-install-wheel", "uv-installer", diff --git a/Cargo.toml b/Cargo.toml index 99b0dcf79..36618fe2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ uv-cli = { path = "crates/uv-cli" } uv-client = { path = "crates/uv-client" } uv-configuration = { path = "crates/uv-configuration" } uv-console = { path = "crates/uv-console" } +uv-dirs = { path = "crates/uv-dirs" } uv-dispatch = { path = "crates/uv-dispatch" } uv-distribution = { path = "crates/uv-distribution" } uv-distribution-filename = { path = "crates/uv-distribution-filename" } diff --git a/crates/uv-cache/Cargo.toml b/crates/uv-cache/Cargo.toml index 8f33d81f0..577a84cf5 100644 --- a/crates/uv-cache/Cargo.toml +++ b/crates/uv-cache/Cargo.toml @@ -17,6 +17,7 @@ doctest = false workspace = true [dependencies] +uv-dirs = { workspace = true } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-distribution-types = { workspace = true } @@ -26,8 +27,6 @@ uv-pypi-types = { workspace = true } uv-static = { workspace = true } clap = { workspace = true, features = ["derive", "env"], optional = true } -directories = { workspace = true } -etcetera = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } nanoid = { workspace = true } rmp-serde = { workspace = true } diff --git a/crates/uv-cache/src/cli.rs b/crates/uv-cache/src/cli.rs index 781b74017..a975a07f4 100644 --- a/crates/uv-cache/src/cli.rs +++ b/crates/uv-cache/src/cli.rs @@ -4,8 +4,6 @@ use uv_static::EnvVars; use crate::Cache; use clap::Parser; -use directories::ProjectDirs; -use etcetera::BaseStrategy; use tracing::{debug, warn}; #[derive(Parser, Debug, Clone)] @@ -45,18 +43,13 @@ impl Cache { Self::temp() } else if let Some(cache_dir) = cache_dir { Ok(Self::from_path(cache_dir)) - } else if let Some(cache_dir) = ProjectDirs::from("", "", "uv") - .map(|dirs| dirs.cache_dir().to_path_buf()) - .filter(|dir| dir.exists()) + } else if let Some(cache_dir) = uv_dirs::legacy_user_cache_dir().filter(|dir| dir.exists()) { // If the user has an existing directory at (e.g.) `/Users/user/Library/Caches/uv`, // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on // macOS. Ok(Self::from_path(cache_dir)) - } else if let Some(cache_dir) = etcetera::base_strategy::choose_base_strategy() - .ok() - .map(|dirs| dirs.cache_dir().join("uv")) - { + } else if let Some(cache_dir) = uv_dirs::user_cache_dir() { if cfg!(windows) { // On Windows, we append `cache` to the LocalAppData directory, i.e., prefer // `C:\Users\User\AppData\Local\uv\cache` over `C:\Users\User\AppData\Local\uv`. diff --git a/crates/uv-dirs/Cargo.toml b/crates/uv-dirs/Cargo.toml new file mode 100644 index 000000000..d32a4a4e3 --- /dev/null +++ b/crates/uv-dirs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "uv-dirs" +version = "0.0.1" +description = "Resolution of directories for storage of uv state" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +uv-static = { workspace = true } + +dirs-sys = { workspace = true } +directories = { workspace = true } +etcetera = { workspace = true } diff --git a/crates/uv-dirs/src/lib.rs b/crates/uv-dirs/src/lib.rs new file mode 100644 index 000000000..759a4df75 --- /dev/null +++ b/crates/uv-dirs/src/lib.rs @@ -0,0 +1,72 @@ +use std::path::PathBuf; + +use etcetera::BaseStrategy; + +use uv_static::EnvVars; + +/// Returns an appropriate user-level directory for storing executables. +/// +/// This follows, in order: +/// +/// - `$OVERRIDE_VARIABLE` (if provided) +/// - `$XDG_BIN_HOME` +/// - `$XDG_DATA_HOME/../bin` +/// - `$HOME/.local/bin` +/// +/// On all platforms. +/// +/// Returns `None` if a directory cannot be found, i.e., if `$HOME` cannot be resolved. Does not +/// check if the directory exists. +pub fn user_executable_directory(override_variable: Option<&'static str>) -> Option { + override_variable + .and_then(std::env::var_os) + .and_then(dirs_sys::is_absolute_path) + .or_else(|| std::env::var_os(EnvVars::XDG_BIN_HOME).and_then(dirs_sys::is_absolute_path)) + .or_else(|| { + std::env::var_os(EnvVars::XDG_DATA_HOME) + .and_then(dirs_sys::is_absolute_path) + .map(|path| path.join("../bin")) + }) + .or_else(|| { + // See https://github.com/dirs-dev/dirs-rs/blob/50b50f31f3363b7656e5e63b3fa1060217cbc844/src/win.rs#L5C58-L5C78 + #[cfg(windows)] + let home_dir = dirs_sys::known_folder_profile(); + #[cfg(not(windows))] + let home_dir = dirs_sys::home_dir(); + home_dir.map(|path| path.join(".local").join("bin")) + }) +} + +/// Returns an appropriate user-level directory for storing the cache. +/// +/// Corresponds to `$XDG_CACHE_HOME/uv` on Unix. +pub fn user_cache_dir() -> Option { + etcetera::base_strategy::choose_base_strategy() + .ok() + .map(|dirs| dirs.cache_dir().join("uv")) +} + +/// Returns the legacy cache directory path. +/// +/// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference +/// for using the XDG directories on all Unix platforms. +pub fn legacy_user_cache_dir() -> Option { + directories::ProjectDirs::from("", "", "uv").map(|dirs| dirs.cache_dir().to_path_buf()) +} + +/// Returns an appropriate user-level directory for storing application state. +/// +/// Corresponds to `$XDG_DATA_HOME/uv` on Unix. +pub fn user_state_dir() -> Option { + etcetera::base_strategy::choose_base_strategy() + .ok() + .map(|dirs| dirs.data_dir().join("uv")) +} + +/// Returns the legacy state directory path. +/// +/// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference +/// for using the XDG directories on all Unix platforms. +pub fn legacy_user_state_dir() -> Option { + directories::ProjectDirs::from("", "", "uv").map(|dirs| dirs.data_dir().to_path_buf()) +} diff --git a/crates/uv-state/Cargo.toml b/crates/uv-state/Cargo.toml index f90413be4..5e3d8bd5b 100644 --- a/crates/uv-state/Cargo.toml +++ b/crates/uv-state/Cargo.toml @@ -16,7 +16,6 @@ doctest = false workspace = true [dependencies] -directories = { workspace = true } -etcetera = { workspace = true } +uv-dirs = { workspace = true } tempfile = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-state/src/lib.rs b/crates/uv-state/src/lib.rs index 9d1f65485..90c930ccc 100644 --- a/crates/uv-state/src/lib.rs +++ b/crates/uv-state/src/lib.rs @@ -4,14 +4,13 @@ use std::{ sync::Arc, }; -use directories::ProjectDirs; -use etcetera::BaseStrategy; use fs_err as fs; use tempfile::{tempdir, TempDir}; /// The main state storage abstraction. /// -/// This is appropriate +/// This is appropriate for storing persistent data that is not user-facing, such as managed Python +/// installations or tool environments. #[derive(Debug, Clone)] pub struct StateStore { /// The state storage. @@ -85,18 +84,12 @@ impl StateStore { pub fn from_settings(state_dir: Option) -> Result { if let Some(state_dir) = state_dir { StateStore::from_path(state_dir) - } else if let Some(data_dir) = ProjectDirs::from("", "", "uv") - .map(|dirs| dirs.data_dir().to_path_buf()) - .filter(|dir| dir.exists()) - { + } else if let Some(data_dir) = uv_dirs::legacy_user_state_dir().filter(|dir| dir.exists()) { // If the user has an existing directory at (e.g.) `/Users/user/Library/Application Support/uv`, // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on // macOS. StateStore::from_path(data_dir) - } else if let Some(data_dir) = etcetera::base_strategy::choose_base_strategy() - .ok() - .map(|dirs| dirs.data_dir().join("uv")) - { + } else if let Some(data_dir) = uv_dirs::user_state_dir() { StateStore::from_path(data_dir) } else { StateStore::from_path(".uv") diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index f74b29fc2..b82791800 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] uv-cache = { workspace = true } +uv-dirs = { workspace = true } uv-fs = { workspace = true } uv-install-wheel = { workspace = true } uv-installer = { workspace = true } @@ -28,8 +29,6 @@ uv-settings = { workspace = true } uv-state = { workspace = true } uv-static = { workspace = true } uv-virtualenv = { workspace = true } - -dirs-sys = { workspace = true } fs-err = { workspace = true } pathdiff = { workspace = true } serde = { workspace = true } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 20b3e559f..631d6c026 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -2,6 +2,7 @@ use core::fmt; use fs_err as fs; +use uv_dirs::user_executable_directory; use uv_pep440::Version; use uv_pep508::{InvalidNameError, PackageName}; @@ -354,36 +355,9 @@ impl fmt::Display for InstalledTool { } } -/// Find a directory to place executables in. -/// -/// This follows, in order: -/// -/// - `$UV_TOOL_BIN_DIR` -/// - `$XDG_BIN_HOME` -/// - `$XDG_DATA_HOME/../bin` -/// - `$HOME/.local/bin` -/// -/// On all platforms. -/// -/// Errors if a directory cannot be found. -pub fn find_executable_directory() -> Result { - std::env::var_os(EnvVars::UV_TOOL_BIN_DIR) - .and_then(dirs_sys::is_absolute_path) - .or_else(|| std::env::var_os(EnvVars::XDG_BIN_HOME).and_then(dirs_sys::is_absolute_path)) - .or_else(|| { - std::env::var_os(EnvVars::XDG_DATA_HOME) - .and_then(dirs_sys::is_absolute_path) - .map(|path| path.join("../bin")) - }) - .or_else(|| { - // See https://github.com/dirs-dev/dirs-rs/blob/50b50f31f3363b7656e5e63b3fa1060217cbc844/src/win.rs#L5C58-L5C78 - #[cfg(windows)] - let home_dir = dirs_sys::known_folder_profile(); - #[cfg(not(windows))] - let home_dir = dirs_sys::home_dir(); - home_dir.map(|path| path.join(".local").join("bin")) - }) - .ok_or(Error::NoExecutableDirectory) +/// Find the tool executable directory. +pub fn tool_executable_dir() -> Result { + user_executable_directory(Some(EnvVars::UV_TOOL_BIN_DIR)).ok_or(Error::NoExecutableDirectory) } /// Find the `.dist-info` directory for a package in an environment. diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 82ce665a4..08fae7fa6 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -16,7 +16,7 @@ use uv_pypi_types::Requirement; use uv_python::PythonEnvironment; use uv_settings::ToolOptions; use uv_shell::Shell; -use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint}; +use uv_tool::{entrypoint_paths, tool_executable_dir, InstalledTools, Tool, ToolEntrypoint}; use uv_warnings::warn_user; use crate::commands::ExitStatus; @@ -79,7 +79,7 @@ pub(crate) fn install_executables( }; // Find a suitable path to install into - let executable_directory = find_executable_directory()?; + let executable_directory = tool_executable_dir()?; fs_err::create_dir_all(&executable_directory) .context("Failed to create executable directory")?; diff --git a/crates/uv/src/commands/tool/dir.rs b/crates/uv/src/commands/tool/dir.rs index 880a8eee3..e8b1a6a40 100644 --- a/crates/uv/src/commands/tool/dir.rs +++ b/crates/uv/src/commands/tool/dir.rs @@ -4,12 +4,12 @@ use owo_colors::OwoColorize; use uv_configuration::PreviewMode; use uv_fs::Simplified; -use uv_tool::{find_executable_directory, InstalledTools}; +use uv_tool::{tool_executable_dir, InstalledTools}; /// Show the tool directory. pub(crate) fn dir(bin: bool, _preview: PreviewMode) -> anyhow::Result<()> { if bin { - let executable_directory = find_executable_directory()?; + let executable_directory = tool_executable_dir()?; println!("{}", executable_directory.simplified_display().cyan()); } else { let installed_tools = diff --git a/crates/uv/src/commands/tool/update_shell.rs b/crates/uv/src/commands/tool/update_shell.rs index 58637c422..9a6191761 100644 --- a/crates/uv/src/commands/tool/update_shell.rs +++ b/crates/uv/src/commands/tool/update_shell.rs @@ -9,14 +9,14 @@ use tracing::debug; use uv_fs::Simplified; use uv_shell::Shell; -use uv_tool::find_executable_directory; +use uv_tool::tool_executable_dir; use crate::commands::ExitStatus; use crate::printer::Printer; /// Ensure that the executable directory is in PATH. pub(crate) async fn update_shell(printer: Printer) -> Result { - let executable_directory = find_executable_directory()?; + let executable_directory = tool_executable_dir()?; debug!( "Ensuring that the executable directory is in PATH: {}", executable_directory.simplified_display()