Files
uv/crates/uv/src/commands/python/uninstall.rs
T
Charlie Marsh e11bbb539a 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>
2024-08-20 11:31:46 -05:00

204 lines
6.4 KiB
Rust

use std::collections::BTreeSet;
use std::fmt::Write;
use anyhow::Result;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use itertools::Itertools;
use owo_colors::OwoColorize;
use uv_python::downloads::PythonDownloadRequest;
use uv_python::managed::ManagedPythonInstallations;
use uv_python::PythonRequest;
use crate::commands::python::{ChangeEvent, ChangeEventKind};
use crate::commands::{elapsed, ExitStatus};
use crate::printer::Printer;
/// Uninstall managed Python versions.
pub(crate) async fn uninstall(
targets: Vec<String>,
all: bool,
printer: Printer,
) -> Result<ExitStatus> {
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 {
let targets = targets.into_iter().collect::<BTreeSet<_>>();
targets
.iter()
.map(|target| PythonRequest::parse(target.as_str()))
.collect::<Vec<_>>()
};
let download_requests = requests
.iter()
.map(|request| {
PythonDownloadRequest::from_request(request).ok_or_else(|| {
anyhow::anyhow!("Cannot uninstall managed Python for request: {request}")
})
})
.collect::<Result<Vec<_>>>()?;
let installed_installations: Vec<_> = installations.find_all()?.collect();
let mut matching_installations = BTreeSet::default();
for (request, download_request) in requests.iter().zip(download_requests) {
if matches!(requests.as_slice(), [PythonRequest::Any]) {
writeln!(printer.stderr(), "Searching for Python installations")?;
} else {
writeln!(
printer.stderr(),
"Searching for Python versions matching: {}",
request.cyan()
)?;
}
let mut found = false;
for installation in installed_installations
.iter()
.filter(|installation| download_request.satisfied_by_key(installation.key()))
{
found = true;
matching_installations.insert(installation.clone());
}
if !found {
if matches!(requests.as_slice(), [PythonRequest::Any]) {
writeln!(printer.stderr(), "No Python installations found")?;
return Ok(ExitStatus::Failure);
}
writeln!(
printer.stderr(),
"No existing installations found for: {}",
request.cyan()
)?;
}
}
if matching_installations.is_empty() {
writeln!(
printer.stderr(),
"No Python installations found matching the requests"
)?;
return Ok(ExitStatus::Failure);
}
let mut tasks = FuturesUnordered::new();
for installation in &matching_installations {
tasks.push(async {
(
installation.key(),
fs_err::tokio::remove_dir_all(installation.path()).await,
)
});
}
let mut uninstalled = vec![];
let mut errors = vec![];
while let Some((key, result)) = tasks.next().await {
if let Err(err) = result {
errors.push((key.clone(), err));
} else {
uninstalled.push(key.clone());
}
}
// Report on any uninstalled installations.
if !uninstalled.is_empty() {
if let [uninstalled] = uninstalled.as_slice() {
// Ex) "Uninstalled Python 3.9.7 in 1.68s"
writeln!(
printer.stderr(),
"{}",
format!(
"Uninstalled {} {}",
format!("Python {}", uninstalled.version()).bold(),
format!("in {}", elapsed(start.elapsed())).dimmed()
)
.dimmed()
)?;
} else {
// Ex) "Uninstalled 2 versions in 1.68s"
let s = if uninstalled.len() == 1 { "" } else { "s" };
writeln!(
printer.stderr(),
"{}",
format!(
"Uninstalled {} {}",
format!("{} version{s}", uninstalled.len()).bold(),
format!("in {}", elapsed(start.elapsed())).dimmed()
)
.dimmed()
)?;
}
for event in uninstalled
.into_iter()
.map(|key| ChangeEvent {
key,
kind: ChangeEventKind::Removed,
})
.sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind)))
{
match event.kind {
ChangeEventKind::Added => {
writeln!(printer.stderr(), " {} {}", "+".green(), event.key.bold())?;
}
ChangeEventKind::Removed => {
writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?;
}
}
}
}
if !errors.is_empty() {
for (key, err) in errors {
writeln!(
printer.stderr(),
"Failed to uninstall {}: {}",
key.green(),
err
)?;
}
return Ok(ExitStatus::Failure);
}
Ok(ExitStatus::Success)
}