mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 21:02:37 +00:00
Migrate to XDG and Linux strategy for macOS directories (#5806)
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 https://github.com/astral-sh/uv/issues/4411. --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
04e3e7ce65
commit
e11bbb539a
9 changed files with 136 additions and 17 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -1185,6 +1185,17 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"home",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
|
|
@ -4624,6 +4635,7 @@ dependencies = [
|
|||
"clap",
|
||||
"directories",
|
||||
"distribution-types",
|
||||
"etcetera",
|
||||
"fs-err",
|
||||
"nanoid",
|
||||
"pypi-types",
|
||||
|
|
@ -5184,6 +5196,7 @@ name = "uv-state"
|
|||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"etcetera",
|
||||
"fs-err",
|
||||
"tempfile",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
directories = { workspace = true }
|
||||
etcetera = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::{
|
|||
};
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use etcetera::BaseStrategy;
|
||||
use fs_err as fs;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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");
|
||||
|
|
@ -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()? {
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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" };
|
||||
|
|
@ -97,7 +119,7 @@ pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Re
|
|||
.join(", ")
|
||||
)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uninstall a tool.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue