mirror of https://github.com/astral-sh/uv
Enforce latest-version in `@latest` requests (#17114)
## Summary
Given `uv tool install {name}@latest`, we make revalidation requests for
`{name}`, but we don't actually add a "latest" constraint when resolving
-- we just assume that since the package is unpinned, and we're fetching
the latest available versions, the resolver will select the latest
version.
However, imagine a package in which the latest version requires Python
3.13 or later, but prior versions support Python 3.9 and up. If we
happen to select Python 3.9 ahead of resolution, and the user requests
`{name}@latest`, we would backtrack to the non-latest version due to the
Python mismatch.
This PR modifies `uv tool install` and `uv tool run` to first determine
the latest version, then provide it as a constraint when resolving.
This commit is contained in:
parent
ed37f3b432
commit
e77ee15204
|
|
@ -12,7 +12,7 @@ use uv_warnings::warn_user_once;
|
|||
/// A client to fetch the latest version of a package from an index.
|
||||
///
|
||||
/// The returned distribution is guaranteed to be compatible with the provided tags and Python
|
||||
/// requirement.
|
||||
/// requirement (if specified).
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LatestClient<'env> {
|
||||
pub(crate) client: &'env RegistryClient,
|
||||
|
|
@ -20,7 +20,7 @@ pub(crate) struct LatestClient<'env> {
|
|||
pub(crate) prerelease: PrereleaseMode,
|
||||
pub(crate) exclude_newer: &'env ExcludeNewer,
|
||||
pub(crate) tags: Option<&'env Tags>,
|
||||
pub(crate) requires_python: &'env RequiresPython,
|
||||
pub(crate) requires_python: Option<&'env RequiresPython>,
|
||||
}
|
||||
|
||||
impl LatestClient<'_> {
|
||||
|
|
@ -30,7 +30,7 @@ impl LatestClient<'_> {
|
|||
package: &PackageName,
|
||||
index: Option<&IndexUrl>,
|
||||
download_concurrency: &Semaphore,
|
||||
) -> anyhow::Result<Option<DistFilename>, uv_client::Error> {
|
||||
) -> Result<Option<DistFilename>, uv_client::Error> {
|
||||
debug!("Fetching latest version of: `{package}`");
|
||||
|
||||
let archives = match self
|
||||
|
|
@ -101,15 +101,17 @@ impl LatestClient<'_> {
|
|||
}
|
||||
|
||||
// Skip distributions that are incompatible with the Python requirement.
|
||||
if let Some(requires_python) = self.requires_python {
|
||||
if file
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.is_some_and(|requires_python| {
|
||||
!self.requires_python.is_contained_by(requires_python)
|
||||
.is_some_and(|file_requires_python| {
|
||||
!requires_python.is_contained_by(file_requires_python)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip distributions that are incompatible with the current platform.
|
||||
if let DistFilename::WheelFilename(filename) = &filename {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ pub(crate) async fn pip_list(
|
|||
prerelease,
|
||||
exclude_newer: &exclude_newer,
|
||||
tags: Some(tags),
|
||||
requires_python: &requires_python,
|
||||
requires_python: Some(&requires_python),
|
||||
};
|
||||
|
||||
let reporter = LatestVersionReporter::from(printer).with_length(results.len() as u64);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ pub(crate) async fn pip_tree(
|
|||
prerelease,
|
||||
exclude_newer: &exclude_newer,
|
||||
tags: Some(tags),
|
||||
requires_python: &requires_python,
|
||||
requires_python: Some(&requires_python),
|
||||
};
|
||||
|
||||
let reporter = LatestVersionReporter::from(printer).with_length(packages.len() as u64);
|
||||
|
|
|
|||
|
|
@ -219,6 +219,9 @@ pub(crate) enum ProjectError {
|
|||
#[error(transparent)]
|
||||
DependencyGroup(#[from] DependencyGroupError),
|
||||
|
||||
#[error(transparent)]
|
||||
Client(#[from] uv_client::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] uv_python::Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ pub(crate) async fn tree(
|
|||
capabilities: &capabilities,
|
||||
prerelease: lock.prerelease_mode(),
|
||||
exclude_newer: &lock.exclude_newer(),
|
||||
requires_python: lock.requires_python(),
|
||||
requires_python: Some(lock.requires_python()),
|
||||
tags: None,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ use std::str::FromStr;
|
|||
|
||||
use anyhow::{Result, bail};
|
||||
use owo_colors::OwoColorize;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use uv_cache::{Cache, Refresh};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||
use uv_configuration::{Concurrency, Constraints, DryRun, Reinstall, TargetTriple, Upgrade};
|
||||
use uv_distribution::LoweredExtraBuildDependencies;
|
||||
use uv_distribution_types::{
|
||||
ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource,
|
||||
UnresolvedRequirementSpecification,
|
||||
ExtraBuildRequires, IndexCapabilities, NameRequirementSpecification, Requirement,
|
||||
RequirementSource, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||
use uv_normalize::PackageName;
|
||||
|
|
@ -29,6 +30,7 @@ use uv_warnings::warn_user;
|
|||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
||||
use crate::commands::pip::operations::{self, Modifications};
|
||||
use crate::commands::pip::{resolution_markers, resolution_tags};
|
||||
|
|
@ -224,6 +226,62 @@ pub(crate) async fn install(
|
|||
}
|
||||
};
|
||||
|
||||
// For `@latest`, fetch the latest version and create a constraint.
|
||||
let latest = if let ToolRequest::Package {
|
||||
target: Target::Latest(_, name, _),
|
||||
..
|
||||
} = &request
|
||||
{
|
||||
// Build the registry client to fetch the latest version.
|
||||
let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone())
|
||||
.index_locations(settings.resolver.index_locations.clone())
|
||||
.index_strategy(settings.resolver.index_strategy)
|
||||
.markers(interpreter.markers())
|
||||
.platform(interpreter.platform())
|
||||
.build();
|
||||
|
||||
// Initialize the capabilities.
|
||||
let capabilities = IndexCapabilities::default();
|
||||
let download_concurrency = Semaphore::new(concurrency.downloads);
|
||||
|
||||
// Initialize the client to fetch the latest version.
|
||||
let latest_client = LatestClient {
|
||||
client: &client,
|
||||
capabilities: &capabilities,
|
||||
prerelease: settings.resolver.prerelease,
|
||||
exclude_newer: &settings.resolver.exclude_newer,
|
||||
tags: None,
|
||||
requires_python: None,
|
||||
};
|
||||
|
||||
// Fetch the latest version.
|
||||
if let Some(dist_filename) = latest_client
|
||||
.find_latest(name, None, &download_concurrency)
|
||||
.await?
|
||||
{
|
||||
let version = dist_filename.version().clone();
|
||||
debug!("Resolved `{name}@latest` to `{name}=={version}`");
|
||||
|
||||
// The constraint pins the version during resolution to prevent backtracking.
|
||||
Some(Requirement {
|
||||
name: name.clone(),
|
||||
extras: vec![].into_boxed_slice(),
|
||||
groups: Box::new([]),
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(version)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let package_name = &requirement.name;
|
||||
|
||||
// If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable.
|
||||
|
|
@ -284,11 +342,11 @@ pub(crate) async fn install(
|
|||
};
|
||||
|
||||
// Resolve the constraints.
|
||||
let constraints = spec
|
||||
let constraints: Vec<_> = spec
|
||||
.constraints
|
||||
.into_iter()
|
||||
.map(|constraint| constraint.requirement)
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
// Resolve the overrides.
|
||||
let overrides = resolve_names(
|
||||
|
|
@ -412,7 +470,7 @@ pub(crate) async fn install(
|
|||
if matches!(
|
||||
site_packages.satisfies_requirements(
|
||||
requirements.iter(),
|
||||
constraints.iter(),
|
||||
constraints.iter().chain(latest.iter()),
|
||||
overrides.iter(),
|
||||
InstallationStrategy::Permissive,
|
||||
&markers,
|
||||
|
|
@ -454,6 +512,7 @@ pub(crate) async fn install(
|
|||
constraints: constraints
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(latest.into_iter())
|
||||
.map(NameRequirementSpecification::from)
|
||||
.collect(),
|
||||
overrides: overrides
|
||||
|
|
|
|||
|
|
@ -10,20 +10,21 @@ use console::Term;
|
|||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use uv_cache::{Cache, Refresh};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_cli::ExternalCommand;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||
use uv_configuration::Concurrency;
|
||||
use uv_configuration::Constraints;
|
||||
use uv_configuration::TargetTriple;
|
||||
use uv_distribution::LoweredExtraBuildDependencies;
|
||||
use uv_distribution_types::InstalledDist;
|
||||
use uv_distribution_types::{
|
||||
IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource,
|
||||
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
IndexCapabilities, IndexUrl, Name, NameRequirementSpecification, Requirement,
|
||||
RequirementSource, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||
|
|
@ -47,6 +48,7 @@ use uv_workspace::WorkspaceCache;
|
|||
use crate::child::run_to_completion;
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::pip;
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
use crate::commands::pip::loggers::{
|
||||
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
|
||||
};
|
||||
|
|
@ -905,6 +907,62 @@ async fn get_or_create_environment(
|
|||
}
|
||||
};
|
||||
|
||||
// For `@latest`, fetch the latest version and create a constraint.
|
||||
let latest = if let ToolRequest::Package {
|
||||
target: Target::Latest(_, name, _),
|
||||
..
|
||||
} = &request
|
||||
{
|
||||
// Build the registry client to fetch the latest version.
|
||||
let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone())
|
||||
.index_locations(settings.resolver.index_locations.clone())
|
||||
.index_strategy(settings.resolver.index_strategy)
|
||||
.markers(interpreter.markers())
|
||||
.platform(interpreter.platform())
|
||||
.build();
|
||||
|
||||
// Initialize the capabilities.
|
||||
let capabilities = IndexCapabilities::default();
|
||||
let download_concurrency = Semaphore::new(concurrency.downloads);
|
||||
|
||||
// Initialize the client to fetch the latest version.
|
||||
let latest_client = LatestClient {
|
||||
client: &client,
|
||||
capabilities: &capabilities,
|
||||
prerelease: settings.resolver.prerelease,
|
||||
exclude_newer: &settings.resolver.exclude_newer,
|
||||
tags: None,
|
||||
requires_python: None,
|
||||
};
|
||||
|
||||
// Fetch the latest version.
|
||||
if let Some(dist_filename) = latest_client
|
||||
.find_latest(name, None, &download_concurrency)
|
||||
.await?
|
||||
{
|
||||
let version = dist_filename.version().clone();
|
||||
debug!("Resolved `{name}@latest` to `{name}=={version}`");
|
||||
|
||||
// The constraint pins the version during resolution to prevent backtracking.
|
||||
Some(Requirement {
|
||||
name: name.clone(),
|
||||
extras: vec![].into_boxed_slice(),
|
||||
groups: Box::new([]),
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(version)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Read the `--with` requirements.
|
||||
let spec = RequirementsSpecification::from_sources(
|
||||
with,
|
||||
|
|
@ -1016,7 +1074,7 @@ async fn get_or_create_environment(
|
|||
if matches!(
|
||||
site_packages.satisfies_requirements(
|
||||
requirements.iter(),
|
||||
constraints.iter(),
|
||||
constraints.iter().chain(latest.iter()),
|
||||
overrides.iter(),
|
||||
InstallationStrategy::Permissive,
|
||||
&markers,
|
||||
|
|
@ -1044,6 +1102,7 @@ async fn get_or_create_environment(
|
|||
.collect(),
|
||||
constraints: constraints
|
||||
.into_iter()
|
||||
.chain(latest.into_iter())
|
||||
.map(NameRequirementSpecification::from)
|
||||
.collect(),
|
||||
overrides: overrides
|
||||
|
|
|
|||
Loading…
Reference in New Issue