mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Merge 164b4de4d8
into a9ea756d14
This commit is contained in:
commit
1070400e5f
15 changed files with 498 additions and 244 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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3165,6 +3165,7 @@ fn resolve_tool() -> anyhow::Result<()> {
|
|||
from: None,
|
||||
with: [],
|
||||
with_requirements: [],
|
||||
with_executables_from: [],
|
||||
with_editable: [],
|
||||
constraints: [],
|
||||
overrides: [],
|
||||
|
|
|
@ -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
|
||||
"###);
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
|
|
@ -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
|
||||
");
|
||||
}
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue