diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 70a31aaa8527..7bd67d48ab73 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -490,7 +490,7 @@ pub(crate) async fn install( } for event in changelog.events() { - let executables = format_executables(&event, &changelog); + let executables = format_executables(&event, &changelog.installed_executables); match event.kind { ChangeEventKind::Added => { writeln!( @@ -554,8 +554,11 @@ pub(crate) async fn install( Ok(ExitStatus::Success) } -fn format_executables(event: &ChangeEvent, changelog: &Changelog) -> String { - let Some(installed) = changelog.installed_executables.get(&event.key) else { +pub(crate) fn format_executables( + event: &ChangeEvent, + executables: &FxHashMap>, +) -> String { + let Some(installed) = executables.get(&event.key) else { return String::new(); }; diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index cfcca6fc9bfe..e02dfc73bb4b 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; use std::fmt::Write; +use std::path::PathBuf; use anyhow::Result; use futures::stream::FuturesUnordered; @@ -7,12 +8,14 @@ use futures::StreamExt; use itertools::Itertools; use owo_colors::OwoColorize; +use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, warn}; use uv_fs::Simplified; use uv_python::downloads::PythonDownloadRequest; use uv_python::managed::{python_executable_dir, ManagedPythonInstallations}; -use uv_python::PythonRequest; +use uv_python::{PythonInstallationKey, PythonRequest}; +use crate::commands::python::install::format_executables; use crate::commands::python::{ChangeEvent, ChangeEventKind}; use crate::commands::{elapsed, ExitStatus}; use crate::printer::Printer; @@ -123,8 +126,10 @@ async fn do_uninstall( return Ok(ExitStatus::Failure); } - // Collect files in a directory - let executables = python_executable_dir()? + // Find and remove all relevant Python executables + let mut uninstalled_executables: FxHashMap> = + FxHashMap::default(); + for executable in python_executable_dir()? .read_dir() .into_iter() .flatten() @@ -148,17 +153,25 @@ async fn do_uninstall( || name == Some(&installation.key().executable_name()) }) }) - // Only include Python executables that match the installations - .filter(|path| { - matching_installations - .iter() - .any(|installation| installation.is_bin_link(path.as_path())) - }) - .collect::>(); + .sorted() + { + let Some(installation) = matching_installations + .iter() + .find(|installation| installation.is_bin_link(executable.as_path())) + else { + continue; + }; - for executable in &executables { - fs_err::remove_file(executable)?; - debug!("Removed {}", executable.user_display()); + fs_err::remove_file(&executable)?; + debug!( + "Removed `{}` for `{}`", + executable.simplified_display(), + installation.key() + ); + uninstalled_executables + .entry(installation.key().clone()) + .or_default() + .insert(executable); } let mut tasks = FuturesUnordered::new(); @@ -218,15 +231,15 @@ async fn do_uninstall( }) .sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind))) { + let executables = format_executables(&event, &uninstalled_executables); match event.kind { - // TODO(zanieb): Track removed executables and report them all here ChangeEventKind::Removed => { writeln!( printer.stderr(), - " {} {} ({})", + " {} {}{}", "-".red(), event.key.bold(), - event.key.executable_name_minor() + executables, )?; } _ => unreachable!(), diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index a8f52f4629f2..2a4d3886eacf 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -87,7 +87,7 @@ fn python_install() { ----- stderr ----- Searching for Python versions matching: Python 3.13 Uninstalled Python 3.13.0 in [TIME] - - cpython-3.13.0-[PLATFORM] (python3.13) + - cpython-3.13.0-[PLATFORM] "###); } @@ -222,7 +222,7 @@ fn python_install_preview() { ----- stderr ----- Searching for Python versions matching: Python 3.13 Uninstalled Python 3.13.0 in [TIME] - - cpython-3.13.0-[PLATFORM] (python3.13) + - cpython-3.13.0-[PLATFORM] (python, python3, python3.13) "###); // The executable should be removed @@ -437,7 +437,7 @@ fn python_install_freethreaded() { ----- stderr ----- Searching for Python installations Uninstalled 2 versions in [TIME] - - cpython-3.13.0-[PLATFORM] (python3.13) + - cpython-3.13.0-[PLATFORM] - cpython-3.13.0+freethreaded-[PLATFORM] (python3.13t) "###); } @@ -551,7 +551,7 @@ fn python_install_default() { ----- stderr ----- Searching for Python installations Uninstalled Python 3.13.0 in [TIME] - - cpython-3.13.0-[PLATFORM] (python3.13) + - cpython-3.13.0-[PLATFORM] (python, python3, python3.13) "###); // The executables should be removed @@ -584,7 +584,7 @@ fn python_install_default() { ----- stderr ----- Searching for Python versions matching: Python 3.13 Uninstalled Python 3.13.0 in [TIME] - - cpython-3.13.0-[PLATFORM] (python3.13) + - cpython-3.13.0-[PLATFORM] (python, python3, python3.13) "###); // We should remove all the executables