mirror of https://github.com/astral-sh/uv
Add `uv tool upgrade` command (#5197)
## Summary Resolves #5188. Most of the changes involve creating a new function in `tool/common.rs` to contain the common functionality previously found in `tool/install.rs`. ## Test Plan `cargo test` ```console ❯ ./target/debug/uv tool upgrade black warning: `uv tool upgrade` is experimental and may change without warning. Resolved 6 packages in 25ms Uninstalled 1 package in 3ms Installed 1 package in 19ms - black==23.1.0 + black==24.4.2 Installed 2 executables: black, blackd ```
This commit is contained in:
parent
bf0497e652
commit
cbc3274848
|
|
@ -2555,6 +2555,8 @@ pub enum ToolCommand {
|
|||
Uvx(ToolRunArgs),
|
||||
/// Install a tool.
|
||||
Install(ToolInstallArgs),
|
||||
/// Upgrade a tool.
|
||||
Upgrade(ToolUpgradeArgs),
|
||||
/// List installed tools.
|
||||
List(ToolListArgs),
|
||||
/// Uninstall a tool.
|
||||
|
|
@ -2712,6 +2714,27 @@ pub struct ToolUninstallArgs {
|
|||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolUpgradeArgs {
|
||||
/// The name of the tool to upgrade.
|
||||
#[arg(required = true)]
|
||||
pub name: Option<PackageName>,
|
||||
|
||||
/// Upgrade all tools.
|
||||
#[arg(long, conflicts_with("name"))]
|
||||
pub all: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub installer: ResolverInstallerArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub build: BuildArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub refresh: RefreshArgs,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct PythonNamespace {
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@ impl Tool {
|
|||
pub fn requirements(&self) -> &[Requirement] {
|
||||
&self.requirements
|
||||
}
|
||||
|
||||
pub fn python(&self) -> &Option<String> {
|
||||
&self.python
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolEntrypoint {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pub(crate) use tool::run::run as tool_run;
|
|||
pub(crate) use tool::run::ToolRunCommand;
|
||||
pub(crate) use tool::uninstall::uninstall as tool_uninstall;
|
||||
pub(crate) use tool::update_shell::update_shell as tool_update_shell;
|
||||
pub(crate) use tool::upgrade::upgrade as tool_upgrade;
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_git::GitResolver;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
use std::fmt::Write;
|
||||
use std::{collections::BTreeSet, ffi::OsString};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use distribution_types::{InstalledDist, Name};
|
||||
use pep508_rs::PackageName;
|
||||
use pypi_types::Requirement;
|
||||
#[cfg(unix)]
|
||||
use uv_fs::replace_symlink;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_tool::entrypoint_paths;
|
||||
use uv_python::PythonEnvironment;
|
||||
use uv_shell::Shell;
|
||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Return all packages which contain an executable with the given name.
|
||||
pub(super) fn matching_packages(name: &str, site_packages: &SitePackages) -> Vec<InstalledDist> {
|
||||
|
|
@ -23,3 +42,235 @@ pub(super) fn matching_packages(name: &str, site_packages: &SitePackages) -> Vec
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Remove any entrypoints attached to the [`Tool`].
|
||||
pub(crate) fn remove_entrypoints(tool: &Tool) {
|
||||
for executable in tool
|
||||
.entrypoints()
|
||||
.iter()
|
||||
.map(|entrypoint| &entrypoint.install_path)
|
||||
{
|
||||
debug!("Removing executable: `{}`", executable.simplified_display());
|
||||
if let Err(err) = fs_err::remove_file(executable) {
|
||||
warn!(
|
||||
"Failed to remove executable: `{}`: {err}",
|
||||
executable.simplified_display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the action to be performed on executables: update or install.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum InstallAction {
|
||||
Update,
|
||||
Install,
|
||||
}
|
||||
|
||||
/// Installs tool executables for a given package and handles any conflicts.
|
||||
pub(crate) fn install_executables(
|
||||
environment: &PythonEnvironment,
|
||||
name: &PackageName,
|
||||
installed_tools: &InstalledTools,
|
||||
printer: Printer,
|
||||
force: bool,
|
||||
python: Option<String>,
|
||||
requirements: Vec<Requirement>,
|
||||
action: InstallAction,
|
||||
) -> 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 = find_executable_directory()?;
|
||||
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
|
||||
.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<_>>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Check if they exist, before installing
|
||||
let mut existing_entry_points = target_entry_points
|
||||
.iter()
|
||||
.filter(|(_, _, target_path)| target_path.exists())
|
||||
.peekable();
|
||||
|
||||
// Ignore any existing entrypoints if the user passed `--force`, or the existing recept was
|
||||
// broken.
|
||||
if force {
|
||||
for (name, _, target) in existing_entry_points {
|
||||
debug!("Removing existing executable: `{name}`");
|
||||
fs_err::remove_file(target)?;
|
||||
}
|
||||
} else 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 (name, source_path, target_path) in &target_entry_points {
|
||||
debug!("Installing executable: `{name}`");
|
||||
#[cfg(unix)]
|
||||
replace_symlink(source_path, target_path).context("Failed to install executable")?;
|
||||
#[cfg(windows)]
|
||||
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
|
||||
}
|
||||
|
||||
let s = if target_entry_points.len() == 1 {
|
||||
""
|
||||
} else {
|
||||
"s"
|
||||
};
|
||||
let install_message = match action {
|
||||
InstallAction::Install => "Installed",
|
||||
InstallAction::Update => "Updated",
|
||||
};
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{install_message} {} executable{s}: {}",
|
||||
target_entry_points.len(),
|
||||
target_entry_points
|
||||
.iter()
|
||||
.map(|(name, _, _)| name.bold())
|
||||
.join(", ")
|
||||
)?;
|
||||
|
||||
debug!("Adding receipt for tool `{}`", name);
|
||||
let tool = Tool::new(
|
||||
requirements.into_iter().collect(),
|
||||
python,
|
||||
target_entry_points
|
||||
.into_iter()
|
||||
.map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)),
|
||||
);
|
||||
installed_tools.add_tool_receipt(name, tool)?;
|
||||
|
||||
// If the executable directory isn't on the user's PATH, warn.
|
||||
if !Shell::contains_path(&executable_directory) {
|
||||
if let Some(shell) = Shell::from_env() {
|
||||
if let Some(command) = shell.prepend_path(&executable_directory) {
|
||||
if shell.configuration_files().is_empty() {
|
||||
warn_user!(
|
||||
"`{}` is not on your PATH. To use installed tools, run `{}`.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
command.green()
|
||||
);
|
||||
} else {
|
||||
warn_user!(
|
||||
"`{}` 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!(
|
||||
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn_user!(
|
||||
"`{}` 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.
|
||||
fn hint_executable_from_dependency(
|
||||
name: &PackageName,
|
||||
site_packages: &SitePackages,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<()> {
|
||||
let packages = matching_packages(name.as_ref(), site_packages);
|
||||
match packages.as_slice() {
|
||||
[] => {}
|
||||
[package] => {
|
||||
let command = format!("uv tool install {}", package.name());
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, an executable with the name `{}` is available via dependency `{}`.\nDid you mean `{}`?",
|
||||
name.cyan(),
|
||||
package.name().cyan(),
|
||||
command.bold(),
|
||||
)?;
|
||||
}
|
||||
packages => {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, an executable with the name `{}` is available via the following dependencies::",
|
||||
name.cyan(),
|
||||
)?;
|
||||
|
||||
for package in packages {
|
||||
writeln!(printer.stdout(), "- {}", package.name().cyan())?;
|
||||
}
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"Did you mean to install one of them instead?"
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,30 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use itertools::Itertools;
|
||||
use anyhow::{bail, Result};
|
||||
use distribution_types::UnresolvedRequirementSpecification;
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::{debug, warn};
|
||||
use tracing::debug;
|
||||
|
||||
use distribution_types::{Name, UnresolvedRequirementSpecification};
|
||||
use pypi_types::Requirement;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{Concurrency, PreviewMode};
|
||||
#[cfg(unix)]
|
||||
use uv_fs::replace_symlink;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonFetch, PythonInstallation, PythonPreference, PythonRequest,
|
||||
};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_shell::Shell;
|
||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||
use uv_tool::InstalledTools;
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
||||
|
||||
use crate::commands::tool::common::remove_entrypoints;
|
||||
use crate::commands::{
|
||||
project::{resolve_environment, resolve_names, sync_environment, update_environment},
|
||||
tool::common::matching_packages,
|
||||
tool::common::InstallAction,
|
||||
};
|
||||
use crate::commands::{reporters::PythonDownloadReporter, tool::common::install_executables};
|
||||
use crate::commands::{ExitStatus, SharedState};
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::ResolverInstallerSettings;
|
||||
|
|
@ -337,213 +329,14 @@ pub(crate) async fn install(
|
|||
.await?
|
||||
};
|
||||
|
||||
let site_packages = SitePackages::from_environment(&environment)?;
|
||||
let installed = site_packages.get_packages(&from.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 = find_executable_directory()?;
|
||||
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
|
||||
.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<_>>();
|
||||
|
||||
if target_entry_points.is_empty() {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"No executables are provided by `{from}`",
|
||||
from = from.name.cyan()
|
||||
)?;
|
||||
|
||||
hint_executable_from_dependency(&from, &site_packages, printer)?;
|
||||
|
||||
// Clean up the environment we just created.
|
||||
installed_tools.remove_environment(&from.name)?;
|
||||
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
// Check if they exist, before installing
|
||||
let mut existing_entry_points = target_entry_points
|
||||
.iter()
|
||||
.filter(|(_, _, target_path)| target_path.exists())
|
||||
.peekable();
|
||||
|
||||
// Ignore any existing entrypoints if the user passed `--force`, or the existing recept was
|
||||
// broken.
|
||||
if force || invalid_tool_receipt {
|
||||
for (name, _, target) in existing_entry_points {
|
||||
debug!("Removing existing executable: `{name}`");
|
||||
fs_err::remove_file(target)?;
|
||||
}
|
||||
} else if existing_entry_points.peek().is_some() {
|
||||
// Clean up the environment we just created
|
||||
installed_tools.remove_environment(&from.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 (name, source_path, target_path) in &target_entry_points {
|
||||
debug!("Installing executable: `{name}`");
|
||||
#[cfg(unix)]
|
||||
replace_symlink(source_path, target_path).context("Failed to install executable")?;
|
||||
#[cfg(windows)]
|
||||
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
|
||||
}
|
||||
|
||||
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(", ")
|
||||
)?;
|
||||
|
||||
debug!("Adding receipt for tool `{}`", from.name);
|
||||
let tool = Tool::new(
|
||||
requirements.into_iter().collect(),
|
||||
install_executables(
|
||||
&environment,
|
||||
&from.name,
|
||||
&installed_tools,
|
||||
printer,
|
||||
force || invalid_tool_receipt,
|
||||
python,
|
||||
target_entry_points
|
||||
.into_iter()
|
||||
.map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)),
|
||||
);
|
||||
installed_tools.add_tool_receipt(&from.name, tool)?;
|
||||
|
||||
// If the executable directory isn't on the user's PATH, warn.
|
||||
if !Shell::contains_path(&executable_directory) {
|
||||
if let Some(shell) = Shell::from_env() {
|
||||
if let Some(command) = shell.prepend_path(&executable_directory) {
|
||||
if shell.configuration_files().is_empty() {
|
||||
warn_user!(
|
||||
"`{}` is not on your PATH. To use installed tools, run `{}`.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
command.green()
|
||||
);
|
||||
} else {
|
||||
warn_user!(
|
||||
"`{}` 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!(
|
||||
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn_user!(
|
||||
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
|
||||
executable_directory.simplified_display().cyan(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
/// Remove any entrypoints attached to the [`Tool`].
|
||||
fn remove_entrypoints(tool: &Tool) {
|
||||
for executable in tool
|
||||
.entrypoints()
|
||||
.iter()
|
||||
.map(|entrypoint| &entrypoint.install_path)
|
||||
{
|
||||
debug!("Removing executable: `{}`", executable.simplified_display());
|
||||
if let Err(err) = fs_err::remove_file(executable) {
|
||||
warn!(
|
||||
"Failed to remove executable: `{}`: {err}",
|
||||
executable.simplified_display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
|
||||
fn hint_executable_from_dependency(
|
||||
from: &Requirement,
|
||||
site_packages: &SitePackages,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let packages = matching_packages(from.name.as_ref(), site_packages);
|
||||
match packages.as_slice() {
|
||||
[] => {}
|
||||
[package] => {
|
||||
let command = format!("uv tool install {}", package.name());
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, an executable with the name `{}` is available via dependency `{}`.\nDid you mean `{}`?",
|
||||
from.name.cyan(),
|
||||
package.name().cyan(),
|
||||
command.bold(),
|
||||
)?;
|
||||
}
|
||||
packages => {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, an executable with the name `{}` is available via the following dependencies::",
|
||||
from.name.cyan(),
|
||||
)?;
|
||||
|
||||
for package in packages {
|
||||
writeln!(printer.stdout(), "- {}", package.name().cyan())?;
|
||||
}
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"Did you mean to install one of them instead?"
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
requirements,
|
||||
InstallAction::Install,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ pub(crate) mod list;
|
|||
pub(crate) mod run;
|
||||
pub(crate) mod uninstall;
|
||||
pub(crate) mod update_shell;
|
||||
pub(crate) mod upgrade;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
use std::{collections::BTreeSet, fmt::Write};
|
||||
|
||||
use anyhow::Result;
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
||||
use crate::commands::project::update_environment;
|
||||
use crate::commands::tool::common::{remove_entrypoints, InstallAction};
|
||||
use crate::commands::{tool::common::install_executables, ExitStatus, SharedState};
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::ResolverInstallerSettings;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{Concurrency, PreviewMode, Upgrade};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_requirements::RequirementsSpecification;
|
||||
use uv_tool::InstalledTools;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
/// Upgrade a tool.
|
||||
pub(crate) async fn upgrade(
|
||||
name: Option<PackageName>,
|
||||
connectivity: Connectivity,
|
||||
settings: ResolverInstallerSettings,
|
||||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
cache: &Cache,
|
||||
preview: PreviewMode,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if preview.is_disabled() {
|
||||
warn_user_once!("`uv tool upgrade` is experimental and may change without warning");
|
||||
}
|
||||
|
||||
// Force upgrades.
|
||||
let settings = ResolverInstallerSettings {
|
||||
upgrade: Upgrade::All,
|
||||
..settings
|
||||
};
|
||||
|
||||
// Initialize any shared state.
|
||||
let state = SharedState::default();
|
||||
|
||||
let installed_tools = InstalledTools::from_settings()?.init()?;
|
||||
let _lock = installed_tools.acquire_lock()?;
|
||||
|
||||
let names: BTreeSet<PackageName> =
|
||||
name.map(|name| BTreeSet::from_iter([name]))
|
||||
.unwrap_or_else(|| {
|
||||
installed_tools
|
||||
.tools()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(name, _)| name)
|
||||
.collect()
|
||||
});
|
||||
|
||||
if names.is_empty() {
|
||||
writeln!(printer.stderr(), "Nothing to upgrade")?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
for name in names {
|
||||
debug!("Upgrading tool: `{name}`");
|
||||
|
||||
// Ensure the tool is installed.
|
||||
let existing_tool_receipt = match installed_tools.get_tool_receipt(&name) {
|
||||
Ok(Some(receipt)) => receipt,
|
||||
Ok(None) => {
|
||||
let install_command = format!("uv tool install {name}");
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"`{}` is not installed; run `{}` to install",
|
||||
name.cyan(),
|
||||
install_command.green()
|
||||
)?;
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
Err(_) => {
|
||||
let install_command = format!("uv tool install --force {name}");
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"`{}` is missing a valid receipt; run `{}` to reinstall",
|
||||
name.cyan(),
|
||||
install_command.green()
|
||||
)?;
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
};
|
||||
|
||||
let existing_environment = match installed_tools.get_environment(&name, cache) {
|
||||
Ok(Some(environment)) => environment,
|
||||
Ok(None) => {
|
||||
let install_command = format!("uv tool install {name}");
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"`{}` is not installed; run `{}` to install",
|
||||
name.cyan(),
|
||||
install_command.green()
|
||||
)?;
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
Err(_) => {
|
||||
let install_command = format!("uv tool install --force {name}");
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"`{}` is missing a valid environment; run `{}` to reinstall",
|
||||
name.cyan(),
|
||||
install_command.green()
|
||||
)?;
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the requirements.
|
||||
let requirements = existing_tool_receipt.requirements();
|
||||
let spec = RequirementsSpecification::from_requirements(requirements.to_vec());
|
||||
|
||||
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory.
|
||||
// This lets us confirm the environment is valid before removing an existing install. However,
|
||||
// entrypoints always contain an absolute path to the relevant Python interpreter, which would
|
||||
// be invalidated by moving the environment.
|
||||
let environment = update_environment(
|
||||
existing_environment,
|
||||
spec,
|
||||
&settings,
|
||||
&state,
|
||||
Box::new(DefaultResolveLogger),
|
||||
Box::new(DefaultInstallLogger),
|
||||
preview,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// At this point, we updated the existing environment, so we should remove any of its
|
||||
// existing executables.
|
||||
remove_entrypoints(&existing_tool_receipt);
|
||||
|
||||
install_executables(
|
||||
&environment,
|
||||
&name,
|
||||
&installed_tools,
|
||||
printer,
|
||||
true,
|
||||
existing_tool_receipt.python().to_owned(),
|
||||
requirements.to_vec(),
|
||||
InstallAction::Update,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
|
@ -796,6 +796,28 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
|
||||
commands::tool_list(args.show_paths, globals.preview, &cache, printer).await
|
||||
}
|
||||
Commands::Tool(ToolNamespace {
|
||||
command: ToolCommand::Upgrade(args),
|
||||
}) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::ToolUpgradeSettings::resolve(args, filesystem);
|
||||
show_settings!(args);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?.with_refresh(args.refresh);
|
||||
|
||||
commands::tool_upgrade(
|
||||
args.name,
|
||||
globals.connectivity,
|
||||
args.settings,
|
||||
Concurrency::default(),
|
||||
globals.native_tls,
|
||||
&cache,
|
||||
globals.preview,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Tool(ToolNamespace {
|
||||
command: ToolCommand::Uninstall(args),
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ use install_wheel_rs::linker::LinkMode;
|
|||
use pep508_rs::{ExtraName, RequirementOrigin};
|
||||
use pypi_types::Requirement;
|
||||
use uv_cache::{CacheArgs, Refresh};
|
||||
use uv_cli::options::{flag, resolver_installer_options, resolver_options};
|
||||
use uv_cli::{
|
||||
options::{flag, resolver_installer_options, resolver_options},
|
||||
ToolUpgradeArgs,
|
||||
};
|
||||
use uv_cli::{
|
||||
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs,
|
||||
Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||
|
|
@ -375,6 +378,42 @@ impl ToolListSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `tool upgrade` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolUpgradeSettings {
|
||||
pub(crate) name: Option<PackageName>,
|
||||
pub(crate) settings: ResolverInstallerSettings,
|
||||
pub(crate) refresh: Refresh,
|
||||
}
|
||||
|
||||
impl ToolUpgradeSettings {
|
||||
/// Resolve the [`ToolUpgradeSettings`] from the CLI and filesystem configuration.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: ToolUpgradeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let ToolUpgradeArgs {
|
||||
name,
|
||||
all,
|
||||
mut installer,
|
||||
build,
|
||||
refresh,
|
||||
} = args;
|
||||
|
||||
if !installer.upgrade && installer.upgrade_package.is_empty() {
|
||||
installer.upgrade = true;
|
||||
}
|
||||
|
||||
Self {
|
||||
name: name.filter(|_| !all),
|
||||
settings: ResolverInstallerSettings::combine(
|
||||
resolver_installer_options(installer, build),
|
||||
filesystem,
|
||||
),
|
||||
refresh: Refresh::from(refresh),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `tool uninstall` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -504,6 +504,14 @@ impl TestContext {
|
|||
command
|
||||
}
|
||||
|
||||
/// Create a `uv upgrade run` command with options shared across scenarios.
|
||||
pub fn tool_upgrade(&self) -> Command {
|
||||
let mut command = Command::new(get_bin());
|
||||
command.arg("tool").arg("upgrade");
|
||||
self.add_shared_args(&mut command);
|
||||
command
|
||||
}
|
||||
|
||||
/// Create a `uv tool install` command with options shared across scenarios.
|
||||
pub fn tool_install(&self) -> Command {
|
||||
let mut command = self.tool_install_without_exclude_newer();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||
|
||||
use assert_fs::prelude::*;
|
||||
|
||||
use common::{uv_snapshot, TestContext};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn test_tool_upgrade_name() {
|
||||
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");
|
||||
|
||||
// Install `black`.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black>=23.1")
|
||||
.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 -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ 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: black, blackd
|
||||
"###);
|
||||
|
||||
// Upgrade `black`. This should be a no-op, since we have the latest version already.
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("black")
|
||||
.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 -----
|
||||
warning: `uv tool upgrade` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Audited [N] packages in [TIME]
|
||||
Updated 2 executables: black, blackd
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_upgrade_all() {
|
||||
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");
|
||||
|
||||
// Install `black==23.1`.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black==23.1")
|
||||
.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 -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ black==23.1.0
|
||||
+ click==8.1.7
|
||||
+ mypy-extensions==1.0.0
|
||||
+ packaging==24.0
|
||||
+ pathspec==0.12.1
|
||||
+ platformdirs==4.2.0
|
||||
Installed 2 executables: black, blackd
|
||||
"###);
|
||||
|
||||
// Install `pytest==8.0`.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("pytest==8.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 -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
+ packaging==24.0
|
||||
+ pluggy==1.4.0
|
||||
+ pytest==8.0.0
|
||||
Installed 2 executables: py.test, pytest
|
||||
"###);
|
||||
|
||||
// Upgrade all. This is a no-op, since we have the latest versions already.
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("--all")
|
||||
.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 -----
|
||||
warning: `uv tool upgrade` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Audited [N] packages in [TIME]
|
||||
Updated 2 executables: black, blackd
|
||||
Resolved [N] packages in [TIME]
|
||||
Audited [N] packages in [TIME]
|
||||
Updated 2 executables: py.test, pytest
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_upgrade_non_existing_package() {
|
||||
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");
|
||||
|
||||
// Attempt to upgrade `black`.
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("black")
|
||||
.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: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool upgrade` is experimental and may change without warning
|
||||
`black` is not installed; run `uv tool install black` to install
|
||||
"###);
|
||||
|
||||
// Attempt to upgrade all.
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("--all")
|
||||
.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 -----
|
||||
warning: `uv tool upgrade` is experimental and may change without warning
|
||||
Nothing to upgrade
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_upgrade_settings() {
|
||||
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");
|
||||
|
||||
// Install `black` with `lowest-direct`.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black>=23")
|
||||
.arg("--resolution=lowest-direct")
|
||||
.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 -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ black==23.1.0
|
||||
+ click==8.1.7
|
||||
+ mypy-extensions==1.0.0
|
||||
+ packaging==24.0
|
||||
+ pathspec==0.12.1
|
||||
+ platformdirs==4.2.0
|
||||
Installed 2 executables: black, blackd
|
||||
"###);
|
||||
|
||||
// Upgrade `black`. It should respect `lowest-direct`, but doesn't right now, so it's
|
||||
// unintentionally upgraded.
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("black")
|
||||
.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 -----
|
||||
warning: `uv tool upgrade` is experimental and may change without warning
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Uninstalled [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
- black==23.1.0
|
||||
+ black==24.3.0
|
||||
Updated 2 executables: black, blackd
|
||||
"###);
|
||||
}
|
||||
|
|
@ -1796,6 +1796,8 @@ uv tool [OPTIONS] <COMMAND>
|
|||
</dd>
|
||||
<dt><a href="#uv-tool-install"><code>uv tool install</code></a></dt><dd><p>Install a tool</p>
|
||||
</dd>
|
||||
<dt><a href="#uv-tool-upgrade"><code>uv tool upgrade</code></a></dt><dd><p>Upgrade a tool</p>
|
||||
</dd>
|
||||
<dt><a href="#uv-tool-list"><code>uv tool list</code></a></dt><dd><p>List installed tools</p>
|
||||
</dd>
|
||||
<dt><a href="#uv-tool-uninstall"><code>uv tool uninstall</code></a></dt><dd><p>Uninstall a tool</p>
|
||||
|
|
@ -2288,6 +2290,239 @@ uv tool install [OPTIONS] <PACKAGE>
|
|||
|
||||
</dd></dl>
|
||||
|
||||
### uv tool upgrade
|
||||
|
||||
Upgrade a tool
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
uv tool upgrade [OPTIONS] <NAME>
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Arguments</h3>
|
||||
|
||||
<dl class="cli-reference"><dt><code>NAME</code></dt><dd><p>The name of the tool to upgrade</p>
|
||||
|
||||
</dd></dl>
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt><code>--all</code></dt><dd><p>Upgrade all tools</p>
|
||||
|
||||
</dd><dt><code>--cache-dir</code> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
|
||||
|
||||
<p>Defaults to <code>$HOME/Library/Caches/uv</code> on macOS, <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on Linux, and <code>{FOLDERID_LocalAppData}\uv\cache</code> on Windows.</p>
|
||||
|
||||
</dd><dt><code>--color</code> <i>color-choice</i></dt><dd><p>Control colors in output</p>
|
||||
|
||||
<p>[default: auto]</p>
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>auto</code>: Enables colored output only when the output is going to a terminal or TTY with support</li>
|
||||
|
||||
<li><code>always</code>: Enables colored output regardless of the detected environment</li>
|
||||
|
||||
<li><code>never</code>: Disables colored output</li>
|
||||
</ul>
|
||||
</dd><dt><code>--compile-bytecode</code></dt><dd><p>Compile Python files to bytecode after installation.</p>
|
||||
|
||||
<p>By default, uv does not compile Python (<code>.py</code>) files to bytecode (<code>__pycache__/*.pyc</code>); instead, compilation is performed lazily the first time a module is imported. For use-cases in which start time is critical, such as CLI applications and Docker containers, this option can be enabled to trade longer installation times for faster start times.</p>
|
||||
|
||||
<p>When enabled, uv will process the entire site-packages directory (including packages that are not being modified by the current operation) for consistency. Like pip, it will also ignore errors.</p>
|
||||
|
||||
</dd><dt><code>--config-file</code> <i>config-file</i></dt><dd><p>The path to a <code>uv.toml</code> file to use for configuration.</p>
|
||||
|
||||
<p>While uv configuration can be included in a <code>pyproject.toml</code> file, it is not allowed in this context.</p>
|
||||
|
||||
</dd><dt><code>--config-setting</code>, <code>-C</code> <i>config-setting</i></dt><dd><p>Settings to pass to the PEP 517 build backend, specified as <code>KEY=VALUE</code> pairs</p>
|
||||
|
||||
</dd><dt><code>--exclude-newer</code> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>
|
||||
|
||||
<p>Accepts both RFC 3339 timestamps (e.g., <code>2006-12-02T02:07:43Z</code>) and UTC dates in the same format (e.g., <code>2006-12-02</code>).</p>
|
||||
|
||||
</dd><dt><code>--extra-index-url</code> <i>extra-index-url</i></dt><dd><p>Extra URLs of package indexes to use, in addition to <code>--index-url</code>.</p>
|
||||
|
||||
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>
|
||||
|
||||
<p>All indexes provided via this flag take priority over the index specified by <code>--index-url</code> (which defaults to PyPI). When multiple <code>--extra-index-url</code> flags are provided, earlier values take priority.</p>
|
||||
|
||||
</dd><dt><code>--find-links</code>, <code>-f</code> <i>find-links</i></dt><dd><p>Locations to search for candidate distributions, in addition to those found in the registry indexes.</p>
|
||||
|
||||
<p>If a path, the target must be a directory that contains packages as wheel files (<code>.whl</code>) or source distributions (<code>.tar.gz</code> or <code>.zip</code>) at the top level.</p>
|
||||
|
||||
<p>If a URL, the page must contain a flat list of links to package files adhering to the formats described above.</p>
|
||||
|
||||
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
||||
|
||||
</dd><dt><code>--index-strategy</code> <i>index-strategy</i></dt><dd><p>The strategy to use when resolving against multiple index URLs.</p>
|
||||
|
||||
<p>By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (<code>first-match</code>). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
|
||||
|
||||
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
|
||||
|
||||
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
|
||||
</ul>
|
||||
</dd><dt><code>--index-url</code>, <code>-i</code> <i>index-url</i></dt><dd><p>The URL of the Python package index (by default: <https://pypi.org/simple>).</p>
|
||||
|
||||
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>
|
||||
|
||||
<p>The index given by this flag is given lower priority than all other indexes specified via the <code>--extra-index-url</code> flag.</p>
|
||||
|
||||
</dd><dt><code>--keyring-provider</code> <i>keyring-provider</i></dt><dd><p>Attempt to use <code>keyring</code> for authentication for index URLs.</p>
|
||||
|
||||
<p>At present, only <code>--keyring-provider subprocess</code> is supported, which configures uv to use the <code>keyring</code> CLI to handle authentication.</p>
|
||||
|
||||
<p>Defaults to <code>disabled</code>.</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>disabled</code>: Do not use keyring for credential lookup</li>
|
||||
|
||||
<li><code>subprocess</code>: Use the <code>keyring</code> command for credential lookup</li>
|
||||
</ul>
|
||||
</dd><dt><code>--link-mode</code> <i>link-mode</i></dt><dd><p>The method to use when installing packages from the global cache.</p>
|
||||
|
||||
<p>Defaults to <code>clone</code> (also known as Copy-on-Write) on macOS, and <code>hardlink</code> on Linux and Windows.</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>clone</code>: Clone (i.e., copy-on-write) packages from the wheel into the <code>site-packages</code> directory</li>
|
||||
|
||||
<li><code>copy</code>: Copy packages from the wheel into the <code>site-packages</code> directory</li>
|
||||
|
||||
<li><code>hardlink</code>: Hard link packages from the wheel into the <code>site-packages</code> directory</li>
|
||||
|
||||
<li><code>symlink</code>: Symbolically link packages from the wheel into the <code>site-packages</code> directory</li>
|
||||
</ul>
|
||||
</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform’s native certificate store.</p>
|
||||
|
||||
<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>
|
||||
|
||||
<p>However, in some cases, you may want to use the platform’s native certificate store, especially if you’re relying on a corporate trust root (e.g., for a mandatory proxy) that’s included in your system’s certificate store.</p>
|
||||
|
||||
</dd><dt><code>--no-binary</code></dt><dd><p>Don’t install pre-built wheels.</p>
|
||||
|
||||
<p>The given packages will be built and installed from source. The resolver will still use pre-built wheels to extract package metadata, if available.</p>
|
||||
|
||||
</dd><dt><code>--no-binary-package</code> <i>no-binary-package</i></dt><dd><p>Don’t install pre-built wheels for a specific package</p>
|
||||
|
||||
</dd><dt><code>--no-build</code></dt><dd><p>Don’t build source distributions.</p>
|
||||
|
||||
<p>When enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.</p>
|
||||
|
||||
</dd><dt><code>--no-build-isolation</code></dt><dd><p>Disable isolation when building source distributions.</p>
|
||||
|
||||
<p>Assumes that build dependencies specified by PEP 518 are already installed.</p>
|
||||
|
||||
</dd><dt><code>--no-build-isolation-package</code> <i>no-build-isolation-package</i></dt><dd><p>Disable isolation when building source distributions for a specific package.</p>
|
||||
|
||||
<p>Assumes that the packages’ build dependencies specified by PEP 518 are already installed.</p>
|
||||
|
||||
</dd><dt><code>--no-build-package</code> <i>no-build-package</i></dt><dd><p>Don’t build source distributions for a specific package</p>
|
||||
|
||||
</dd><dt><code>--no-cache</code>, <code>-n</code></dt><dd><p>Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation</p>
|
||||
|
||||
</dd><dt><code>--no-config</code></dt><dd><p>Avoid discovering configuration files (<code>pyproject.toml</code>, <code>uv.toml</code>).</p>
|
||||
|
||||
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
|
||||
|
||||
</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
|
||||
|
||||
</dd><dt><code>--no-progress</code></dt><dd><p>Hide all progress outputs.</p>
|
||||
|
||||
<p>For example, spinners or progress bars.</p>
|
||||
|
||||
</dd><dt><code>--no-sources</code></dt><dd><p>Ignore the <code>tool.uv.sources</code> table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources</p>
|
||||
|
||||
</dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p>
|
||||
|
||||
<p>When disabled, uv will only use locally cached data and locally available files.</p>
|
||||
|
||||
</dd><dt><code>--prerelease</code> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
|
||||
|
||||
<p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>disallow</code>: Disallow all pre-release versions</li>
|
||||
|
||||
<li><code>allow</code>: Allow all pre-release versions</li>
|
||||
|
||||
<li><code>if-necessary</code>: Allow pre-release versions if all versions of a package are pre-release</li>
|
||||
|
||||
<li><code>explicit</code>: Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements</li>
|
||||
|
||||
<li><code>if-necessary-or-explicit</code>: Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements</li>
|
||||
</ul>
|
||||
</dd><dt><code>--python-fetch</code> <i>python-fetch</i></dt><dd><p>Whether to automatically download Python when required</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>automatic</code>: Automatically fetch managed Python installations when needed</li>
|
||||
|
||||
<li><code>manual</code>: Do not automatically fetch managed Python installations; require explicit installation</li>
|
||||
</ul>
|
||||
</dd><dt><code>--python-preference</code> <i>python-preference</i></dt><dd><p>Whether to prefer uv-managed or system Python installations.</p>
|
||||
|
||||
<p>By default, uv prefers using Python versions it manages. However, it will use system Python installations if a uv-managed Python is not installed. This option allows prioritizing or ignoring system Python installations.</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>only-managed</code>: Only use managed Python installations; never use system Python installations</li>
|
||||
|
||||
<li><code>managed</code>: Prefer managed Python installations over system Python installations</li>
|
||||
|
||||
<li><code>system</code>: Prefer system Python installations over managed Python installations</li>
|
||||
|
||||
<li><code>only-system</code>: Only use system Python installations; never use managed Python installations</li>
|
||||
</ul>
|
||||
</dd><dt><code>--quiet</code>, <code>-q</code></dt><dd><p>Do not print any output</p>
|
||||
|
||||
</dd><dt><code>--refresh</code></dt><dd><p>Refresh all cached data</p>
|
||||
|
||||
</dd><dt><code>--refresh-package</code> <i>refresh-package</i></dt><dd><p>Refresh cached data for a specific package</p>
|
||||
|
||||
</dd><dt><code>--reinstall</code></dt><dd><p>Reinstall all packages, regardless of whether they’re already installed. Implies <code>--refresh</code></p>
|
||||
|
||||
</dd><dt><code>--reinstall-package</code> <i>reinstall-package</i></dt><dd><p>Reinstall a specific package, regardless of whether it’s already installed. Implies <code>--refresh-package</code></p>
|
||||
|
||||
</dd><dt><code>--resolution</code> <i>resolution</i></dt><dd><p>The strategy to use when selecting between the different compatible versions for a given package requirement.</p>
|
||||
|
||||
<p>By default, uv will use the latest compatible version of each package (<code>highest</code>).</p>
|
||||
|
||||
<p>Possible values:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>highest</code>: Resolve the highest compatible version of each package</li>
|
||||
|
||||
<li><code>lowest</code>: Resolve the lowest compatible version of each package</li>
|
||||
|
||||
<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
|
||||
</ul>
|
||||
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file</p>
|
||||
|
||||
</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file</p>
|
||||
|
||||
</dd><dt><code>--verbose</code>, <code>-v</code></dt><dd><p>Use verbose output.</p>
|
||||
|
||||
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)</p>
|
||||
|
||||
</dd><dt><code>--version</code>, <code>-V</code></dt><dd><p>Display the uv version</p>
|
||||
|
||||
</dd></dl>
|
||||
|
||||
### uv tool list
|
||||
|
||||
List installed tools
|
||||
|
|
|
|||
Loading…
Reference in New Issue