From 214b4116faa2f2616e36007dd3504808570f81ce Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 24 Jun 2024 20:16:29 -0500 Subject: [PATCH] Add support for `--reinstall` and `--reinstall-package` in `uv tool install` --- Cargo.lock | 1 + crates/uv-tool/Cargo.toml | 1 + crates/uv-tool/src/lib.rs | 24 ++++++--- crates/uv/src/commands/tool/install.rs | 70 ++++++++++++++++++++------ 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b93ba802cbe9f..95e2715773d44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5026,6 +5026,7 @@ dependencies = [ "toml", "toml_edit", "tracing", + "uv-cache", "uv-extract", "uv-fs", "uv-state", diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index c461f6fabe08a..fb6949595e621 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -22,6 +22,7 @@ uv-virtualenv = { workspace = true } uv-toolchain = { workspace = true } install-wheel-rs = { workspace = true } pep440_rs = { workspace = true } +uv-cache = { workspace = true } thiserror = { workspace = true } directories = { workspace = true } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 6ce7baab9246c..c227cce4ed882 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -7,6 +7,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use thiserror::Error; use tracing::debug; +use uv_cache::Cache; use uv_fs::Simplified; use uv_toolchain::{Interpreter, PythonEnvironment}; @@ -20,9 +21,9 @@ pub enum Error { #[error(transparent)] IO(#[from] io::Error), // TODO(zanieb): Improve the error handling here - #[error("Failed to update `tools.toml` at {0}")] + #[error("Failed to update `tools.toml` metadata at {0}")] TomlEdit(PathBuf, #[source] tools_toml::Error), - #[error("Failed to read `tools.toml` at {0}")] + #[error("Failed to read `tools.toml` metadata at {0}")] TomlRead(PathBuf, #[source] toml::de::Error), #[error(transparent)] VirtualEnvError(#[from] uv_virtualenv::Error), @@ -32,6 +33,8 @@ pub enum Error { DistInfoMissing(String, PathBuf), #[error("Failed to find a directory for executables")] NoExecutableDirectory, + #[error(transparent)] + EnvironmentError(#[from] uv_toolchain::Error), } /// A collection of uv-managed tools installed on the current system. @@ -97,21 +100,28 @@ impl InstalledTools { Ok(()) } - pub fn create_environment( + pub fn environment( &self, name: &str, + remove_existing: bool, interpreter: Interpreter, + cache: &Cache, ) -> Result { let environment_path = self.root.join(name); + if !remove_existing && environment_path.exists() { + debug!( + "Using existing environment for tool `{name}` at `{}`.", + environment_path.user_display() + ); + return Ok(PythonEnvironment::from_root(environment_path, cache)?); + } + debug!( - "Creating environment for tool `{name}` at {}.", + "Creating environment for tool `{name}` at `{}`.", environment_path.user_display() ); - // Discover an interpreter. - // Note we force preview on during `uv tool run` for now since the entire interface is in preview - // Create a virtual environment. let venv = uv_virtualenv::create_venv( &environment_path, diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 6d92460d438bb..84cecc205f6c9 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -7,10 +7,10 @@ use distribution_types::Name; use pep508_rs::Requirement; use pypi_types::VerbatimParsedUrl; -use tracing::debug; +use tracing::{debug, trace}; use uv_cache::Cache; use uv_client::Connectivity; -use uv_configuration::{Concurrency, PreviewMode}; +use uv_configuration::{Concurrency, PreviewMode, Reinstall}; use uv_fs::Simplified; use uv_installer::SitePackages; use uv_requirements::RequirementsSource; @@ -44,29 +44,44 @@ pub(crate) async fn install( if preview.is_disabled() { warn_user!("`uv tool install` is experimental and may change without warning."); } + let from = from.unwrap_or(name.clone()); let installed_tools = InstalledTools::from_settings()?; + // TODO(zanieb): Figure out the interface here, do we infer the name or do we match the `run --from` interface? + let from = Requirement::::from_str(&from)?; + let existing_tool_entry = installed_tools.find_tool_entry(&name)?; // TODO(zanieb): Automatically replace an existing tool if the request differs - if let Some(_) = installed_tools.find_tool_entry(&name)? { + let reinstall_entry_points = if let Some(_) = existing_tool_entry { if force { debug!("Replacing existing tool due to `--force` flag."); + false } else { - writeln!(printer.stderr(), "Tool `{name}` is already installed.")?; - return Ok(ExitStatus::Failure); + match settings.reinstall { + Reinstall::All => { + debug!("Replacing existing tool due to `--reinstall` flag."); + true + } + // Do not replace the entry points unless the tool is explicitly requested + Reinstall::Packages(ref packages) => packages.contains(&from.name), + // If not reinstalling... then we're done + Reinstall::None => { + writeln!(printer.stderr(), "Tool `{name}` is already installed.")?; + return Ok(ExitStatus::Failure); + } + } } - } - - // TODO(zanieb): Figure out the interface here, do we infer the name or do we match the `run --from` interface? - let from = from.unwrap_or(name.clone()); + } else { + false + }; - let requirements = [Requirement::from_str(&from)] + let requirements = [Requirement::from_str(&from.name.to_string())] .into_iter() .chain(with.iter().map(|name| Requirement::from_str(name))) .collect::>, _>>()?; // TODO(zanieb): Duplicative with the above parsing but needed for `update_environment` - let requirements_sources = [RequirementsSource::from_package(from.clone())] + let requirements_sources = [RequirementsSource::from_package(from.name.to_string())] .into_iter() .chain(with.into_iter().map(RequirementsSource::from_package)) .collect::>(); @@ -90,7 +105,13 @@ pub(crate) async fn install( // TODO(zanieb): Build the environment in the cache directory then copy into the tool directory // This lets us confirm the environment is valid before removing an existing install - let environment = installed_tools.create_environment(&name, interpreter)?; + let environment = installed_tools.environment( + &name, + // Do not remove the existing environment if we're reinstalling a subset of packages + !matches!(settings.reinstall, Reinstall::Packages(_)), + interpreter, + &cache, + )?; // Install the ephemeral requirements. let environment = update_environment( @@ -112,13 +133,23 @@ pub(crate) async fn install( bail!("Expected at least one requirement") }; + // Exit early if we're not supposed to be reinstalling entry points + // e.g. `--reinstall-package` was used for some dependency + if existing_tool_entry.is_some() && !reinstall_entry_points { + writeln!(printer.stderr(), "Updated environment for tool `{name}`")?; + return Ok(ExitStatus::Success); + } + // Find a suitable path to install into // TODO(zanieb): Warn if this directory is not on the PATH let executable_directory = find_executable_directory()?; fs_err::create_dir_all(&executable_directory) .context("Failed to create executable directory")?; - debug!("Installing into {}", executable_directory.user_display()); + debug!( + "Installing tool entry points into {}", + executable_directory.user_display() + ); let entrypoints = entrypoint_paths( &environment, @@ -145,9 +176,12 @@ pub(crate) async fn install( .iter() .filter(|(_, _, target)| target.exists()) .peekable(); - if force { + + // Note we use `reinstall_entry_points` here instead of `reinstall`; requesting reinstall + // will _not_ remove existing entry points when they are not managed by uv. + if force || reinstall_entry_points { for (name, _, target) in existing_targets { - debug!("Removing existing install of `{name}`"); + debug!("Removing existing entry point `{name}`"); fs_err::remove_file(target)?; } } else if existing_targets.peek().is_some() { @@ -176,9 +210,13 @@ pub(crate) async fn install( } else { fs_err::copy(path, target)?; }; + writeln!(printer.stderr(), "Installed `{name}`")?; } - debug!("Adding `{name}` to {}", path.user_display()); + trace!( + "Tracking installed tool `{name}` in tool metadata at `{}`", + path.user_display() + ); let installed_tools = installed_tools.init()?; installed_tools.add_tool_entry(&name, &tool)?;