Skip to content

Commit

Permalink
Migrate to XDG and Linux strategy for macOS directories (#5806)
Browse files Browse the repository at this point in the history
This PR moves us to the Linux strategy for our global directories on
macOS. We both feel on the team _and_ have received feedback (in Issues
and Polls) that the `Application Support` directories are more intended
for GUIs, and CLI tools are correct to respect the XDG variables and use
the same directory paths on Linux and macOS.

Namely, we now use:

- `/Users/crmarsh/.local/share/uv/tools` (for tools)
- `/Users/crmarsh/.local/share/uv/python` (for Pythons)
- `/Users/crmarsh/.cache/uv` (for the cache)

The strategy is such that if the `/Users/crmarsh/Library/Application
Support/uv` already exists, we keep using it -- same goes for
`/Users/crmarsh/Library/Caches/uv`, so **it's entirely backwards
compatible**.

If you want to force a migration to the new schema, you can run:

- `uv cache clean`
- `uv tool uninstall --all`
- `uv python uninstall --all`

Which will clean up the macOS-specific directories, paving the way for
the above paths. In other words, once you run those commands, subsequent
`uv` operations will automatically use the `~/.cache` and `~/.local`
variants.

Closes #4411.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
  • Loading branch information
charliermarsh and zanieb committed Aug 19, 2024
1 parent c4e2eb8 commit 1810454
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 17 deletions.
13 changes: 13 additions & 0 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 @@ -84,6 +84,7 @@ dirs-sys = { version = "0.4.1" }
dunce = { version = "1.0.4" }
either = { version = "1.12.0" }
encoding_rs_io = { version = "0.1.7" }
etcetera = { version = "0.8.0" }
flate2 = { version = "1.0.28", default-features = false }
fs-err = { version = "2.11.0" }
fs2 = { version = "0.4.3" }
Expand Down
3 changes: 2 additions & 1 deletion crates/uv-cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ uv-normalize = { 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 }
rustc-hash = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
walkdir = { workspace = true }
rmp-serde = { workspace = true }
25 changes: 18 additions & 7 deletions crates/uv-cache/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::io;
use std::path::PathBuf;

use crate::Cache;
use clap::Parser;
use directories::ProjectDirs;

use crate::Cache;
use etcetera::BaseStrategy;

#[derive(Parser, Debug, Clone)]
#[command(next_help_heading = "Cache options")]
Expand Down Expand Up @@ -40,13 +40,24 @@ impl Cache {
/// Returns an absolute cache dir.
pub fn from_settings(no_cache: bool, cache_dir: Option<PathBuf>) -> Result<Self, io::Error> {
if no_cache {
Cache::temp()
Self::temp()
} else if let Some(cache_dir) = cache_dir {
Ok(Cache::from_path(cache_dir))
} else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") {
Ok(Cache::from_path(project_dirs.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())
{
// 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"))
{
Ok(Self::from_path(cache_dir))
} else {
Ok(Cache::from_path(".uv_cache"))
Ok(Self::from_path(".uv_cache"))
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions crates/uv-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io:
}
}

#[cfg(unix)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
fs_err::remove_file(path.as_ref())
}

#[cfg(windows)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
match junction::delete(dunce::simplified(path.as_ref())) {
Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
}
}

/// Return a [`NamedTempFile`] in the specified directory.
///
/// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default.
Expand Down Expand Up @@ -283,6 +301,14 @@ pub fn files(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
.map(|entry| entry.path())
}

/// Returns `true` if a path is a temporary file or directory.
pub fn is_temporary(path: impl AsRef<Path>) -> bool {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.map_or(false, |name| name.starts_with(".tmp"))
}

/// A file lock that is automatically released when dropped.
#[derive(Debug)]
pub struct LockedFile(fs_err::File);
Expand Down
1 change: 1 addition & 0 deletions crates/uv-state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ workspace = true

[dependencies]
directories = { workspace = true }
etcetera = { workspace = true }
tempfile = { workspace = true }
fs-err = { workspace = true }
16 changes: 14 additions & 2 deletions crates/uv-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
};

use directories::ProjectDirs;
use etcetera::BaseStrategy;
use fs_err as fs;
use tempfile::{tempdir, TempDir};

Expand Down Expand Up @@ -84,8 +85,19 @@ 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(project_dirs) = ProjectDirs::from("", "", "uv") {
StateStore::from_path(project_dirs.data_dir())
} else if let Some(data_dir) = ProjectDirs::from("", "", "uv")
.map(|dirs| dirs.data_dir().to_path_buf())
.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"))
{
StateStore::from_path(data_dir)
} else {
StateStore::from_path(".uv")
}
Expand Down
36 changes: 34 additions & 2 deletions crates/uv/src/commands/python/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,42 @@ pub(crate) async fn uninstall(

printer: Printer,
) -> Result<ExitStatus> {
let start = std::time::Instant::now();

let installations = ManagedPythonInstallations::from_settings()?.init()?;
let _lock = installations.acquire_lock()?;

// Perform the uninstallation.
do_uninstall(&installations, targets, all, printer).await?;

// Clean up any empty directories.
if uv_fs::directories(installations.root()).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(&installations.root()).await?;

if let Some(top_level) = installations.root().parent() {
// Remove the `toolchains` symlink.
match uv_fs::remove_symlink(top_level.join("toolchains")) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}

if uv_fs::directories(top_level).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(top_level).await?;
}
}
}

Ok(ExitStatus::Success)
}

/// Perform the uninstallation of managed Python installations.
async fn do_uninstall(
installations: &ManagedPythonInstallations,
targets: Vec<String>,
all: bool,
printer: Printer,
) -> Result<ExitStatus> {
let start = std::time::Instant::now();

let requests = if all {
vec![PythonRequest::Any]
} else {
Expand Down Expand Up @@ -108,6 +139,7 @@ pub(crate) async fn uninstall(
}
}

// Report on any uninstalled installations.
if !uninstalled.is_empty() {
if let [uninstalled] = uninstalled.as_slice() {
// Ex) "Uninstalled Python 3.9.7 in 1.68s"
Expand Down
32 changes: 27 additions & 5 deletions crates/uv/src/commands/tool/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
Err(err) => return Err(err.into()),
};

// Perform the uninstallation.
do_uninstall(&installed_tools, name, printer).await?;

// Clean up any empty directories.
if uv_fs::directories(installed_tools.root()).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(&installed_tools.root()).await?;
if let Some(top_level) = installed_tools.root().parent() {
if uv_fs::directories(top_level).all(|path| uv_fs::is_temporary(&path)) {
fs_err::tokio::remove_dir_all(top_level).await?;
}
}
}

Ok(ExitStatus::Success)
}

/// Perform the uninstallation.
async fn do_uninstall(
installed_tools: &InstalledTools,
name: Option<PackageName>,
printer: Printer,
) -> Result<()> {
let mut dangling = false;
let mut entrypoints = if let Some(name) = name {
let Some(receipt) = installed_tools.get_tool_receipt(&name)? else {
Expand All @@ -37,7 +59,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
printer.stderr(),
"Removed dangling environment for `{name}`"
)?;
return Ok(ExitStatus::Success);
return Ok(());
}
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("`{name}` is not installed");
Expand All @@ -48,7 +70,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
}
};

uninstall_tool(&name, &receipt, &installed_tools).await?
uninstall_tool(&name, &receipt, installed_tools).await?
} else {
let mut entrypoints = vec![];
for (name, receipt) in installed_tools.tools()? {
Expand All @@ -72,7 +94,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
}
};

entrypoints.extend(uninstall_tool(&name, &receipt, &installed_tools).await?);
entrypoints.extend(uninstall_tool(&name, &receipt, installed_tools).await?);
}
entrypoints
};
Expand All @@ -83,7 +105,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
if !dangling {
writeln!(printer.stderr(), "Nothing to uninstall")?;
}
return Ok(ExitStatus::Success);
return Ok(());
}

let s = if entrypoints.len() == 1 { "" } else { "s" };
Expand All @@ -97,7 +119,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
.join(", ")
)?;

Ok(ExitStatus::Success)
Ok(())
}

/// Uninstall a tool.
Expand Down

0 comments on commit 1810454

Please sign in to comment.