mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35: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),
|
Install(ToolInstallArgs),
|
||||||
/// List installed tools.
|
/// List installed tools.
|
||||||
List(ToolListArgs),
|
List(ToolListArgs),
|
||||||
|
/// Uninstall a tool.
|
||||||
|
Uninstall(ToolUninstallArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
@ -1975,6 +1977,12 @@ pub struct ToolInstallArgs {
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct ToolListArgs;
|
pub struct ToolListArgs;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct ToolUninstallArgs {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct ToolchainNamespace {
|
pub struct ToolchainNamespace {
|
||||||
|
|
|
@ -143,6 +143,9 @@ impl InstalledTools {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove the environment for a tool.
|
||||||
|
///
|
||||||
|
/// Does not remove the tool's entrypoints.
|
||||||
pub fn remove_environment(&self, name: &str) -> Result<(), Error> {
|
pub fn remove_environment(&self, name: &str) -> Result<(), Error> {
|
||||||
let _lock = self.acquire_lock();
|
let _lock = self.acquire_lock();
|
||||||
let environment_path = self.root.join(name);
|
let environment_path = self.root.join(name);
|
||||||
|
|
|
@ -25,8 +25,8 @@ pub struct Tool {
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct ToolEntrypoint {
|
pub struct ToolEntrypoint {
|
||||||
name: String,
|
pub name: String,
|
||||||
install_path: PathBuf,
|
pub install_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format an array so that each element is on its own line and has a trailing comma.
|
/// Format an array so that each element is on its own line and has a trailing comma.
|
||||||
|
@ -105,6 +105,10 @@ impl Tool {
|
||||||
|
|
||||||
table
|
table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn entrypoints(&self) -> &[ToolEntrypoint] {
|
||||||
|
&self.entrypoints
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolEntrypoint {
|
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::install::install as tool_install;
|
||||||
pub(crate) use tool::list::list as tool_list;
|
pub(crate) use tool::list::list as tool_list;
|
||||||
pub(crate) use tool::run::run as tool_run;
|
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::find::find as toolchain_find;
|
||||||
pub(crate) use toolchain::install::install as toolchain_install;
|
pub(crate) use toolchain::install::install as toolchain_install;
|
||||||
pub(crate) use toolchain::list::list as toolchain_list;
|
pub(crate) use toolchain::list::list as toolchain_list;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub(crate) mod install;
|
pub(crate) mod install;
|
||||||
pub(crate) mod list;
|
pub(crate) mod list;
|
||||||
pub(crate) mod run;
|
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
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Tool(ToolNamespace {
|
Commands::Tool(ToolNamespace {
|
||||||
command: ToolCommand::List(args),
|
command: ToolCommand::List(args),
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -840,6 +839,15 @@ async fn run() -> Result<ExitStatus> {
|
||||||
|
|
||||||
commands::tool_list(globals.preview, printer).await
|
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 {
|
Commands::Toolchain(ToolchainNamespace {
|
||||||
command: ToolchainCommand::List(args),
|
command: ToolchainCommand::List(args),
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
@ -14,8 +14,8 @@ use uv_cli::{
|
||||||
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
|
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
|
||||||
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs,
|
PipSyncArgs, PipTreeArgs, PipUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs,
|
||||||
ToolListArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs,
|
ToolListArgs, ToolRunArgs, ToolUninstallArgs, ToolchainFindArgs, ToolchainInstallArgs,
|
||||||
VenvArgs,
|
ToolchainListArgs, VenvArgs,
|
||||||
};
|
};
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{
|
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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub(crate) enum ToolchainListKinds {
|
pub(crate) enum ToolchainListKinds {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
|
@ -420,6 +420,14 @@ impl TestContext {
|
||||||
command
|
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.
|
/// Create a `uv add` command for the given requirements.
|
||||||
pub fn add(&self, reqs: &[&str]) -> Command {
|
pub fn add(&self, reqs: &[&str]) -> Command {
|
||||||
let mut command = Command::new(get_bin());
|
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