mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add uv tool uninstall
(#4641)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
8d9b4a5e1c
commit
7da3423af9
10 changed files with 239 additions and 5 deletions
|
@ -1875,6 +1875,8 @@ pub enum ToolCommand {
|
|||
Install(ToolInstallArgs),
|
||||
/// List installed tools.
|
||||
List(ToolListArgs),
|
||||
/// Uninstall a tool.
|
||||
Uninstall(ToolUninstallArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
@ -1975,6 +1977,12 @@ pub struct ToolInstallArgs {
|
|||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolListArgs;
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolUninstallArgs {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolchainNamespace {
|
||||
|
|
|
@ -143,6 +143,9 @@ impl InstalledTools {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the environment for a tool.
|
||||
///
|
||||
/// Does not remove the tool's entrypoints.
|
||||
pub fn remove_environment(&self, name: &str) -> Result<(), Error> {
|
||||
let _lock = self.acquire_lock();
|
||||
let environment_path = self.root.join(name);
|
||||
|
|
|
@ -25,8 +25,8 @@ pub struct Tool {
|
|||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ToolEntrypoint {
|
||||
name: String,
|
||||
install_path: PathBuf,
|
||||
pub name: String,
|
||||
pub install_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Format an array so that each element is on its own line and has a trailing comma.
|
||||
|
@ -105,6 +105,10 @@ impl Tool {
|
|||
|
||||
table
|
||||
}
|
||||
|
||||
pub fn entrypoints(&self) -> &[ToolEntrypoint] {
|
||||
&self.entrypoints
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolEntrypoint {
|
||||
|
|
|
@ -27,6 +27,7 @@ pub(crate) use self_update::self_update;
|
|||
pub(crate) use tool::install::install as tool_install;
|
||||
pub(crate) use tool::list::list as tool_list;
|
||||
pub(crate) use tool::run::run as tool_run;
|
||||
pub(crate) use tool::uninstall::uninstall as tool_uninstall;
|
||||
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;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub(crate) mod install;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod run;
|
||||
pub(crate) mod uninstall;
|
||||
|
|
65
crates/uv/src/commands/tool/uninstall.rs
Normal file
65
crates/uv/src/commands/tool/uninstall.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools;
|
||||
|
||||
use tracing::debug;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_fs::Simplified;
|
||||
use uv_tool::InstalledTools;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Uninstall a tool.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn uninstall(
|
||||
name: String,
|
||||
preview: PreviewMode,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if preview.is_disabled() {
|
||||
warn_user_once!("`uv tool uninstall` is experimental and may change without warning.");
|
||||
}
|
||||
|
||||
let installed_tools = InstalledTools::from_settings()?;
|
||||
let Some(receipt) = installed_tools.get_tool_receipt(&name)? else {
|
||||
bail!("Tool `{}` is not installed", name);
|
||||
};
|
||||
|
||||
// Remove the tool itself.
|
||||
installed_tools.remove_environment(&name)?;
|
||||
|
||||
// Remove the tool's entrypoints.
|
||||
let entrypoints = receipt.entrypoints();
|
||||
for entrypoint in entrypoints {
|
||||
debug!(
|
||||
"Removing entrypoint: {}",
|
||||
entrypoint.install_path.user_display()
|
||||
);
|
||||
match fs_err::tokio::remove_file(&entrypoint.install_path).await {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
debug!(
|
||||
"Entrypoint not found: {}",
|
||||
entrypoint.install_path.user_display()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Uninstalled: {}",
|
||||
entrypoints
|
||||
.iter()
|
||||
.map(|entrypoint| &entrypoint.name)
|
||||
.join(", ")
|
||||
)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
|
@ -830,7 +830,6 @@ async fn run() -> Result<ExitStatus> {
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
Commands::Tool(ToolNamespace {
|
||||
command: ToolCommand::List(args),
|
||||
}) => {
|
||||
|
@ -840,6 +839,15 @@ async fn run() -> Result<ExitStatus> {
|
|||
|
||||
commands::tool_list(globals.preview, printer).await
|
||||
}
|
||||
Commands::Tool(ToolNamespace {
|
||||
command: ToolCommand::Uninstall(args),
|
||||
}) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::ToolUninstallSettings::resolve(args, filesystem);
|
||||
show_settings!(args);
|
||||
|
||||
commands::tool_uninstall(args.name, globals.preview, printer).await
|
||||
}
|
||||
Commands::Toolchain(ToolchainNamespace {
|
||||
command: ToolchainCommand::List(args),
|
||||
}) => {
|
||||
|
|
|
@ -14,8 +14,8 @@ use uv_cli::{
|
|||
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
|
||||
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs,
|
||||
ToolListArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs,
|
||||
VenvArgs,
|
||||
ToolListArgs, ToolRunArgs, ToolUninstallArgs, ToolchainFindArgs, ToolchainInstallArgs,
|
||||
ToolchainListArgs, VenvArgs,
|
||||
};
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{
|
||||
|
@ -291,6 +291,23 @@ impl ToolListSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `tool uninstall` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolUninstallSettings {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
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<FilesystemOptions>) -> Self {
|
||||
let ToolUninstallArgs { name } = args;
|
||||
|
||||
Self { name }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) enum ToolchainListKinds {
|
||||
#[default]
|
||||
|
|
|
@ -420,6 +420,14 @@ impl TestContext {
|
|||
command
|
||||
}
|
||||
|
||||
/// Create a `uv tool uninstall` command with options shared across scenarios.
|
||||
pub fn tool_uninstall(&self) -> std::process::Command {
|
||||
let mut command = std::process::Command::new(get_bin());
|
||||
command.arg("tool").arg("uninstall");
|
||||
self.add_shared_args(&mut command);
|
||||
command
|
||||
}
|
||||
|
||||
/// Create a `uv add` command for the given requirements.
|
||||
pub fn add(&self, reqs: &[&str]) -> Command {
|
||||
let mut command = Command::new(get_bin());
|
||||
|
|
119
crates/uv/tests/tool_uninstall.rs
Normal file
119
crates/uv/tests/tool_uninstall.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::fixture::PathChild;
|
||||
use common::{uv_snapshot, TestContext};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn tool_uninstall() {
|
||||
let context = TestContext::new("3.12");
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Install `black`
|
||||
context
|
||||
.tool_install()
|
||||
.arg("black==24.2.0")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_uninstall().arg("black")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool uninstall` is experimental and may change without warning.
|
||||
Uninstalled: black, blackd
|
||||
"###);
|
||||
|
||||
// After uninstalling the tool, it shouldn't be listed.
|
||||
uv_snapshot!(context.filters(), context.tool_list()
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool list` is experimental and may change without warning.
|
||||
No tools installed
|
||||
"###);
|
||||
|
||||
// After uninstalling the tool, we should be able to reinstall it.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black==24.2.0")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning.
|
||||
Resolved 6 packages in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
+ black==24.2.0
|
||||
+ click==8.1.7
|
||||
+ mypy-extensions==1.0.0
|
||||
+ packaging==24.0
|
||||
+ pathspec==0.12.1
|
||||
+ platformdirs==4.2.0
|
||||
Installed: black, blackd
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_uninstall_not_installed() {
|
||||
let context = TestContext::new("3.12");
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_uninstall().arg("black")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool uninstall` is experimental and may change without warning.
|
||||
error: Tool `black` is not installed
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_uninstall_missing_receipt() {
|
||||
let context = TestContext::new("3.12");
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Install `black`
|
||||
context
|
||||
.tool_install()
|
||||
.arg("black==24.2.0")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
fs_err::remove_file(tool_dir.join("black").join("uv-receipt.toml")).unwrap();
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_uninstall().arg("black")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool uninstall` is experimental and may change without warning.
|
||||
error: Tool `black` is not installed
|
||||
"###);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue