Skip to content

Commit

Permalink
Add uv tool install --force
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Jun 25, 2024
1 parent 502c1da commit 14be755
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 7 deletions.
9 changes: 8 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use clap::{Args, Parser, Subcommand};
use distribution_types::{FlatIndexLocation, IndexUrl};
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
ConfigSettingEntry, IndexStrategy, KeyringProviderType, PackageNameSpecifier, Reinstall,
TargetTriple,
};
use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
Expand Down Expand Up @@ -1889,6 +1890,12 @@ pub struct ToolInstallArgs {
#[command(flatten)]
pub refresh: RefreshArgs,

/// Force installation of the tool.
///
/// Will replace any existing entrypoints with the same name in the executable directory.
#[arg(long)]
pub force: bool,

/// The Python interpreter to use to build the run environment.
///
/// By default, `uv` uses the virtual environment in the current working directory or any parent
Expand Down
59 changes: 53 additions & 6 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::ffi::OsString;
use std::fmt::Write;
use std::str::FromStr;

Expand Down Expand Up @@ -29,6 +30,7 @@ pub(crate) async fn install(
python: Option<String>,
from: Option<String>,
with: Vec<String>,
force: bool,
settings: ResolverInstallerSettings,
_isolated: bool,
preview: PreviewMode,
Expand All @@ -45,10 +47,14 @@ pub(crate) async fn install(

let installed_tools = InstalledTools::from_settings()?;

// TODO(zanieb): Allow replacing an existing tool
// TODO(zanieb): Automatically replace an existing tool if the request differs
if let Some(_) = installed_tools.find_tool_entry(&name)? {
writeln!(printer.stderr(), "Tool `{name}` is already installed.")?;
return Ok(ExitStatus::Failure);
if force {
debug!("Replacing existing tool due to `--force` flag.");
} else {
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?
Expand Down Expand Up @@ -112,18 +118,59 @@ pub(crate) async fn install(
fs_err::create_dir_all(&executable_directory)
.context("Failed to create executable directory")?;

debug!("Installing into {}", executable_directory.user_display());

let entrypoints = entrypoint_paths(
&environment,
installed_dist.name(),
installed_dist.version(),
None,
)?;

// Determine the entry points targets
let targets = entrypoints
.into_iter()
.map(|(name, path)| {
let target = executable_directory.join(
path.file_name()
.map(|str| str.to_owned())
.unwrap_or_else(|| OsString::from(name.clone())),
);
(name, path, target)
})
.collect::<Vec<_>>();

// Check if they exist, before installing
let mut existing_targets = targets
.iter()
.filter(|(_, _, target)| target.exists())
.peekable();
if force {
for (name, _, target) in existing_targets {
debug!("Removing existing install of `{name}`");
fs_err::remove_file(target)?;
}
} else if existing_targets.peek().is_some() {
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::<Vec<_>>();
let (s, exists) = if existing_targets.len() == 1 {
("", "exists")
} else {
("s", "exist")
};
bail!(
"Entry point{s} for tool already {exists}, use `--force` to overwrite: {}",
existing_targets.join(", ")
)
}

// TODO(zanieb): Handle the case where there are no entrypoints
// TODO(zanieb): Better error when an entry point exists, check if they all are don't exist first
for (name, path) in entrypoints {
let target = executable_directory.join(path.file_name().unwrap());
debug!("Installing {name} to {}", target.user_display());
// let existing = entrypoints.iter().map(|(name, file_name)|
for (name, path, target) in targets {
debug!("Installing `{name}`");
if cfg!(unix) {
fs_err::os::unix::fs::symlink(path, target)?;
} else {
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,7 @@ async fn run() -> Result<ExitStatus> {
args.python,
args.from,
args.with,
args.force,
args.settings,
globals.isolated,
globals.preview,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ pub(crate) struct ToolInstallSettings {
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverInstallerSettings,
pub(crate) force: bool,
}

impl ToolInstallSettings {
Expand All @@ -252,6 +253,7 @@ impl ToolInstallSettings {
from,
with,
installer,
force,
build,
refresh,
python,
Expand All @@ -262,6 +264,7 @@ impl ToolInstallSettings {
from,
with,
python,
force,
refresh: Refresh::from(refresh),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
Expand Down

0 comments on commit 14be755

Please sign in to comment.