This commit is contained in:
Aaron Ang 2025-07-02 12:13:11 -03:00 committed by GitHub
commit 1070400e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 498 additions and 244 deletions

View file

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

View file

@ -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`).
///

View file

@ -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
}
}

View file

@ -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.

View file

@ -48,6 +48,7 @@ pub(crate) async fn install(
editable: bool,
from: Option<String>,
with: &[RequirementsSource],
mut with_executables_from: Vec<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);
@ -558,7 +563,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.
@ -585,8 +590,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)) => {
@ -598,11 +603,14 @@ pub(crate) async fn install(
}
};
with_executables_from.push(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,
@ -610,5 +618,7 @@ pub(crate) async fn install(
overrides,
build_constraints,
printer,
)
)?;
Ok(ExitStatus::Success)
}

View file

@ -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.

View file

@ -3,6 +3,7 @@ use itertools::Itertools;
use owo_colors::OwoColorize;
use std::collections::BTreeMap;
use std::fmt::Write;
use std::str::FromStr;
use tracing::debug;
use uv_cache::Cache;
@ -376,12 +377,20 @@ async fn upgrade_tool(
// existing executables.
remove_entrypoints(&existing_tool_receipt);
let mut entrypoints: Vec<_> = existing_tool_receipt
.entrypoints()
.iter()
.filter_map(|entry| PackageName::from_str(entry.from.as_ref()?).ok())
.collect();
entrypoints.push(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(),

View file

@ -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,

View file

@ -595,6 +595,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: Vec<PackageName>,
pub(crate) with_editable: Vec<String>,
pub(crate) constraints: Vec<PathBuf>,
pub(crate) overrides: Vec<PathBuf>,
@ -619,6 +620,7 @@ impl ToolInstallSettings {
with,
with_editable,
with_requirements,
with_executables_from,
constraints,
overrides,
build_constraints,
@ -659,6 +661,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)

View file

@ -3165,6 +3165,7 @@ fn resolve_tool() -> anyhow::Result<()> {
from: None,
with: [],
with_requirements: [],
with_executables_from: [],
with_editable: [],
constraints: [],
overrides: [],

View file

@ -82,8 +82,8 @@ fn tool_install() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -168,7 +168,7 @@ fn tool_install() {
[tool]
requirements = [{ name = "flask" }]
entrypoints = [
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@ -382,8 +382,8 @@ fn tool_install_with_compatible_build_constraints() -> Result<()> {
]
build-constraint-dependencies = [{ name = "setuptools", specifier = ">=40" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -448,9 +448,9 @@ fn tool_install_suggest_other_packages_with_executable() {
uv_snapshot!(filters, context.tool_install()
.arg("fastapi==0.111.0")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
No executables are provided by `fastapi`
However, an executable with the name `fastapi` is available via dependency `fastapi-cli`.
@ -494,7 +494,8 @@ fn tool_install_suggest_other_packages_with_executable() {
+ uvicorn==0.29.0
+ watchfiles==0.21.0
+ websockets==12.0
"###);
error: Failed to install entrypoints for `fastapi`
");
}
/// Test installing a tool at a version
@ -565,8 +566,8 @@ fn tool_install_version() {
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -649,7 +650,7 @@ fn tool_install_editable() {
[tool]
requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
]
[tool.options]
@ -690,7 +691,7 @@ fn tool_install_editable() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
]
[tool.options]
@ -733,8 +734,8 @@ fn tool_install_editable() {
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -781,8 +782,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -821,9 +822,9 @@ fn tool_install_remove_on_empty() -> Result<()> {
.arg(black.path())
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
No executables are provided by `black`
@ -839,7 +840,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
- packaging==24.0
- pathspec==0.12.1
- platformdirs==4.2.0
"###);
error: Failed to install entrypoints for `black`
");
// Re-request `black`. It should reinstall, without requiring `--force`.
uv_snapshot!(context.filters(), context.tool_install()
@ -871,8 +873,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -949,7 +951,7 @@ fn tool_install_editable_from() {
[tool]
requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
]
[tool.options]
@ -1101,8 +1103,8 @@ fn tool_install_already_installed() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -1137,8 +1139,8 @@ fn tool_install_already_installed() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -1428,8 +1430,8 @@ fn tool_install_force() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -1466,8 +1468,8 @@ fn tool_install_force() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -1649,9 +1651,9 @@ fn tool_install_no_entrypoints() {
.arg("iniconfig")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
No executables are provided by `iniconfig`
@ -1660,7 +1662,8 @@ fn tool_install_no_entrypoints() {
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
error: Failed to install entrypoints for `iniconfig`
");
// Ensure the tool environment is not created.
tool_dir
@ -1795,8 +1798,8 @@ fn tool_install_unnamed_package() {
[tool]
requirements = [{ name = "black", url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -1910,8 +1913,8 @@ fn tool_install_unnamed_from() {
[tool]
requirements = [{ name = "black", url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2004,8 +2007,8 @@ fn tool_install_unnamed_with() {
{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2073,8 +2076,8 @@ fn tool_install_requirements_txt() {
{ name = "iniconfig" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2118,8 +2121,8 @@ fn tool_install_requirements_txt() {
{ name = "idna" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2182,8 +2185,8 @@ fn tool_install_requirements_txt_arguments() {
{ name = "idna" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2296,8 +2299,8 @@ fn tool_install_upgrade() {
[tool]
requirements = [{ name = "black", specifier = "==24.1.1" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2330,8 +2333,8 @@ fn tool_install_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2370,8 +2373,8 @@ fn tool_install_upgrade() {
{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2410,8 +2413,8 @@ fn tool_install_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -2721,7 +2724,7 @@ fn tool_install_warn_path() {
.arg("black==24.1.1")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env_remove(EnvVars::PATH), @r###"
.env_remove(EnvVars::PATH), @r#"
success: true
exit_code: 0
----- stdout -----
@ -2738,7 +2741,7 @@ fn tool_install_warn_path() {
+ platformdirs==4.2.0
Installed 2 executables: black, blackd
warning: `[TEMP_DIR]/bin` is not on your PATH. To use installed tools, run `export PATH="[TEMP_DIR]/bin:$PATH"` or `uv tool update-shell`.
"###);
"#);
}
/// Test installing and reinstalling with an invalid receipt.
@ -2875,16 +2878,16 @@ fn tool_install_malformed_dist_info() {
filters => context.filters(),
}, {
// We should have a tool receipt
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r###"
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
{ name = "app", install-path = "[TEMP_DIR]/bin/app" },
{ name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
exclude-newer = "2025-01-18T00:00:00Z"
"###);
"#);
});
}
@ -2959,7 +2962,7 @@ fn tool_install_settings() {
[tool]
requirements = [{ name = "flask", specifier = ">=3" }]
entrypoints = [
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@ -2992,7 +2995,7 @@ fn tool_install_settings() {
[tool]
requirements = [{ name = "flask", specifier = ">=3" }]
entrypoints = [
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@ -3032,7 +3035,7 @@ fn tool_install_settings() {
[tool]
requirements = [{ name = "flask", specifier = ">=3" }]
entrypoints = [
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@ -3081,8 +3084,8 @@ fn tool_install_at_version() {
[tool]
requirements = [{ name = "black", specifier = "==24.1.0" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3147,8 +3150,8 @@ fn tool_install_at_latest() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3189,16 +3192,16 @@ fn tool_install_from_at_latest() {
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r###"
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
{ name = "app", install-path = "[TEMP_DIR]/bin/app" },
{ name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
exclude-newer = "2025-01-18T00:00:00Z"
"###);
"#);
});
}
@ -3234,16 +3237,16 @@ fn tool_install_from_at_version() {
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r###"
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [{ name = "executable-application", specifier = "==0.2.0" }]
entrypoints = [
{ name = "app", install-path = "[TEMP_DIR]/bin/app" },
{ name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
exclude-newer = "2025-01-18T00:00:00Z"
"###);
"#);
});
}
@ -3287,8 +3290,8 @@ fn tool_install_at_latest_upgrade() {
[tool]
requirements = [{ name = "black", specifier = "==24.1.1" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3321,8 +3324,8 @@ fn tool_install_at_latest_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3358,8 +3361,8 @@ fn tool_install_at_latest_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3420,8 +3423,8 @@ fn tool_install_constraints() -> Result<()> {
{ name = "anyio", specifier = ">=3" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3527,8 +3530,8 @@ fn tool_install_overrides() -> Result<()> {
{ name = "anyio", specifier = ">=3" },
]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -3630,3 +3633,99 @@ fn tool_install_mismatched_name() {
error: Package name (`black`) provided with `--from` does not match install request (`flask`)
"###);
}
/// Test installing a tool together with some additional entrypoints
/// from other packages.
#[test]
fn tool_install_additional_entrypoints() {
let context = TestContext::new("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("--with-executables-from")
.arg("ansible-core")
.arg("--with-executables-from")
.arg("black")
.arg("ansible==9.3.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ ansible==9.3.0
+ ansible-core==2.16.4
+ black==24.3.0
+ cffi==1.16.0
+ click==8.1.7
+ cryptography==42.0.5
+ jinja2==3.1.3
+ markupsafe==2.1.5
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
+ pycparser==2.21
+ pyyaml==6.0.1
+ resolvelib==1.0.1
Installed 11 executables from `ansible-core`: ansible, ansible-config, ansible-connection, ansible-console, ansible-doc, ansible-galaxy, ansible-inventory, ansible-playbook, ansible-pull, ansible-test, ansible-vault
Installed 2 executables from `black`: black, blackd
Installed 1 executable: ansible-community
");
// NOTE(lucab): on Windows `name` values contain an `.exe` suffix,
// which is hidden in the snapshot but results in a different sorting.
#[cfg(not(target_family = "windows"))]
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("ansible").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [
{ name = "ansible", specifier = "==9.3.0" },
{ name = "ansible-core" },
{ name = "black" },
]
entrypoints = [
{ name = "ansible", install-path = "[TEMP_DIR]/bin/ansible", from = "ansible-core" },
{ name = "ansible-community", install-path = "[TEMP_DIR]/bin/ansible-community", from = "ansible" },
{ name = "ansible-config", install-path = "[TEMP_DIR]/bin/ansible-config", from = "ansible-core" },
{ name = "ansible-connection", install-path = "[TEMP_DIR]/bin/ansible-connection", from = "ansible-core" },
{ name = "ansible-console", install-path = "[TEMP_DIR]/bin/ansible-console", from = "ansible-core" },
{ name = "ansible-doc", install-path = "[TEMP_DIR]/bin/ansible-doc", from = "ansible-core" },
{ name = "ansible-galaxy", install-path = "[TEMP_DIR]/bin/ansible-galaxy", from = "ansible-core" },
{ name = "ansible-inventory", install-path = "[TEMP_DIR]/bin/ansible-inventory", from = "ansible-core" },
{ name = "ansible-playbook", install-path = "[TEMP_DIR]/bin/ansible-playbook", from = "ansible-core" },
{ name = "ansible-pull", install-path = "[TEMP_DIR]/bin/ansible-pull", from = "ansible-core" },
{ name = "ansible-test", install-path = "[TEMP_DIR]/bin/ansible-test", from = "ansible-core" },
{ name = "ansible-vault", install-path = "[TEMP_DIR]/bin/ansible-vault", from = "ansible-core" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
exclude-newer = "2024-03-25T00:00:00Z"
"#);
});
uv_snapshot!(context.filters(), context.tool_uninstall()
.arg("ansible")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 14 executables: ansible, ansible-community, ansible-config, ansible-connection, ansible-console, ansible-doc, ansible-galaxy, ansible-inventory, ansible-playbook, ansible-pull, ansible-test, ansible-vault, black, blackd
"###);
}

View file

@ -218,8 +218,8 @@ fn tool_list_deprecated() -> Result<()> {
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@ -234,8 +234,8 @@ fn tool_list_deprecated() -> Result<()> {
[tool]
requirements = ["black==24.2.0"]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
"#,
)?;
@ -261,8 +261,8 @@ fn tool_list_deprecated() -> Result<()> {
[tool]
requirements = ["black<>24.2.0"]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
"#,
)?;

View file

@ -835,3 +835,71 @@ fn tool_upgrade_python_with_all() {
assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]");
});
}
/// Upgrade a tool together with any additional entrypoints from other
/// packages.
#[test]
fn test_tool_upgrade_additional_entrypoints() {
let context = TestContext::new_with_versions(&["3.11", "3.12"])
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
// Install `babel` entrypoint, and all additional ones from `black` too.
uv_snapshot!(context.filters(), context.tool_install()
.arg("--python")
.arg("3.11")
.arg("--with-executables-from")
.arg("black")
.arg("babel==2.14.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.14.0
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables from `black`: black, blackd
Installed 1 executable: pybabel
");
// Upgrade python, and make sure that all the entrypoints above get
// re-installed.
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--python")
.arg("3.12")
.arg("babel")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.14.0
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables from `black`: black, blackd
Installed 1 executable: pybabel
Upgraded tool environment for `babel` to Python 3.12
");
}

View file

@ -213,6 +213,14 @@ As with `uvx`, installations can include additional packages:
$ uv tool install mkdocs --with mkdocs-material
```
Multiple related executables can be installed together in the same tool environment, using the
`--with-executables-from` flag. For example, the following will install the executables from
`ansible`, plus those ones provided by `ansible-core` and `ansible-lint`:
```console
$ uv tool install --with-executables-from ansible-core --with-executables-from ansible-lint ansible
```
## Upgrading tools
To upgrade a tool, use `uv tool upgrade`:

View file

@ -2097,6 +2097,8 @@ uv tool install [OPTIONS] <PACKAGE>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd><dt id="uv-tool-install--with"><a href="#uv-tool-install--with"><code>--with</code></a> <i>with</i></dt><dd><p>Include the following additional requirements</p>
</dd><dt id="uv-tool-install--with-editable"><a href="#uv-tool-install--with-editable"><code>--with-editable</code></a> <i>with-editable</i></dt><dd><p>Include the given packages in editable mode</p>
</dd><dt id="uv-tool-install--with-executables-from"><a href="#uv-tool-install--with-executables-from"><code>--with-executables-from</code></a> <i>with-executables-from</i></dt><dd><p>Install executables from an additional package.</p>
<p>May be provided multiple times.</p>
</dd><dt id="uv-tool-install--with-requirements"><a href="#uv-tool-install--with-requirements"><code>--with-requirements</code></a> <i>with-requirements</i></dt><dd><p>Include all requirements listed in the given <code>requirements.txt</code> files</p>
</dd></dl>