Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for --reinstall and --reinstall-package in uv tool install #4504

Merged
merged 1 commit into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -21,6 +21,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 }
tracing = { workspace = true }
Expand Down
21 changes: 17 additions & 4 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::io::{self, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::debug;
use uv_cache::Cache;
use uv_fs::{LockedFile, Simplified};
use uv_toolchain::{Interpreter, PythonEnvironment};

Expand All @@ -21,9 +22,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] Box<toml::de::Error>),
#[error(transparent)]
VirtualEnvError(#[from] uv_virtualenv::Error),
Expand All @@ -33,6 +34,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 @@ -121,16 +124,26 @@ impl InstalledTools {
Ok(())
}

pub fn create_environment(
pub fn environment(
&self,
name: &str,
remove_existing: bool,
interpreter: Interpreter,
cache: &Cache,
) -> Result<PythonEnvironment, Error> {
let _lock = self.acquire_lock();
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()
);

Expand Down
85 changes: 64 additions & 21 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use itertools::Itertools;

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};
#[cfg(unix)]
use uv_fs::replace_symlink;
use uv_fs::Simplified;
Expand Down Expand Up @@ -47,29 +47,44 @@ pub(crate) async fn install(
if preview.is_disabled() {
warn_user_once!("`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 installed_tools.find_tool_entry(&name)?.is_some() {
let reinstall_entry_points = if existing_tool_entry.is_some() {
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.as_ref())]
.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 @@ -93,7 +108,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 @@ -115,13 +136,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 @@ -130,6 +161,7 @@ pub(crate) async fn install(
)?;

// Determine the entry points targets
// Use a sorted collection for deterministic output
let targets = entrypoints
.into_iter()
.map(|(name, path)| {
Expand All @@ -140,16 +172,19 @@ pub(crate) async fn install(
);
(name, path, target)
})
.collect::<Vec<_>>();
.collect::<BTreeSet<_>>();

// Check if they exist, before installing
let mut existing_targets = targets
.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 All @@ -159,7 +194,7 @@ pub(crate) async fn install(
let existing_targets = existing_targets
// SAFETY: We know the target has a filename because we just constructed it above
.map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
.collect::<BTreeSet<_>>();
.collect::<Vec<_>>();
let (s, exists) = if existing_targets.len() == 1 {
("", "exists")
} else {
Expand All @@ -172,15 +207,23 @@ pub(crate) async fn install(
}

// TODO(zanieb): Handle the case where there are no entrypoints
for (name, path, target) in targets {
for (name, path, target) in &targets {
debug!("Installing `{name}`");
#[cfg(unix)]
replace_symlink(&path, &target).context("Failed to install entrypoint")?;
replace_symlink(path, target).context("Failed to install entrypoint")?;
#[cfg(windows)]
fs_err::copy(&path, &target).context("Failed to install entrypoint")?;
fs_err::copy(path, target).context("Failed to install entrypoint")?;
}
writeln!(
printer.stdout(),
"Installed: {}",
targets.iter().map(|(name, _, _)| name).join(", ")
)?;

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
Loading
Loading