Enable --all to uninstall all managed tools (#4937)

## Summary

Like #4932 but for tools.
This commit is contained in:
Charlie Marsh 2024-07-09 12:26:17 -07:00 committed by GitHub
parent bb703b8343
commit 55e1a7e011
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 76 additions and 43 deletions

View file

@ -2133,7 +2133,12 @@ pub struct ToolListArgs;
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct ToolUninstallArgs { pub struct ToolUninstallArgs {
/// The name of the tool to uninstall. /// The name of the tool to uninstall.
pub name: PackageName, #[arg(required = true)]
pub name: Option<PackageName>,
/// Uninstall all tools.
#[arg(long, conflicts_with("name"))]
pub all: bool,
} }
#[derive(Args)] #[derive(Args)]

View file

@ -86,7 +86,7 @@ impl InstalledTools {
} }
/// Return the metadata for all installed tools. /// Return the metadata for all installed tools.
pub fn tools(&self) -> Result<Vec<(String, Tool)>, Error> { pub fn tools(&self) -> Result<Vec<(PackageName, Tool)>, Error> {
let _lock = self.acquire_lock(); let _lock = self.acquire_lock();
let mut tools = Vec::new(); let mut tools = Vec::new();
for directory in uv_fs::directories(self.root()) { for directory in uv_fs::directories(self.root()) {
@ -102,6 +102,7 @@ impl InstalledTools {
}; };
let tool_receipt = ToolReceipt::from_string(contents) let tool_receipt = ToolReceipt::from_string(contents)
.map_err(|err| Error::ReceiptRead(path, Box::new(err)))?; .map_err(|err| Error::ReceiptRead(path, Box::new(err)))?;
let name = PackageName::from_str(&name)?;
tools.push((name, tool_receipt.tool)); tools.push((name, tool_receipt.tool));
} }
Ok(tools) Ok(tools)
@ -256,16 +257,15 @@ impl InstalledTools {
)) ))
} }
pub fn version(&self, name: &str, cache: &Cache) -> Result<Version, Error> { pub fn version(&self, name: &PackageName, cache: &Cache) -> Result<Version, Error> {
let environment_path = self.root.join(name); let environment_path = self.root.join(name.to_string());
let package_name = PackageName::from_str(name)?;
let environment = PythonEnvironment::from_root(&environment_path, cache)?; let environment = PythonEnvironment::from_root(&environment_path, cache)?;
let site_packages = SitePackages::from_environment(&environment) let site_packages = SitePackages::from_environment(&environment)
.map_err(|err| Error::EnvironmentRead(environment_path.clone(), err.to_string()))?; .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 let package = packages
.first() .first()
.ok_or_else(|| Error::MissingToolPackage(package_name, environment_path))?; .ok_or_else(|| Error::MissingToolPackage(name.clone(), environment_path))?;
Ok(package.version().clone()) Ok(package.version().clone())
} }

View file

@ -8,7 +8,7 @@ use tracing::debug;
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_tool::InstalledTools; use uv_tool::{InstalledTools, Tool, ToolEntrypoint};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
@ -16,7 +16,7 @@ use crate::printer::Printer;
/// Uninstall a tool. /// Uninstall a tool.
pub(crate) async fn uninstall( pub(crate) async fn uninstall(
name: PackageName, name: Option<PackageName>,
preview: PreviewMode, preview: PreviewMode,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -25,27 +25,64 @@ pub(crate) async fn uninstall(
} }
let installed_tools = InstalledTools::from_settings()?; 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<Vec<ToolEntrypoint>> {
// Remove the tool itself. // Remove the tool itself.
installed_tools.remove_environment(&name)?; tools.remove_environment(name)?;
// Remove the tool's entrypoints. // Remove the tool's entrypoints.
let entrypoints = receipt.entrypoints(); let entrypoints = receipt.entrypoints();
@ -68,16 +105,5 @@ pub(crate) async fn uninstall(
} }
} }
let s = if entrypoints.len() == 1 { "" } else { "s" }; Ok(entrypoints.to_vec())
writeln!(
printer.stderr(),
"Uninstalled {} executable{s}: {}",
entrypoints.len(),
entrypoints
.iter()
.map(|entrypoint| entrypoint.name.bold())
.join(", ")
)?;
Ok(ExitStatus::Success)
} }

View file

@ -300,16 +300,18 @@ impl ToolListSettings {
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct ToolUninstallSettings { pub(crate) struct ToolUninstallSettings {
pub(crate) name: PackageName, pub(crate) name: Option<PackageName>,
} }
impl ToolUninstallSettings { impl ToolUninstallSettings {
/// Resolve the [`ToolUninstallSettings`] from the CLI and filesystem configuration. /// Resolve the [`ToolUninstallSettings`] from the CLI and filesystem configuration.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: ToolUninstallArgs, _filesystem: Option<FilesystemOptions>) -> Self { pub(crate) fn resolve(args: ToolUninstallArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let ToolUninstallArgs { name } = args; let ToolUninstallArgs { name, all } = args;
Self { name } Self {
name: name.filter(|_| !all),
}
} }
} }