mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 02:48:17 +00:00
Refactor incompatiblity tracking for distributions (#1298)
Extends the "compatibility" types introduced in #1293 to apply to source distributions as well as wheels. - We now track the most-relevant incompatible source distribution - Exclude newer, Python requirements, and yanked versions are all tracked as incompatibilities in the new model (this lets us remove `DistMetadata`!)
This commit is contained in:
parent
1181aa9be4
commit
10c4effbd3
16 changed files with 498 additions and 343 deletions
|
@ -1,7 +1,7 @@
|
|||
use pubgrub::range::Range;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use distribution_types::CompatibleDist;
|
||||
use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
|
||||
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
|
||||
|
@ -192,7 +192,7 @@ impl CandidateSelector {
|
|||
for (version, maybe_dist) in versions {
|
||||
steps += 1;
|
||||
|
||||
let dist = if version.any_prerelease() {
|
||||
let candidate = if version.any_prerelease() {
|
||||
if range.contains(version) {
|
||||
match allow_prerelease {
|
||||
AllowPreRelease::Yes => {
|
||||
|
@ -209,7 +209,7 @@ impl CandidateSelector {
|
|||
);
|
||||
// If pre-releases are allowed, treat them equivalently
|
||||
// to stable distributions.
|
||||
dist
|
||||
Candidate::new(package_name, version, dist)
|
||||
}
|
||||
AllowPreRelease::IfNecessary => {
|
||||
let Some(dist) = maybe_dist.prioritized_dist() else {
|
||||
|
@ -235,7 +235,7 @@ impl CandidateSelector {
|
|||
// current range.
|
||||
prerelease = Some(PreReleaseCandidate::NotNecessary);
|
||||
|
||||
// Always return the first-matching stable distribution.
|
||||
// Return the first-matching stable distribution.
|
||||
if range.contains(version) {
|
||||
let Some(dist) = maybe_dist.prioritized_dist() else {
|
||||
continue;
|
||||
|
@ -248,18 +248,29 @@ impl CandidateSelector {
|
|||
steps,
|
||||
version,
|
||||
);
|
||||
dist
|
||||
Candidate::new(package_name, version, dist)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Skip empty candidates due to exclude newer
|
||||
if dist.exclude_newer() && dist.incompatible_wheel().is_none() && dist.get().is_none() {
|
||||
// If candidate is not compatible due to exclude newer, continue searching.
|
||||
// This is a special case — we pretend versions with exclude newer incompatibilities
|
||||
// do not exist so that they are not present in error messages in our test suite.
|
||||
// TODO(zanieb): Now that `--exclude-newer` is user facing we may want to consider
|
||||
// flagging this behavior such that we _will_ report filtered distributions due to
|
||||
// exclude-newer in our error messages.
|
||||
if matches!(
|
||||
candidate.dist(),
|
||||
CandidateDist::Incompatible(
|
||||
IncompatibleDist::Source(IncompatibleSource::ExcludeNewer(_))
|
||||
| IncompatibleDist::Wheel(IncompatibleWheel::ExcludeNewer(_))
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some(Candidate::new(package_name, version, dist));
|
||||
return Some(candidate);
|
||||
}
|
||||
tracing::trace!(
|
||||
"exhausted all candidates for package {:?} with range {:?} \
|
||||
|
@ -281,23 +292,26 @@ impl CandidateSelector {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum CandidateDist<'a> {
|
||||
Compatible(CompatibleDist<'a>),
|
||||
Incompatible(Option<&'a IncompatibleWheel>),
|
||||
ExcludeNewer,
|
||||
Incompatible(IncompatibleDist),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PrioritizedDist> for CandidateDist<'a> {
|
||||
fn from(value: &'a PrioritizedDist) -> Self {
|
||||
if let Some(dist) = value.get() {
|
||||
CandidateDist::Compatible(dist)
|
||||
} else if value.exclude_newer() && value.incompatible_wheel().is_none() {
|
||||
// If empty because of exclude-newer, mark as a special case
|
||||
CandidateDist::ExcludeNewer
|
||||
} else {
|
||||
CandidateDist::Incompatible(
|
||||
value
|
||||
.incompatible_wheel()
|
||||
.map(|(_, incompatibility)| incompatibility),
|
||||
)
|
||||
// TODO(zanieb)
|
||||
// We always return the source distribution (if one exists) instead of the wheel
|
||||
// but in the future we may want to return both so the resolver can explain
|
||||
// why neither distribution kind can be used.
|
||||
let dist = if let Some((_, incompatibility)) = value.incompatible_source() {
|
||||
IncompatibleDist::Source(incompatibility.clone())
|
||||
} else if let Some((_, incompatibility)) = value.incompatible_wheel() {
|
||||
IncompatibleDist::Wheel(incompatibility.clone())
|
||||
} else {
|
||||
IncompatibleDist::Unavailable
|
||||
};
|
||||
CandidateDist::Incompatible(dist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use anyhow::Result;
|
||||
use futures::{stream, Stream, StreamExt, TryStreamExt};
|
||||
use rustc_hash::FxHashMap;
|
||||
use uv_traits::NoBinary;
|
||||
use uv_traits::{NoBinary, NoBuild};
|
||||
|
||||
use distribution_filename::DistFilename;
|
||||
use distribution_types::{Dist, IndexUrl, Resolution};
|
||||
|
@ -26,6 +26,7 @@ pub struct DistFinder<'a> {
|
|||
interpreter: &'a Interpreter,
|
||||
flat_index: &'a FlatIndex,
|
||||
no_binary: &'a NoBinary,
|
||||
no_build: &'a NoBuild,
|
||||
}
|
||||
|
||||
impl<'a> DistFinder<'a> {
|
||||
|
@ -36,6 +37,7 @@ impl<'a> DistFinder<'a> {
|
|||
interpreter: &'a Interpreter,
|
||||
flat_index: &'a FlatIndex,
|
||||
no_binary: &'a NoBinary,
|
||||
no_build: &'a NoBuild,
|
||||
) -> Self {
|
||||
Self {
|
||||
tags,
|
||||
|
@ -44,6 +46,7 @@ impl<'a> DistFinder<'a> {
|
|||
interpreter,
|
||||
flat_index,
|
||||
no_binary,
|
||||
no_build,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,6 +138,11 @@ impl<'a> DistFinder<'a> {
|
|||
NoBinary::All => true,
|
||||
NoBinary::Packages(packages) => packages.contains(&requirement.name),
|
||||
};
|
||||
let no_build = match self.no_build {
|
||||
NoBuild::None => false,
|
||||
NoBuild::All => true,
|
||||
NoBuild::Packages(packages) => packages.contains(&requirement.name),
|
||||
};
|
||||
|
||||
// Prioritize the flat index by initializing the "best" matches with its entries.
|
||||
let matching_override = if let Some(flat_index) = flat_index {
|
||||
|
@ -154,8 +162,10 @@ impl<'a> DistFinder<'a> {
|
|||
Some(version.clone()),
|
||||
resolvable_dist
|
||||
.compatible_wheel()
|
||||
.map(|(dist, tag_priority)| (dist.dist.clone(), tag_priority)),
|
||||
resolvable_dist.source().map(|dist| dist.dist.clone()),
|
||||
.map(|(dist, tag_priority)| (dist.clone(), tag_priority)),
|
||||
resolvable_dist
|
||||
.compatible_source()
|
||||
.map(std::clone::Clone::clone),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
|
@ -213,7 +223,7 @@ impl<'a> DistFinder<'a> {
|
|||
}
|
||||
|
||||
// Find the most-compatible sdist, if no wheel was found.
|
||||
if best_wheel.is_none() {
|
||||
if !no_build && best_wheel.is_none() {
|
||||
for version_sdist in files.source_dists {
|
||||
// Only add dists compatible with the python version.
|
||||
// This is relevant for source dists which give no other indication of their
|
||||
|
|
|
@ -15,10 +15,10 @@ pub(crate) struct FilePins(FxHashMap<PackageName, FxHashMap<pep440_rs::Version,
|
|||
impl FilePins {
|
||||
/// Pin a candidate package.
|
||||
pub(crate) fn insert(&mut self, candidate: &Candidate, dist: &CompatibleDist) {
|
||||
self.0.entry(candidate.name().clone()).or_default().insert(
|
||||
candidate.version().clone(),
|
||||
dist.for_installation().dist.clone(),
|
||||
);
|
||||
self.0
|
||||
.entry(candidate.name().clone())
|
||||
.or_default()
|
||||
.insert(candidate.version().clone(), dist.for_installation().clone());
|
||||
}
|
||||
|
||||
/// Return the pinned file for the given package name and version, if it exists.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use distribution_types::{CompatibleDist, Dist};
|
||||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use uv_interpreter::Interpreter;
|
||||
|
||||
|
@ -30,42 +29,4 @@ impl PythonRequirement {
|
|||
pub fn target(&self) -> &Version {
|
||||
&self.target
|
||||
}
|
||||
|
||||
/// If the dist doesn't match the given Python requirement, return the version specifiers.
|
||||
pub(crate) fn validate_dist<'a>(
|
||||
&self,
|
||||
dist: &'a CompatibleDist,
|
||||
) -> Option<&'a VersionSpecifiers> {
|
||||
// Validate the _installed_ file.
|
||||
let requires_python = dist.for_installation().requires_python.as_ref()?;
|
||||
|
||||
// If the dist doesn't support the target Python version, return the failing version
|
||||
// specifiers.
|
||||
if !requires_python.contains(self.target()) {
|
||||
return Some(requires_python);
|
||||
}
|
||||
|
||||
// If the dist is a source distribution, and doesn't support the installed Python
|
||||
// version, return the failing version specifiers, since we won't be able to build it.
|
||||
if matches!(dist.for_installation().dist, Dist::Source(_))
|
||||
&& !requires_python.contains(self.installed())
|
||||
{
|
||||
return Some(requires_python);
|
||||
}
|
||||
|
||||
// Validate the resolved file.
|
||||
let requires_python = dist.for_resolution().requires_python.as_ref()?;
|
||||
|
||||
// If the dist is a source distribution, and doesn't support the installed Python
|
||||
// version, return the failing version specifiers, since we won't be able to build it.
|
||||
// This isn't strictly necessary, since if `dist.resolve_metadata()` is a source distribution, it
|
||||
// should be the same file as `dist.install_metadata()` (validated above).
|
||||
if matches!(dist.for_resolution().dist, Dist::Source(_))
|
||||
&& !requires_python.contains(self.installed())
|
||||
{
|
||||
return Some(requires_python);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ use pubgrub::solver::{Incompatibility, State};
|
|||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use tokio::select;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tracing::{debug, info_span, instrument, trace, warn, Instrument};
|
||||
use tracing::{debug, info_span, instrument, trace, Instrument};
|
||||
use url::Url;
|
||||
|
||||
use distribution_filename::WheelFilename;
|
||||
use distribution_types::{
|
||||
BuiltDist, Dist, DistributionMetadata, IncompatibleWheel, Name, RemoteSource, SourceDist,
|
||||
VersionOrUrl,
|
||||
BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel,
|
||||
Name, RemoteSource, SourceDist, VersionOrUrl,
|
||||
};
|
||||
use pep440_rs::{Version, VersionSpecifiers, MIN_VERSION};
|
||||
use pep440_rs::{Version, MIN_VERSION};
|
||||
use pep508_rs::{MarkerEnvironment, Requirement};
|
||||
use platform_tags::{IncompatibleTag, Tags};
|
||||
use pypi_types::{Metadata23, Yanked};
|
||||
|
@ -53,6 +53,7 @@ pub use crate::resolver::provider::{
|
|||
};
|
||||
use crate::resolver::reporter::Facade;
|
||||
pub use crate::resolver::reporter::{BuildId, Reporter};
|
||||
|
||||
use crate::yanks::AllowedYanks;
|
||||
use crate::{DependencyMode, Options};
|
||||
|
||||
|
@ -65,12 +66,8 @@ mod urls;
|
|||
/// Unlike [`PackageUnavailable`] this applies to a single version of the package
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnavailableVersion {
|
||||
/// Version is incompatible due to the `Requires-Python` version specifiers for that package.
|
||||
RequiresPython(VersionSpecifiers),
|
||||
/// Version is incompatible because it is yanked
|
||||
Yanked(Yanked),
|
||||
/// Version is incompatible because it has no usable distributions
|
||||
NoDistributions(Option<IncompatibleWheel>),
|
||||
IncompatibleDist(IncompatibleDist),
|
||||
}
|
||||
|
||||
/// The package is unavailable and cannot be used
|
||||
|
@ -97,7 +94,6 @@ pub struct Resolver<'a, Provider: ResolverProvider> {
|
|||
constraints: Constraints,
|
||||
overrides: Overrides,
|
||||
editables: Editables,
|
||||
allowed_yanks: AllowedYanks,
|
||||
urls: Urls,
|
||||
dependency_mode: DependencyMode,
|
||||
markers: &'a MarkerEnvironment,
|
||||
|
@ -134,8 +130,10 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, DefaultResolverProvid
|
|||
flat_index,
|
||||
tags,
|
||||
PythonRequirement::new(interpreter, markers),
|
||||
AllowedYanks::from_manifest(&manifest, markers),
|
||||
options.exclude_newer,
|
||||
build_context.no_binary(),
|
||||
build_context.no_build(),
|
||||
);
|
||||
Self::new_custom_io(
|
||||
manifest,
|
||||
|
@ -163,7 +161,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
unavailable_packages: DashMap::default(),
|
||||
visited: DashSet::default(),
|
||||
selector: CandidateSelector::for_resolution(options, &manifest, markers),
|
||||
allowed_yanks: AllowedYanks::from_manifest(&manifest, markers),
|
||||
dependency_mode: options.dependency_mode,
|
||||
urls: Urls::from_manifest(&manifest, markers)?,
|
||||
project: manifest.project,
|
||||
|
@ -341,10 +338,13 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
ResolverVersion::Available(version) => version,
|
||||
ResolverVersion::Unavailable(version, unavailable) => {
|
||||
let reason = match unavailable {
|
||||
UnavailableVersion::RequiresPython(requires_python) => {
|
||||
// Incompatible requires-python versions are special in that we track
|
||||
// them as incompatible dependencies instead of marking the package version
|
||||
// as unavailable directly
|
||||
// Incompatible requires-python versions are special in that we track
|
||||
// them as incompatible dependencies instead of marking the package version
|
||||
// as unavailable directly
|
||||
UnavailableVersion::IncompatibleDist(
|
||||
IncompatibleDist::Source(IncompatibleSource::RequiresPython(requires_python))
|
||||
| IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(requires_python))
|
||||
) => {
|
||||
let python_version = requires_python
|
||||
.iter()
|
||||
.map(PubGrubSpecifier::try_from)
|
||||
|
@ -363,30 +363,51 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
state.partial_solution.add_decision(next.clone(), version);
|
||||
continue;
|
||||
}
|
||||
UnavailableVersion::Yanked(yanked) => match yanked {
|
||||
Yanked::Bool(_) => "it was yanked".to_string(),
|
||||
Yanked::Reason(reason) => format!(
|
||||
"it was yanked (reason: {})",
|
||||
reason.trim().trim_end_matches('.')
|
||||
),
|
||||
},
|
||||
UnavailableVersion::NoDistributions(best_incompatible) => {
|
||||
if let Some(best_incompatible) = best_incompatible {
|
||||
match best_incompatible {
|
||||
IncompatibleWheel::NoBinary => "no source distribution is available and using wheels is disabled".to_string(),
|
||||
IncompatibleWheel::RequiresPython => "no wheels are available that meet your required Python version".to_string(),
|
||||
IncompatibleWheel::Tag(tag) => {
|
||||
match tag {
|
||||
IncompatibleTag::Invalid => "no wheels are available with valid tags".to_string(),
|
||||
IncompatibleTag::Python => "no wheels are available with a matching Python implementation".to_string(),
|
||||
IncompatibleTag::Abi => "no wheels are available with a matching Python ABI".to_string(),
|
||||
IncompatibleTag::Platform => "no wheels are available with a matching platform".to_string(),
|
||||
UnavailableVersion::IncompatibleDist(incompatibility) => {
|
||||
match incompatibility {
|
||||
IncompatibleDist::Wheel(incompatibility) => {
|
||||
match incompatibility {
|
||||
IncompatibleWheel::NoBinary => "no source distribution is available and using wheels is disabled".to_string(),
|
||||
IncompatibleWheel::Tag(tag) => {
|
||||
match tag {
|
||||
IncompatibleTag::Invalid => "no wheels are available with valid tags".to_string(),
|
||||
IncompatibleTag::Python => "no wheels are available with a matching Python implementation".to_string(),
|
||||
IncompatibleTag::Abi => "no wheels are available with a matching Python ABI".to_string(),
|
||||
IncompatibleTag::Platform => "no wheels are available with a matching platform".to_string(),
|
||||
}
|
||||
}
|
||||
IncompatibleWheel::Yanked(yanked) => match yanked {
|
||||
Yanked::Bool(_) => "it was yanked".to_string(),
|
||||
Yanked::Reason(reason) => format!(
|
||||
"it was yanked (reason: {})",
|
||||
reason.trim().trim_end_matches('.')
|
||||
),
|
||||
},
|
||||
IncompatibleWheel::ExcludeNewer(ts) => match ts {
|
||||
Some(_) => "it was published after the exclude newer time".to_string(),
|
||||
None => "it has no publish time".to_string()
|
||||
}
|
||||
IncompatibleWheel::RequiresPython(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO(zanieb): It's unclear why we would encounter this case still
|
||||
"no wheels are available for your system".to_string()
|
||||
IncompatibleDist::Source(incompatibility) => {
|
||||
match incompatibility {
|
||||
IncompatibleSource::NoBuild => "no wheels are usable and building from source is disabled".to_string(),
|
||||
IncompatibleSource::Yanked(yanked) => match yanked {
|
||||
Yanked::Bool(_) => "it was yanked".to_string(),
|
||||
Yanked::Reason(reason) => format!(
|
||||
"it was yanked (reason: {})",
|
||||
reason.trim().trim_end_matches('.')
|
||||
),
|
||||
},
|
||||
IncompatibleSource::ExcludeNewer(ts) => match ts {
|
||||
Some(_) => "it was published after the exclude newer time".to_string(),
|
||||
None => "it has no publish time".to_string()
|
||||
}
|
||||
IncompatibleSource::RequiresPython(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
IncompatibleDist::Unavailable => "no distributions are available".to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -521,7 +542,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
#[instrument(skip_all, fields(%package))]
|
||||
async fn choose_version(
|
||||
&self,
|
||||
package: &PubGrubPackage,
|
||||
package: &'a PubGrubPackage,
|
||||
range: &Range<Version>,
|
||||
pins: &mut FilePins,
|
||||
request_sink: &tokio::sync::mpsc::Sender<Request>,
|
||||
|
@ -644,42 +665,15 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
|
||||
let dist = match candidate.dist() {
|
||||
CandidateDist::Compatible(dist) => dist,
|
||||
CandidateDist::ExcludeNewer => {
|
||||
// If the version is incompatible because of `exclude_newer`, pretend the versions do not exist
|
||||
return Ok(None);
|
||||
}
|
||||
CandidateDist::Incompatible(incompatibility) => {
|
||||
// If the version is incompatible because no distributions match, exit early.
|
||||
// If the version is incompatible because no distributions are compatible, exit early.
|
||||
return Ok(Some(ResolverVersion::Unavailable(
|
||||
candidate.version().clone(),
|
||||
UnavailableVersion::NoDistributions(incompatibility.cloned()),
|
||||
UnavailableVersion::IncompatibleDist(incompatibility.clone()),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// If the version is incompatible because it was yanked, exit early.
|
||||
if dist.yanked().is_yanked() {
|
||||
if self
|
||||
.allowed_yanks
|
||||
.allowed(package_name, candidate.version())
|
||||
{
|
||||
warn!("Allowing yanked version: {}", candidate.package_id());
|
||||
} else {
|
||||
return Ok(Some(ResolverVersion::Unavailable(
|
||||
candidate.version().clone(),
|
||||
UnavailableVersion::Yanked(dist.yanked().clone()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// If the version is incompatible because of its Python requirement
|
||||
if let Some(requires_python) = self.python_requirement.validate_dist(dist) {
|
||||
return Ok(Some(ResolverVersion::Unavailable(
|
||||
candidate.version().clone(),
|
||||
UnavailableVersion::RequiresPython(requires_python.clone()),
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(extra) = extra {
|
||||
debug!(
|
||||
"Selecting: {}[{}]=={} ({})",
|
||||
|
@ -687,7 +681,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
extra,
|
||||
candidate.version(),
|
||||
dist.for_resolution()
|
||||
.dist
|
||||
.filename()
|
||||
.unwrap_or(Cow::Borrowed("unknown filename"))
|
||||
);
|
||||
|
@ -697,7 +690,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
candidate.name(),
|
||||
candidate.version(),
|
||||
dist.for_resolution()
|
||||
.dist
|
||||
.filename()
|
||||
.unwrap_or(Cow::Borrowed("unknown filename"))
|
||||
);
|
||||
|
@ -711,7 +703,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
|
||||
// Emit a request to fetch the metadata for this version.
|
||||
if self.index.distributions.register(candidate.package_id()) {
|
||||
let dist = dist.for_resolution().dist.clone();
|
||||
let dist = dist.for_resolution().clone();
|
||||
request_sink.send(Request::Dist(dist)).await?;
|
||||
}
|
||||
|
||||
|
@ -1023,7 +1015,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
};
|
||||
|
||||
// Try to find a compatible version. If there aren't any compatible versions,
|
||||
// short-circuit and return `None`.
|
||||
// short-circuit.
|
||||
let Some(candidate) = self.selector.select(&package_name, &range, version_map)
|
||||
else {
|
||||
return Ok(None);
|
||||
|
@ -1034,14 +1026,9 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
return Ok(None);
|
||||
};
|
||||
|
||||
// If the Python version is incompatible, short-circuit.
|
||||
if self.python_requirement.validate_dist(dist).is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Emit a request to fetch the metadata for this version.
|
||||
if self.index.distributions.register(candidate.package_id()) {
|
||||
let dist = dist.for_resolution().dist.clone();
|
||||
let dist = dist.for_resolution().clone();
|
||||
|
||||
let (metadata, precise) = self
|
||||
.provider
|
||||
|
|
|
@ -10,10 +10,11 @@ use pypi_types::Metadata23;
|
|||
use uv_client::{FlatIndex, RegistryClient};
|
||||
use uv_distribution::DistributionDatabase;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_traits::{BuildContext, NoBinary};
|
||||
use uv_traits::{BuildContext, NoBinary, NoBuild};
|
||||
|
||||
use crate::python_requirement::PythonRequirement;
|
||||
use crate::version_map::VersionMap;
|
||||
use crate::yanks::AllowedYanks;
|
||||
|
||||
pub type PackageVersionsResult = Result<VersionsResponse, uv_client::Error>;
|
||||
pub type WheelMetadataResult = Result<(Metadata23, Option<Url>), uv_distribution::Error>;
|
||||
|
@ -66,8 +67,10 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext + Send + Sync> {
|
|||
flat_index: FlatIndex,
|
||||
tags: Tags,
|
||||
python_requirement: PythonRequirement,
|
||||
allowed_yanks: AllowedYanks,
|
||||
exclude_newer: Option<DateTime<Utc>>,
|
||||
no_binary: NoBinary,
|
||||
no_build: NoBuild,
|
||||
}
|
||||
|
||||
impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Context> {
|
||||
|
@ -79,8 +82,10 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
|
|||
flat_index: &'a FlatIndex,
|
||||
tags: &'a Tags,
|
||||
python_requirement: PythonRequirement,
|
||||
allowed_yanks: AllowedYanks,
|
||||
exclude_newer: Option<DateTime<Utc>>,
|
||||
no_binary: &'a NoBinary,
|
||||
no_build: &'a NoBuild,
|
||||
) -> Self {
|
||||
Self {
|
||||
fetcher,
|
||||
|
@ -88,8 +93,10 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
|
|||
flat_index: flat_index.clone(),
|
||||
tags: tags.clone(),
|
||||
python_requirement,
|
||||
allowed_yanks,
|
||||
exclude_newer,
|
||||
no_binary: no_binary.clone(),
|
||||
no_build: no_build.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,9 +120,11 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
|
|||
&index,
|
||||
&self.tags,
|
||||
&self.python_requirement,
|
||||
&self.allowed_yanks,
|
||||
self.exclude_newer.as_ref(),
|
||||
self.flat_index.get(package_name).cloned(),
|
||||
&self.no_binary,
|
||||
&self.no_build,
|
||||
))),
|
||||
Err(err) => match err.into_kind() {
|
||||
uv_client::ErrorKind::PackageNotFound(_) => {
|
||||
|
|
|
@ -2,20 +2,24 @@ use std::collections::btree_map::{BTreeMap, Entry};
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use distribution_filename::DistFilename;
|
||||
use distribution_types::{Dist, IncompatibleWheel, IndexUrl, PrioritizedDist, WheelCompatibility};
|
||||
use pep440_rs::Version;
|
||||
use distribution_filename::{DistFilename, WheelFilename};
|
||||
use distribution_types::{
|
||||
Dist, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
|
||||
SourceDistCompatibility, WheelCompatibility,
|
||||
};
|
||||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use platform_tags::Tags;
|
||||
use pypi_types::Hashes;
|
||||
use pypi_types::{Hashes, Yanked};
|
||||
use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize};
|
||||
use uv_client::{FlatDistributions, OwnedArchive, SimpleMetadata, VersionFiles};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_traits::NoBinary;
|
||||
use uv_traits::{NoBinary, NoBuild};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::python_requirement::PythonRequirement;
|
||||
use crate::{python_requirement::PythonRequirement, yanks::AllowedYanks};
|
||||
|
||||
/// A map from versions to distributions.
|
||||
#[derive(Debug)]
|
||||
|
@ -25,6 +29,14 @@ pub struct VersionMap {
|
|||
|
||||
impl VersionMap {
|
||||
/// Initialize a [`VersionMap`] from the given metadata.
|
||||
///
|
||||
/// Note it is possible for files to have a different yank status per PEP 592 but in the official
|
||||
/// PyPI warehouse this cannot happen.
|
||||
///
|
||||
/// Here, we track if each file is yanked separately. If a release is partially yanked, the
|
||||
/// unyanked distributions _can_ be used.
|
||||
///
|
||||
/// PEP 592: <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
|
||||
#[instrument(skip_all, fields(package_name))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_metadata(
|
||||
|
@ -33,9 +45,11 @@ impl VersionMap {
|
|||
index: &IndexUrl,
|
||||
tags: &Tags,
|
||||
python_requirement: &PythonRequirement,
|
||||
allowed_yanks: &AllowedYanks,
|
||||
exclude_newer: Option<&DateTime<Utc>>,
|
||||
flat_index: Option<FlatDistributions>,
|
||||
no_binary: &NoBinary,
|
||||
no_build: &NoBuild,
|
||||
) -> Self {
|
||||
let mut map = BTreeMap::new();
|
||||
// Create stubs for each entry in simple metadata. The full conversion
|
||||
|
@ -85,15 +99,27 @@ impl VersionMap {
|
|||
NoBinary::All => true,
|
||||
NoBinary::Packages(packages) => packages.contains(package_name),
|
||||
};
|
||||
// Check if source distributions are allowed for this package.
|
||||
let no_build = match no_build {
|
||||
NoBuild::None => false,
|
||||
NoBuild::All => true,
|
||||
NoBuild::Packages(packages) => packages.contains(package_name),
|
||||
};
|
||||
let allowed_yanks = allowed_yanks
|
||||
.allowed_versions(package_name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
inner: VersionMapInner::Lazy(VersionMapLazy {
|
||||
map,
|
||||
simple_metadata,
|
||||
no_binary,
|
||||
no_build,
|
||||
index: index.clone(),
|
||||
tags: tags.clone(),
|
||||
python_requirement: python_requirement.clone(),
|
||||
exclude_newer: exclude_newer.copied(),
|
||||
allowed_yanks,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -256,6 +282,8 @@ struct VersionMapLazy {
|
|||
simple_metadata: OwnedArchive<SimpleMetadata>,
|
||||
/// When true, wheels aren't allowed.
|
||||
no_binary: bool,
|
||||
/// When true, source dists aren't allowed.
|
||||
no_build: bool,
|
||||
/// The URL of the index where this package came from.
|
||||
index: IndexUrl,
|
||||
/// The set of compatibility tags that determines whether a wheel is usable
|
||||
|
@ -267,6 +295,8 @@ struct VersionMapLazy {
|
|||
python_requirement: PythonRequirement,
|
||||
/// Whether files newer than this timestamp should be excluded or not.
|
||||
exclude_newer: Option<DateTime<Utc>>,
|
||||
/// Which yanked versions are allowed
|
||||
allowed_yanks: FxHashSet<Version>,
|
||||
}
|
||||
|
||||
impl VersionMapLazy {
|
||||
|
@ -319,69 +349,62 @@ impl VersionMapLazy {
|
|||
.expect("archived version files should deserialize");
|
||||
let mut priority_dist = init.cloned().unwrap_or_default();
|
||||
for (filename, file) in files.all() {
|
||||
if let Some(exclude_newer) = self.exclude_newer {
|
||||
// Support resolving as if it were an earlier timestamp, at least as long files have
|
||||
// upload time information.
|
||||
let (excluded, upload_time) = if let Some(exclude_newer) = self.exclude_newer {
|
||||
match file.upload_time_utc_ms.as_ref() {
|
||||
Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
|
||||
priority_dist.set_exclude_newer();
|
||||
continue;
|
||||
(true, Some(upload_time))
|
||||
}
|
||||
None => {
|
||||
warn_user_once!(
|
||||
"{} is missing an upload date, but user provided: {exclude_newer}",
|
||||
file.filename,
|
||||
);
|
||||
priority_dist.set_exclude_newer();
|
||||
continue;
|
||||
(true, None)
|
||||
}
|
||||
_ => {}
|
||||
_ => (false, None),
|
||||
}
|
||||
}
|
||||
let yanked = file.yanked.clone().unwrap_or_default();
|
||||
} else {
|
||||
(false, None)
|
||||
};
|
||||
|
||||
// Prioritize amongst all available files.
|
||||
let version = filename.version().clone();
|
||||
let requires_python = file.requires_python.clone();
|
||||
let yanked = file.yanked.clone();
|
||||
let hash = file.hashes.clone();
|
||||
match filename {
|
||||
DistFilename::WheelFilename(filename) => {
|
||||
// Determine a compatibility for the wheel based on tags
|
||||
let mut compatibility =
|
||||
WheelCompatibility::from(filename.compatibility(&self.tags));
|
||||
|
||||
if compatibility.is_compatible() {
|
||||
// Check for Python version incompatibility
|
||||
if let Some(ref requires_python) = file.requires_python {
|
||||
if !requires_python.contains(self.python_requirement.target()) {
|
||||
compatibility = WheelCompatibility::Incompatible(
|
||||
IncompatibleWheel::RequiresPython,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all wheels as incompatibility when binaries are disabled
|
||||
if self.no_binary {
|
||||
compatibility =
|
||||
WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
|
||||
}
|
||||
};
|
||||
|
||||
let compatibility = self.wheel_compatibility(
|
||||
&filename,
|
||||
&version,
|
||||
requires_python,
|
||||
yanked,
|
||||
excluded,
|
||||
upload_time,
|
||||
);
|
||||
let dist = Dist::from_registry(
|
||||
DistFilename::WheelFilename(filename),
|
||||
file,
|
||||
self.index.clone(),
|
||||
);
|
||||
priority_dist.insert_built(
|
||||
dist,
|
||||
requires_python,
|
||||
yanked,
|
||||
Some(hash),
|
||||
compatibility,
|
||||
);
|
||||
priority_dist.insert_built(dist, Some(hash), compatibility);
|
||||
}
|
||||
DistFilename::SourceDistFilename(filename) => {
|
||||
let compatibility = self.source_dist_compatibility(
|
||||
&version,
|
||||
requires_python,
|
||||
yanked,
|
||||
excluded,
|
||||
upload_time,
|
||||
);
|
||||
let dist = Dist::from_registry(
|
||||
DistFilename::SourceDistFilename(filename),
|
||||
file,
|
||||
self.index.clone(),
|
||||
);
|
||||
priority_dist.insert_source(dist, requires_python, yanked, Some(hash));
|
||||
priority_dist.insert_source(dist, Some(hash), compatibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -393,6 +416,88 @@ impl VersionMapLazy {
|
|||
};
|
||||
simple.dist.get_or_init(get_or_init).as_ref()
|
||||
}
|
||||
|
||||
fn source_dist_compatibility(
|
||||
&self,
|
||||
version: &Version,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
yanked: Option<Yanked>,
|
||||
excluded: bool,
|
||||
upload_time: Option<i64>,
|
||||
) -> SourceDistCompatibility {
|
||||
// Check if builds are disabled
|
||||
if self.no_build {
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
|
||||
}
|
||||
|
||||
// Check if after upload time cutoff
|
||||
if excluded {
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
|
||||
upload_time,
|
||||
));
|
||||
}
|
||||
|
||||
// Check if yanked
|
||||
if let Some(yanked) = yanked {
|
||||
if yanked.is_yanked() && !self.allowed_yanks.contains(version) {
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(yanked));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Python version is supported
|
||||
// Source distributions must meet both the _target_ Python version and the
|
||||
// _installed_ Python version (to build successfully)
|
||||
if let Some(requires_python) = requires_python {
|
||||
if !requires_python.contains(self.python_requirement.target())
|
||||
|| !requires_python.contains(self.python_requirement.installed())
|
||||
{
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::RequiresPython(
|
||||
requires_python,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
SourceDistCompatibility::Compatible
|
||||
}
|
||||
|
||||
fn wheel_compatibility(
|
||||
&self,
|
||||
filename: &WheelFilename,
|
||||
version: &Version,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
yanked: Option<Yanked>,
|
||||
excluded: bool,
|
||||
upload_time: Option<i64>,
|
||||
) -> WheelCompatibility {
|
||||
// Check if binaries are disabled
|
||||
if self.no_binary {
|
||||
return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
|
||||
}
|
||||
|
||||
// Check if after upload time cutoff
|
||||
if excluded {
|
||||
return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
|
||||
}
|
||||
|
||||
// Check if yanked
|
||||
if let Some(yanked) = yanked {
|
||||
if yanked.is_yanked() && !self.allowed_yanks.contains(version) {
|
||||
return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a Python version incompatibility`
|
||||
if let Some(requires_python) = requires_python {
|
||||
if !requires_python.contains(self.python_requirement.target()) {
|
||||
return WheelCompatibility::Incompatible(IncompatibleWheel::RequiresPython(
|
||||
requires_python,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine a compatibility for the wheel based on tags
|
||||
WheelCompatibility::from(filename.compatibility(&self.tags))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a possibly initialized [`PrioritizedDist`] for
|
||||
|
@ -402,7 +507,7 @@ enum LazyPrioritizedDist {
|
|||
/// Represents a eagerly constructed distribution from a
|
||||
/// `FlatDistributions`.
|
||||
OnlyFlat(PrioritizedDist),
|
||||
/// Represents a lazyily constructed distribution from an index into a
|
||||
/// Represents a lazily constructed distribution from an index into a
|
||||
/// `VersionFiles` from `SimpleMetadata`.
|
||||
OnlySimple(SimplePrioritizedDist),
|
||||
/// Combines the above. This occurs when we have data from both a flat
|
||||
|
|
|
@ -8,8 +8,8 @@ use crate::Manifest;
|
|||
|
||||
/// A set of package versions that are permitted, even if they're marked as yanked by the
|
||||
/// relevant index.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
|
||||
|
||||
impl AllowedYanks {
|
||||
pub(crate) fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
|
||||
|
@ -49,11 +49,12 @@ impl AllowedYanks {
|
|||
Self(allowed_yanks)
|
||||
}
|
||||
|
||||
/// Returns `true` if the given package version is allowed, even if it's marked as yanked by
|
||||
/// the relevant index.
|
||||
pub(crate) fn allowed(&self, package_name: &PackageName, version: &Version) -> bool {
|
||||
self.0
|
||||
.get(package_name)
|
||||
.is_some_and(|allowed_yanks| allowed_yanks.contains(version))
|
||||
/// Returns versions for the given package which are allowed even if marked as yanked by the
|
||||
/// relevant index.
|
||||
pub(crate) fn allowed_versions(
|
||||
&self,
|
||||
package_name: &PackageName,
|
||||
) -> Option<&FxHashSet<Version>> {
|
||||
self.0.get(package_name)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue