Skip to content

Commit

Permalink
Add uv-dirs to consolidate directory lookup methods (#8453)
Browse files Browse the repository at this point in the history
I need the executable directory outside `uv-tool` and figured I should
consolidate these to a central location.
  • Loading branch information
zanieb authored Oct 22, 2024
1 parent dc32b26 commit 2f6fa08
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 67 deletions.
18 changes: 13 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
3 changes: 1 addition & 2 deletions crates/uv-cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand Down
11 changes: 2 additions & 9 deletions crates/uv-cache/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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`.
Expand Down
24 changes: 24 additions & 0 deletions crates/uv-dirs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
72 changes: 72 additions & 0 deletions crates/uv-dirs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
directories::ProjectDirs::from("", "", "uv").map(|dirs| dirs.data_dir().to_path_buf())
}
3 changes: 1 addition & 2 deletions crates/uv-state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
15 changes: 4 additions & 11 deletions crates/uv-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -85,18 +84,12 @@ impl StateStore {
pub fn from_settings(state_dir: Option<PathBuf>) -> Result<Self, io::Error> {
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")
Expand Down
3 changes: 1 addition & 2 deletions crates/uv-tool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand Down
34 changes: 4 additions & 30 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<PathBuf, Error> {
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<PathBuf, Error> {
user_executable_directory(Some(EnvVars::UV_TOOL_BIN_DIR)).ok_or(Error::NoExecutableDirectory)
}

/// Find the `.dist-info` directory for a package in an environment.
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/tool/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")?;

Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/tool/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/tool/update_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExitStatus> {
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()
Expand Down

0 comments on commit 2f6fa08

Please sign in to comment.