Skip to content

Commit

Permalink
Add support for --reinstall and --reinstall-package in `uv tool i…
Browse files Browse the repository at this point in the history
…nstall`
  • Loading branch information
zanieb committed Jun 25, 2024
1 parent 14be755 commit 214b411
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/uv-tool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
24 changes: 17 additions & 7 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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),
Expand All @@ -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.
Expand Down Expand Up @@ -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<PythonEnvironment, Error> {
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,
Expand Down
70 changes: 54 additions & 16 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<VerbatimParsedUrl>::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::<Result<Vec<Requirement<VerbatimParsedUrl>>, _>>()?;

// 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::<Vec<_>>();
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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)?;

Expand Down

0 comments on commit 214b411

Please sign in to comment.