mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Support installing additional executables in uv tool install
This commit is contained in:
parent
6a5d2f1ec4
commit
9367ca45ea
9 changed files with 236 additions and 159 deletions
|
@ -4335,6 +4335,12 @@ pub struct ToolInstallArgs {
|
|||
#[arg(long)]
|
||||
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Install executables from an additional package.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
#[arg(long)]
|
||||
pub with_executables_from: Vec<PackageName>,
|
||||
|
||||
/// Constrain versions using the given requirements files.
|
||||
///
|
||||
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
|
||||
|
|
|
@ -5,6 +5,7 @@ use anyhow::{Context, Result};
|
|||
use console::Term;
|
||||
|
||||
use uv_fs::{CWD, Simplified};
|
||||
use uv_pep508::PackageName;
|
||||
use uv_requirements_txt::RequirementsTxtRequirement;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -203,6 +204,16 @@ impl RequirementsSource {
|
|||
Ok(Self::Package(requirement))
|
||||
}
|
||||
|
||||
/// Parse a [`RequirementsSource`] from a [`PackageName`].
|
||||
///
|
||||
/// Unlike [`RequirementsSource::from_package`], this method does not prompt the user and
|
||||
/// expects a valid [`PackageName`] instead of an arbitrary string.
|
||||
pub fn from_package_name(name: &PackageName) -> Result<Self> {
|
||||
let requirement = RequirementsTxtRequirement::parse(name.as_str(), &*CWD, false)
|
||||
.with_context(|| format!("Failed to parse: `{name}`"))?;
|
||||
Ok(Self::Package(requirement))
|
||||
}
|
||||
|
||||
/// Parse a [`RequirementsSource`] from a user-provided string, assumed to be a `--with`
|
||||
/// package (e.g., `uvx --with flask ruff`).
|
||||
///
|
||||
|
|
|
@ -103,6 +103,7 @@ impl TryFrom<ToolWire> for Tool {
|
|||
pub struct ToolEntrypoint {
|
||||
pub name: String,
|
||||
pub install_path: PathBuf,
|
||||
pub from: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for ToolEntrypoint {
|
||||
|
@ -166,10 +167,9 @@ impl Tool {
|
|||
overrides: Vec<Requirement>,
|
||||
build_constraints: Vec<Requirement>,
|
||||
python: Option<PythonRequest>,
|
||||
entrypoints: impl Iterator<Item = ToolEntrypoint>,
|
||||
mut entrypoints: Vec<ToolEntrypoint>,
|
||||
options: ToolOptions,
|
||||
) -> Self {
|
||||
let mut entrypoints: Vec<_> = entrypoints.collect();
|
||||
entrypoints.sort();
|
||||
Self {
|
||||
requirements,
|
||||
|
@ -345,8 +345,12 @@ impl Tool {
|
|||
|
||||
impl ToolEntrypoint {
|
||||
/// Create a new [`ToolEntrypoint`].
|
||||
pub fn new(name: String, install_path: PathBuf) -> Self {
|
||||
Self { name, install_path }
|
||||
pub fn new(name: String, install_path: PathBuf, from: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
install_path,
|
||||
from: Some(from),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the TOML table for this entrypoint.
|
||||
|
@ -358,6 +362,9 @@ impl ToolEntrypoint {
|
|||
// Use cross-platform slashes so the toml string type does not change
|
||||
value(PortablePath::from(&self.install_path).to_string()),
|
||||
);
|
||||
if let Some(from) = &self.from {
|
||||
table.insert("from", value(from));
|
||||
}
|
||||
table
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
use anyhow::{Context, bail};
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::collections::Bound;
|
||||
use std::fmt::Write;
|
||||
use std::{collections::BTreeSet, ffi::OsString};
|
||||
use std::{
|
||||
collections::{BTreeSet, Bound},
|
||||
env::consts::EXE_SUFFIX,
|
||||
ffi::OsString,
|
||||
fmt::Write,
|
||||
iter,
|
||||
path::Path,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
|
@ -22,12 +27,12 @@ use uv_python::{
|
|||
};
|
||||
use uv_settings::{PythonInstallMirrors, ToolOptions};
|
||||
use uv_shell::Shell;
|
||||
use uv_tool::{InstalledTools, Tool, ToolEntrypoint, entrypoint_paths, tool_executable_dir};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_tool::{InstalledTools, Tool, ToolEntrypoint, entrypoint_paths};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::commands::pip;
|
||||
use crate::commands::project::ProjectError;
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::{ExitStatus, pip};
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Return all packages which contain an executable with the given name.
|
||||
|
@ -169,8 +174,9 @@ pub(crate) async fn refine_interpreter(
|
|||
pub(crate) fn finalize_tool_install(
|
||||
environment: &PythonEnvironment,
|
||||
name: &PackageName,
|
||||
entrypoints: impl IntoIterator<Item = PackageName>,
|
||||
installed_tools: &InstalledTools,
|
||||
options: ToolOptions,
|
||||
options: &ToolOptions,
|
||||
force: bool,
|
||||
python: Option<PythonRequest>,
|
||||
requirements: Vec<Requirement>,
|
||||
|
@ -178,120 +184,137 @@ pub(crate) fn finalize_tool_install(
|
|||
overrides: Vec<Requirement>,
|
||||
build_constraints: Vec<Requirement>,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<ExitStatus> {
|
||||
let site_packages = SitePackages::from_environment(environment)?;
|
||||
let installed = site_packages.get_packages(name);
|
||||
let Some(installed_dist) = installed.first().copied() else {
|
||||
bail!("Expected at least one requirement")
|
||||
};
|
||||
|
||||
// Find a suitable path to install into
|
||||
let executable_directory = tool_executable_dir()?;
|
||||
) -> anyhow::Result<()> {
|
||||
let executable_directory = uv_tool::tool_executable_dir()?;
|
||||
fs_err::create_dir_all(&executable_directory)
|
||||
.context("Failed to create executable directory")?;
|
||||
|
||||
debug!(
|
||||
"Installing tool executables into: {}",
|
||||
executable_directory.user_display()
|
||||
);
|
||||
|
||||
let entry_points = entrypoint_paths(
|
||||
&site_packages,
|
||||
installed_dist.name(),
|
||||
installed_dist.version(),
|
||||
)?;
|
||||
|
||||
// Determine the entry points targets. Use a sorted collection for deterministic output.
|
||||
let target_entry_points = entry_points
|
||||
let mut installed_entrypoints = Vec::new();
|
||||
let site_packages = SitePackages::from_environment(environment)?;
|
||||
let ordered_packages = entrypoints
|
||||
// Install dependencies first
|
||||
.into_iter()
|
||||
.map(|(name, source_path)| {
|
||||
let target_path = executable_directory.join(
|
||||
source_path
|
||||
.file_name()
|
||||
.map(std::borrow::ToOwned::to_owned)
|
||||
.unwrap_or_else(|| OsString::from(name.clone())),
|
||||
);
|
||||
(name, source_path, target_path)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
.filter(|pkg| pkg != name)
|
||||
.collect::<BTreeSet<_>>()
|
||||
// Then install the main package last
|
||||
.into_iter()
|
||||
.chain(iter::once(name.clone()));
|
||||
|
||||
if target_entry_points.is_empty() {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"No executables are provided by `{from}`",
|
||||
from = name.cyan()
|
||||
)?;
|
||||
|
||||
hint_executable_from_dependency(name, &site_packages, printer)?;
|
||||
|
||||
// Clean up the environment we just created.
|
||||
installed_tools.remove_environment(name)?;
|
||||
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
// Error if we're overwriting an existing entrypoint, unless the user passed `--force`.
|
||||
if !force {
|
||||
let mut existing_entry_points = target_entry_points
|
||||
.iter()
|
||||
.filter(|(_, _, target_path)| target_path.exists())
|
||||
.peekable();
|
||||
if existing_entry_points.peek().is_some() {
|
||||
// Clean up the environment we just created
|
||||
installed_tools.remove_environment(name)?;
|
||||
|
||||
let existing_entry_points = existing_entry_points
|
||||
// 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_entry_points.len() == 1 {
|
||||
("", "exists")
|
||||
} else {
|
||||
("s", "exist")
|
||||
};
|
||||
bail!(
|
||||
"Executable{s} already {exists}: {} (use `--force` to overwrite)",
|
||||
existing_entry_points
|
||||
.iter()
|
||||
.map(|name| name.bold())
|
||||
.join(", ")
|
||||
)
|
||||
for package in ordered_packages {
|
||||
if package == *name {
|
||||
debug!("Installing entrypoints for tool `{package}`");
|
||||
} else {
|
||||
debug!("Installing entrypoints for `{package}` as part of tool `{name}`");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
let itself = std::env::current_exe().ok();
|
||||
let installed = site_packages.get_packages(&package);
|
||||
let dist = installed
|
||||
.first()
|
||||
.context("Expected at least one requirement")?;
|
||||
let dist_entrypoints = entrypoint_paths(&site_packages, dist.name(), dist.version())?;
|
||||
|
||||
for (name, source_path, target_path) in &target_entry_points {
|
||||
debug!("Installing executable: `{name}`");
|
||||
// Determine the entry points targets. Use a sorted collection for deterministic output.
|
||||
let target_entrypoints = dist_entrypoints
|
||||
.into_iter()
|
||||
.map(|(name, source_path)| {
|
||||
let target_path = executable_directory.join(
|
||||
source_path
|
||||
.file_name()
|
||||
.map(std::borrow::ToOwned::to_owned)
|
||||
.unwrap_or_else(|| OsString::from(name.clone())),
|
||||
);
|
||||
(name, source_path, target_path)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
#[cfg(unix)]
|
||||
replace_symlink(source_path, target_path).context("Failed to install executable")?;
|
||||
if target_entrypoints.is_empty() {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"No executables are provided by `{}`",
|
||||
package.cyan()
|
||||
)?;
|
||||
|
||||
hint_executable_from_dependency(&package, &site_packages, printer)?;
|
||||
|
||||
// Clean up the environment we just created.
|
||||
installed_tools.remove_environment(&package)?;
|
||||
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to install entrypoints for `{}`",
|
||||
package.cyan()
|
||||
));
|
||||
}
|
||||
|
||||
// Error if we're overwriting an existing entrypoint, unless the user passed `--force`.
|
||||
if !force {
|
||||
let mut existing_entrypoints = target_entrypoints
|
||||
.iter()
|
||||
.filter(|(_, _, target_path)| target_path.exists())
|
||||
.peekable();
|
||||
if existing_entrypoints.peek().is_some() {
|
||||
// Clean up the environment we just created
|
||||
installed_tools.remove_environment(name)?;
|
||||
|
||||
let existing_entrypoints = existing_entrypoints
|
||||
// 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_entrypoints.len() == 1 {
|
||||
("", "exists")
|
||||
} else {
|
||||
("s", "exist")
|
||||
};
|
||||
bail!(
|
||||
"Executable{s} already {exists}: {} (use `--force` to overwrite)",
|
||||
existing_entrypoints
|
||||
.iter()
|
||||
.map(|name| name.bold())
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if itself.as_ref().is_some_and(|itself| {
|
||||
std::path::absolute(target_path).is_ok_and(|target| *itself == target)
|
||||
}) {
|
||||
self_replace::self_replace(source_path).context("Failed to install entrypoint")?;
|
||||
} else {
|
||||
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
|
||||
}
|
||||
}
|
||||
let itself = std::env::current_exe().ok();
|
||||
|
||||
let s = if target_entry_points.len() == 1 {
|
||||
""
|
||||
} else {
|
||||
"s"
|
||||
};
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Installed {} executable{s}: {}",
|
||||
target_entry_points.len(),
|
||||
target_entry_points
|
||||
.iter()
|
||||
.map(|(name, _, _)| name.bold())
|
||||
.join(", ")
|
||||
)?;
|
||||
let mut names = BTreeSet::new();
|
||||
for (name, src, target) in target_entrypoints {
|
||||
debug!("Installing executable: `{name}`");
|
||||
|
||||
#[cfg(unix)]
|
||||
replace_symlink(src, &target).context("Failed to install executable")?;
|
||||
|
||||
#[cfg(windows)]
|
||||
if itself.as_ref().is_some_and(|itself| {
|
||||
std::path::absolute(&target).is_ok_and(|target| *itself == target)
|
||||
}) {
|
||||
self_replace::self_replace(src).context("Failed to install entrypoint")?;
|
||||
} else {
|
||||
fs_err::copy(src, &target).context("Failed to install entrypoint")?;
|
||||
}
|
||||
|
||||
names.insert(name.trim_end_matches(EXE_SUFFIX).to_string());
|
||||
let tool_entry = ToolEntrypoint::new(name, target, package.to_string());
|
||||
installed_entrypoints.push(tool_entry);
|
||||
}
|
||||
|
||||
let s = if names.len() == 1 { "" } else { "s" };
|
||||
let from_pkg = if *name == package {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" from `{package}`")
|
||||
};
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Installed {} executable{s}{from_pkg}: {}",
|
||||
names.len(),
|
||||
names.iter().map(|name| name.bold()).join(", ")
|
||||
)?;
|
||||
}
|
||||
|
||||
debug!("Adding receipt for tool `{name}`");
|
||||
let tool = Tool::new(
|
||||
|
@ -300,45 +323,48 @@ pub(crate) fn finalize_tool_install(
|
|||
overrides,
|
||||
build_constraints,
|
||||
python,
|
||||
target_entry_points
|
||||
.into_iter()
|
||||
.map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)),
|
||||
options,
|
||||
installed_entrypoints,
|
||||
options.clone(),
|
||||
);
|
||||
installed_tools.add_tool_receipt(name, tool)?;
|
||||
|
||||
warn_out_of_path(&executable_directory);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn warn_out_of_path(executable_directory: &Path) {
|
||||
// If the executable directory isn't on the user's PATH, warn.
|
||||
if !Shell::contains_path(&executable_directory) {
|
||||
if !Shell::contains_path(executable_directory) {
|
||||
if let Some(shell) = Shell::from_env() {
|
||||
if let Some(command) = shell.prepend_path(&executable_directory) {
|
||||
if let Some(command) = shell.prepend_path(executable_directory) {
|
||||
if shell.supports_update() {
|
||||
warn_user!(
|
||||
warn_user_once!(
|
||||
"`{}` is not on your PATH. To use installed tools, run `{}` or `{}`.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
command.green(),
|
||||
"uv tool update-shell".green()
|
||||
);
|
||||
} else {
|
||||
warn_user!(
|
||||
warn_user_once!(
|
||||
"`{}` is not on your PATH. To use installed tools, run `{}`.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
command.green()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn_user!(
|
||||
warn_user_once!(
|
||||
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn_user!(
|
||||
warn_user_once!(
|
||||
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -48,6 +48,7 @@ pub(crate) async fn install(
|
|||
editable: bool,
|
||||
from: Option<String>,
|
||||
with: &[RequirementsSource],
|
||||
mut with_executables_from: BTreeSet<PackageName>,
|
||||
constraints: &[RequirementsSource],
|
||||
overrides: &[RequirementsSource],
|
||||
build_constraints: &[RequirementsSource],
|
||||
|
@ -112,7 +113,7 @@ pub(crate) async fn install(
|
|||
};
|
||||
|
||||
// Resolve the `--from` requirement.
|
||||
let from = match &request {
|
||||
let requirement = match &request {
|
||||
// Ex) `ruff`
|
||||
ToolRequest::Package {
|
||||
executable,
|
||||
|
@ -226,6 +227,8 @@ pub(crate) async fn install(
|
|||
}
|
||||
};
|
||||
|
||||
let package_name = &requirement.name;
|
||||
|
||||
// If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable.
|
||||
let settings = if request.is_latest() {
|
||||
ResolverInstallerSettings {
|
||||
|
@ -233,7 +236,7 @@ pub(crate) async fn install(
|
|||
upgrade: settings
|
||||
.resolver
|
||||
.upgrade
|
||||
.combine(Upgrade::package(from.name.clone())),
|
||||
.combine(Upgrade::package(package_name.clone())),
|
||||
..settings.resolver
|
||||
},
|
||||
..settings
|
||||
|
@ -247,7 +250,7 @@ pub(crate) async fn install(
|
|||
ResolverInstallerSettings {
|
||||
reinstall: settings
|
||||
.reinstall
|
||||
.combine(Reinstall::package(from.name.clone())),
|
||||
.combine(Reinstall::package(package_name.clone())),
|
||||
..settings
|
||||
}
|
||||
} else {
|
||||
|
@ -267,7 +270,7 @@ pub(crate) async fn install(
|
|||
// Resolve the `--from` and `--with` requirements.
|
||||
let requirements = {
|
||||
let mut requirements = Vec::with_capacity(1 + with.len());
|
||||
requirements.push(from.clone());
|
||||
requirements.push(requirement.clone());
|
||||
requirements.extend(
|
||||
resolve_names(
|
||||
spec.requirements.clone(),
|
||||
|
@ -331,16 +334,16 @@ pub(crate) async fn install(
|
|||
// (If we find existing entrypoints later on, and the tool _doesn't_ exist, we'll avoid removing
|
||||
// the external tool's entrypoints (without `--force`).)
|
||||
let (existing_tool_receipt, invalid_tool_receipt) =
|
||||
match installed_tools.get_tool_receipt(&from.name) {
|
||||
match installed_tools.get_tool_receipt(package_name) {
|
||||
Ok(None) => (None, false),
|
||||
Ok(Some(receipt)) => (Some(receipt), false),
|
||||
Err(_) => {
|
||||
// If the tool is not installed properly, remove the environment and continue.
|
||||
match installed_tools.remove_environment(&from.name) {
|
||||
match installed_tools.remove_environment(package_name) {
|
||||
Ok(()) => {
|
||||
warn_user!(
|
||||
"Removed existing `{from}` with invalid receipt",
|
||||
from = from.name.cyan()
|
||||
"Removed existing `{}` with invalid receipt",
|
||||
package_name.cyan()
|
||||
);
|
||||
}
|
||||
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
|
@ -354,20 +357,20 @@ pub(crate) async fn install(
|
|||
|
||||
let existing_environment =
|
||||
installed_tools
|
||||
.get_environment(&from.name, &cache)?
|
||||
.get_environment(package_name, &cache)?
|
||||
.filter(|environment| {
|
||||
if environment.uses(&interpreter) {
|
||||
trace!(
|
||||
"Existing interpreter matches the requested interpreter for `{}`: {}",
|
||||
from.name,
|
||||
package_name,
|
||||
environment.interpreter().sys_executable().display()
|
||||
);
|
||||
true
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
printer.stderr(),
|
||||
"Ignoring existing environment for `{from}`: the requested Python interpreter does not match the environment interpreter",
|
||||
from = from.name.cyan(),
|
||||
"Ignoring existing environment for `{}`: the requested Python interpreter does not match the environment interpreter",
|
||||
package_name.cyan(),
|
||||
);
|
||||
false
|
||||
}
|
||||
|
@ -392,15 +395,17 @@ pub(crate) async fn install(
|
|||
{
|
||||
if *tool_receipt.options() != options {
|
||||
// ...but the options differ, we need to update the receipt.
|
||||
installed_tools
|
||||
.add_tool_receipt(&from.name, tool_receipt.clone().with_options(options))?;
|
||||
installed_tools.add_tool_receipt(
|
||||
package_name,
|
||||
tool_receipt.clone().with_options(options),
|
||||
)?;
|
||||
}
|
||||
|
||||
// We're done, though we might need to update the receipt.
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"`{from}` is already installed",
|
||||
from = from.cyan()
|
||||
"`{}` is already installed",
|
||||
requirement.cyan()
|
||||
)?;
|
||||
|
||||
return Ok(ExitStatus::Success);
|
||||
|
@ -556,7 +561,7 @@ pub(crate) async fn install(
|
|||
},
|
||||
};
|
||||
|
||||
let environment = installed_tools.create_environment(&from.name, interpreter, preview)?;
|
||||
let environment = installed_tools.create_environment(package_name, interpreter, preview)?;
|
||||
|
||||
// At this point, we removed any existing environment, so we should remove any of its
|
||||
// executables.
|
||||
|
@ -583,8 +588,8 @@ pub(crate) async fn install(
|
|||
.await
|
||||
.inspect_err(|_| {
|
||||
// If we failed to sync, remove the newly created environment.
|
||||
debug!("Failed to sync environment; removing `{}`", from.name);
|
||||
let _ = installed_tools.remove_environment(&from.name);
|
||||
debug!("Failed to sync environment; removing `{}`", package_name);
|
||||
let _ = installed_tools.remove_environment(package_name);
|
||||
}) {
|
||||
Ok(environment) => environment,
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
|
@ -596,11 +601,14 @@ pub(crate) async fn install(
|
|||
}
|
||||
};
|
||||
|
||||
with_executables_from.insert(package_name.clone());
|
||||
|
||||
finalize_tool_install(
|
||||
&environment,
|
||||
&from.name,
|
||||
package_name,
|
||||
with_executables_from,
|
||||
&installed_tools,
|
||||
options,
|
||||
&options,
|
||||
force || invalid_tool_receipt,
|
||||
python_request,
|
||||
requirements,
|
||||
|
@ -608,5 +616,7 @@ pub(crate) async fn install(
|
|||
overrides,
|
||||
build_constraints,
|
||||
printer,
|
||||
)
|
||||
)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::env::consts::EXE_SUFFIX;
|
||||
use std::fmt::Write;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
|
@ -148,7 +149,11 @@ async fn do_uninstall(
|
|||
}
|
||||
entrypoints
|
||||
};
|
||||
entrypoints.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
entrypoints.sort_unstable_by(|a, b| {
|
||||
let a_trimmed = a.name.trim_end_matches(EXE_SUFFIX);
|
||||
let b_trimmed = b.name.trim_end_matches(EXE_SUFFIX);
|
||||
a_trimmed.cmp(b_trimmed)
|
||||
});
|
||||
|
||||
if entrypoints.is_empty() {
|
||||
// If we removed at least one dangling environment, there's no need to summarize.
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
use tracing::debug;
|
||||
|
||||
use uv_cache::Cache;
|
||||
|
@ -375,12 +376,20 @@ async fn upgrade_tool(
|
|||
// existing executables.
|
||||
remove_entrypoints(&existing_tool_receipt);
|
||||
|
||||
let mut entrypoints: BTreeSet<_> = existing_tool_receipt
|
||||
.entrypoints()
|
||||
.iter()
|
||||
.filter_map(|entry| PackageName::from_str(entry.from.as_ref()?).ok())
|
||||
.collect();
|
||||
entrypoints.insert(name.clone());
|
||||
|
||||
// If we modified the target tool, reinstall the entrypoints.
|
||||
finalize_tool_install(
|
||||
&environment,
|
||||
name,
|
||||
entrypoints,
|
||||
installed_tools,
|
||||
ToolOptions::from(options),
|
||||
&ToolOptions::from(options),
|
||||
true,
|
||||
existing_tool_receipt.python().to_owned(),
|
||||
existing_tool_receipt.requirements().to_vec(),
|
||||
|
|
|
@ -1229,21 +1229,19 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
.combine(Refresh::from(args.settings.resolver.upgrade.clone())),
|
||||
);
|
||||
|
||||
let mut requirements = Vec::with_capacity(
|
||||
args.with.len() + args.with_editable.len() + args.with_requirements.len(),
|
||||
);
|
||||
for package in args.with {
|
||||
requirements.push(RequirementsSource::from_with_package_argument(&package)?);
|
||||
let mut requirements = Vec::new();
|
||||
for pkg in args.with {
|
||||
requirements.push(RequirementsSource::from_with_package_argument(&pkg)?);
|
||||
}
|
||||
for package in args.with_editable {
|
||||
requirements.push(RequirementsSource::from_editable(&package)?);
|
||||
for pkg in &args.with_executables_from {
|
||||
requirements.push(RequirementsSource::from_package_name(pkg)?);
|
||||
}
|
||||
for pkg in args.with_editable {
|
||||
requirements.push(RequirementsSource::from_editable(&pkg)?);
|
||||
}
|
||||
for path in args.with_requirements {
|
||||
requirements.push(RequirementsSource::from_requirements_file(path)?);
|
||||
}
|
||||
requirements.extend(
|
||||
args.with_requirements
|
||||
.into_iter()
|
||||
.map(RequirementsSource::from_requirements_file)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
|
||||
let constraints = args
|
||||
.constraints
|
||||
|
@ -1266,6 +1264,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
args.editable,
|
||||
args.from,
|
||||
&requirements,
|
||||
args.with_executables_from,
|
||||
&constraints,
|
||||
&overrides,
|
||||
&build_constraints,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::env::VarError;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
|
@ -587,6 +588,7 @@ pub(crate) struct ToolInstallSettings {
|
|||
pub(crate) from: Option<String>,
|
||||
pub(crate) with: Vec<String>,
|
||||
pub(crate) with_requirements: Vec<PathBuf>,
|
||||
pub(crate) with_executables_from: BTreeSet<PackageName>,
|
||||
pub(crate) with_editable: Vec<String>,
|
||||
pub(crate) constraints: Vec<PathBuf>,
|
||||
pub(crate) overrides: Vec<PathBuf>,
|
||||
|
@ -611,6 +613,7 @@ impl ToolInstallSettings {
|
|||
with,
|
||||
with_editable,
|
||||
with_requirements,
|
||||
with_executables_from,
|
||||
constraints,
|
||||
overrides,
|
||||
build_constraints,
|
||||
|
@ -651,6 +654,7 @@ impl ToolInstallSettings {
|
|||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
.collect(),
|
||||
with_executables_from: with_executables_from.into_iter().collect(),
|
||||
constraints: constraints
|
||||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue