diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 109598f0d..3c416997d 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2022,15 +2022,18 @@ pub enum ToolchainCommand { /// List the available toolchains. List(ToolchainListArgs), - /// Download and install a specific toolchain. + /// Download and install toolchains. Install(ToolchainInstallArgs), - /// Search for a toolchain + /// Search for a toolchain. #[command(disable_version_flag = true)] Find(ToolchainFindArgs), /// Show the toolchains directory. Dir, + + /// Uninstall toolchains. + Uninstall(ToolchainUninstallArgs), } #[derive(Args)] @@ -2064,6 +2067,13 @@ pub struct ToolchainInstallArgs { pub force: bool, } +#[derive(Args)] +#[allow(clippy::struct_excessive_bools)] +pub struct ToolchainUninstallArgs { + /// The toolchains to uninstall. + pub targets: Vec, +} + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct ToolchainFindArgs { diff --git a/crates/uv-toolchain/src/implementation.rs b/crates/uv-toolchain/src/implementation.rs index ee47d7047..4cfd1ef9e 100644 --- a/crates/uv-toolchain/src/implementation.rs +++ b/crates/uv-toolchain/src/implementation.rs @@ -17,7 +17,7 @@ pub enum ImplementationName { PyPy, } -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd)] pub enum LenientImplementationName { Known(ImplementationName), Unknown(String), diff --git a/crates/uv-toolchain/src/managed.rs b/crates/uv-toolchain/src/managed.rs index 58363e1ae..e245a319e 100644 --- a/crates/uv-toolchain/src/managed.rs +++ b/crates/uv-toolchain/src/managed.rs @@ -204,7 +204,7 @@ Error=This toolchain is managed by uv and should not be modified. "; /// A uv-managed Python toolchain installed on the current system.. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct InstalledToolchain { /// The path to the top-level directory of the installed toolchain. path: PathBuf, diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 9e454b47c..6a5b2f5ab 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -33,6 +33,7 @@ pub(crate) use toolchain::dir::dir as toolchain_dir; pub(crate) use toolchain::find::find as toolchain_find; pub(crate) use toolchain::install::install as toolchain_install; pub(crate) use toolchain::list::list as toolchain_list; +pub(crate) use toolchain::uninstall::uninstall as toolchain_uninstall; use uv_cache::Cache; use uv_fs::Simplified; use uv_installer::compile_tree; diff --git a/crates/uv/src/commands/toolchain/install.rs b/crates/uv/src/commands/toolchain/install.rs index 371835ee8..6631445a7 100644 --- a/crates/uv/src/commands/toolchain/install.rs +++ b/crates/uv/src/commands/toolchain/install.rs @@ -63,7 +63,7 @@ pub(crate) async fn install( { writeln!( printer.stderr(), - "Found installed toolchain '{}' that satisfies {request}", + "Found installed toolchain `{}` that satisfies {request}", toolchain.key() )?; if force { diff --git a/crates/uv/src/commands/toolchain/mod.rs b/crates/uv/src/commands/toolchain/mod.rs index 370274394..06cda22fb 100644 --- a/crates/uv/src/commands/toolchain/mod.rs +++ b/crates/uv/src/commands/toolchain/mod.rs @@ -2,3 +2,4 @@ pub(crate) mod dir; pub(crate) mod find; pub(crate) mod install; pub(crate) mod list; +pub(crate) mod uninstall; diff --git a/crates/uv/src/commands/toolchain/uninstall.rs b/crates/uv/src/commands/toolchain/uninstall.rs new file mode 100644 index 000000000..cf6633209 --- /dev/null +++ b/crates/uv/src/commands/toolchain/uninstall.rs @@ -0,0 +1,120 @@ +use anyhow::Result; +use futures::StreamExt; +use itertools::Itertools; +use std::collections::BTreeSet; +use std::fmt::Write; +use uv_configuration::PreviewMode; +use uv_toolchain::downloads::{self, PythonDownloadRequest}; +use uv_toolchain::managed::InstalledToolchains; +use uv_toolchain::ToolchainRequest; +use uv_warnings::warn_user_once; + +use crate::commands::ExitStatus; +use crate::printer::Printer; + +/// Uninstall Python toolchains. +pub(crate) async fn uninstall( + targets: Vec, + preview: PreviewMode, + printer: Printer, +) -> Result { + if preview.is_disabled() { + warn_user_once!("`uv toolchain uninstall` is experimental and may change without warning."); + } + + let toolchains = InstalledToolchains::from_settings()?.init()?; + + let requests = targets + .iter() + .map(|target| ToolchainRequest::parse(target.as_str())) + .collect::>(); + + let download_requests = requests + .iter() + .map(PythonDownloadRequest::from_request) + .collect::, downloads::Error>>()?; + + let installed_toolchains: Vec<_> = toolchains.find_all()?.collect(); + let mut matching_toolchains = BTreeSet::default(); + for (request, download_request) in requests.iter().zip(download_requests) { + writeln!( + printer.stderr(), + "Looking for installed toolchains matching {request} ({download_request})" + )?; + let mut found = false; + for toolchain in installed_toolchains + .iter() + .filter(|toolchain| download_request.satisfied_by_key(toolchain.key())) + { + found = true; + if matching_toolchains.insert(toolchain.clone()) { + writeln!( + printer.stderr(), + "Found toolchain `{}` that matches {request}", + toolchain.key() + )?; + } + } + if !found { + writeln!(printer.stderr(), "No toolchains found matching {request}")?; + } + } + + if matching_toolchains.is_empty() { + if matches!(requests.as_slice(), [ToolchainRequest::Any]) { + writeln!(printer.stderr(), "No installed toolchains found")?; + } else if requests.len() > 1 { + writeln!( + printer.stderr(), + "No toolchains found matching the requests" + )?; + } else { + writeln!(printer.stderr(), "No toolchains found matching the request")?; + } + return Ok(ExitStatus::Failure); + } + + let tasks = futures::stream::iter(matching_toolchains.iter()) + .map(|toolchain| async { + ( + toolchain.key(), + fs_err::tokio::remove_dir_all(toolchain.path()).await, + ) + }) + .buffered(4); + + let results = tasks.collect::>().await; + let mut failed = false; + for (key, result) in results.iter().sorted_by_key(|(key, _)| key) { + if let Err(err) = result { + failed = true; + writeln!( + printer.stderr(), + "Failed to uninstall toolchain `{key}`: {err}" + )?; + } else { + writeln!(printer.stderr(), "Uninstalled toolchain `{key}`")?; + } + } + + if failed { + if matching_toolchains.len() > 1 { + writeln!(printer.stderr(), "Uninstall of some toolchains failed")?; + } + return Ok(ExitStatus::Failure); + } + + let s = if matching_toolchains.len() == 1 { + "" + } else { + "s" + }; + + writeln!( + printer.stderr(), + "Uninstalled {} toolchain{s}", + matching_toolchains.len() + )?; + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 985b3a9d1..6eace57eb 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -906,6 +906,15 @@ async fn run() -> Result { ) .await } + Commands::Toolchain(ToolchainNamespace { + command: ToolchainCommand::Uninstall(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::ToolchainUninstallSettings::resolve(args, filesystem); + show_settings!(args); + + commands::toolchain_uninstall(args.targets, globals.preview, printer).await + } Commands::Toolchain(ToolchainNamespace { command: ToolchainCommand::Find(args), }) => { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 5aafb30b9..83daa302c 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -15,7 +15,7 @@ use uv_cli::{ PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, ToolchainFindArgs, ToolchainInstallArgs, - ToolchainListArgs, VenvArgs, + ToolchainListArgs, ToolchainUninstallArgs, VenvArgs, }; use uv_client::Connectivity; use uv_configuration::{ @@ -374,6 +374,26 @@ impl ToolchainInstallSettings { } } +/// The resolved settings to use for a `toolchain uninstall` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct ToolchainUninstallSettings { + pub(crate) targets: Vec, +} + +impl ToolchainUninstallSettings { + /// Resolve the [`ToolchainUninstallSettings`] from the CLI and filesystem configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve( + args: ToolchainUninstallArgs, + _filesystem: Option, + ) -> Self { + let ToolchainUninstallArgs { targets } = args; + + Self { targets } + } +} + /// The resolved settings to use for a `toolchain find` invocation. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)]