diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index db2f1eddb..72cba19a9 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2133,7 +2133,12 @@ pub struct ToolListArgs; #[allow(clippy::struct_excessive_bools)] pub struct ToolUninstallArgs { /// The name of the tool to uninstall. - pub name: PackageName, + #[arg(required = true)] + pub name: Option, + + /// Uninstall all tools. + #[arg(long, conflicts_with("name"))] + pub all: bool, } #[derive(Args)] diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index a2fc1a675..69049070a 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -86,7 +86,7 @@ impl InstalledTools { } /// Return the metadata for all installed tools. - pub fn tools(&self) -> Result, Error> { + pub fn tools(&self) -> Result, Error> { let _lock = self.acquire_lock(); let mut tools = Vec::new(); for directory in uv_fs::directories(self.root()) { @@ -102,6 +102,7 @@ impl InstalledTools { }; let tool_receipt = ToolReceipt::from_string(contents) .map_err(|err| Error::ReceiptRead(path, Box::new(err)))?; + let name = PackageName::from_str(&name)?; tools.push((name, tool_receipt.tool)); } Ok(tools) @@ -256,16 +257,15 @@ impl InstalledTools { )) } - pub fn version(&self, name: &str, cache: &Cache) -> Result { - let environment_path = self.root.join(name); - let package_name = PackageName::from_str(name)?; + pub fn version(&self, name: &PackageName, cache: &Cache) -> Result { + let environment_path = self.root.join(name.to_string()); let environment = PythonEnvironment::from_root(&environment_path, cache)?; let site_packages = SitePackages::from_environment(&environment) .map_err(|err| Error::EnvironmentRead(environment_path.clone(), err.to_string()))?; - let packages = site_packages.get_packages(&package_name); + let packages = site_packages.get_packages(name); let package = packages .first() - .ok_or_else(|| Error::MissingToolPackage(package_name, environment_path))?; + .ok_or_else(|| Error::MissingToolPackage(name.clone(), environment_path))?; Ok(package.version().clone()) } diff --git a/crates/uv/src/commands/tool/uninstall.rs b/crates/uv/src/commands/tool/uninstall.rs index a23b8b06e..e77a51850 100644 --- a/crates/uv/src/commands/tool/uninstall.rs +++ b/crates/uv/src/commands/tool/uninstall.rs @@ -8,7 +8,7 @@ use tracing::debug; use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_normalize::PackageName; -use uv_tool::InstalledTools; +use uv_tool::{InstalledTools, Tool, ToolEntrypoint}; use uv_warnings::warn_user_once; use crate::commands::ExitStatus; @@ -16,7 +16,7 @@ use crate::printer::Printer; /// Uninstall a tool. pub(crate) async fn uninstall( - name: PackageName, + name: Option, preview: PreviewMode, printer: Printer, ) -> Result { @@ -25,27 +25,64 @@ pub(crate) async fn uninstall( } let installed_tools = InstalledTools::from_settings()?; - let Some(receipt) = installed_tools.get_tool_receipt(&name)? else { - // If the tool is not installed, attempt to remove the environment anyway. - match installed_tools.remove_environment(&name) { - Ok(()) => { - writeln!( - printer.stderr(), - "Removed dangling environment for `{name}`" - )?; - return Ok(ExitStatus::Success); - } - Err(uv_tool::Error::IO(err)) if err.kind() == std::io::ErrorKind::NotFound => { - bail!("`{name}` is not installed"); - } - Err(err) => { - return Err(err.into()); - } - } - }; + let mut entrypoints = if let Some(name) = name { + let Some(receipt) = installed_tools.get_tool_receipt(&name)? else { + // If the tool is not installed, attempt to remove the environment anyway. + match installed_tools.remove_environment(&name) { + Ok(()) => { + writeln!( + printer.stderr(), + "Removed dangling environment for `{name}`" + )?; + return Ok(ExitStatus::Success); + } + Err(uv_tool::Error::IO(err)) if err.kind() == std::io::ErrorKind::NotFound => { + bail!("`{name}` is not installed"); + } + Err(err) => { + return Err(err.into()); + } + } + }; + + uninstall_tool(&name, &receipt, &installed_tools).await? + } else { + let mut entrypoints = vec![]; + for (name, receipt) in installed_tools.tools()? { + entrypoints.extend(uninstall_tool(&name, &receipt, &installed_tools).await?); + } + entrypoints + }; + entrypoints.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + + if entrypoints.is_empty() { + writeln!(printer.stderr(), "Nothing to uninstall")?; + return Ok(ExitStatus::Success); + } + + let s = if entrypoints.len() == 1 { "" } else { "s" }; + writeln!( + printer.stderr(), + "Uninstalled {} executable{s}: {}", + entrypoints.len(), + entrypoints + .iter() + .map(|entrypoint| entrypoint.name.bold()) + .join(", ") + )?; + + Ok(ExitStatus::Success) +} + +/// Uninstall a tool. +async fn uninstall_tool( + name: &PackageName, + receipt: &Tool, + tools: &InstalledTools, +) -> Result> { // Remove the tool itself. - installed_tools.remove_environment(&name)?; + tools.remove_environment(name)?; // Remove the tool's entrypoints. let entrypoints = receipt.entrypoints(); @@ -68,16 +105,5 @@ pub(crate) async fn uninstall( } } - let s = if entrypoints.len() == 1 { "" } else { "s" }; - writeln!( - printer.stderr(), - "Uninstalled {} executable{s}: {}", - entrypoints.len(), - entrypoints - .iter() - .map(|entrypoint| entrypoint.name.bold()) - .join(", ") - )?; - - Ok(ExitStatus::Success) + Ok(entrypoints.to_vec()) } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 476d21feb..5e028794a 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -300,16 +300,18 @@ impl ToolListSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolUninstallSettings { - pub(crate) name: PackageName, + pub(crate) name: Option, } impl ToolUninstallSettings { /// Resolve the [`ToolUninstallSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: ToolUninstallArgs, _filesystem: Option) -> Self { - let ToolUninstallArgs { name } = args; + let ToolUninstallArgs { name, all } = args; - Self { name } + Self { + name: name.filter(|_| !all), + } } }