mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +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
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -915,6 +915,7 @@ version = "0.0.1"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"cache-key",
|
||||
"chrono",
|
||||
"data-encoding",
|
||||
"distribution-filename",
|
||||
"fs-err",
|
||||
|
|
|
@ -25,6 +25,7 @@ uv-normalize = { path = "../uv-normalize" }
|
|||
pypi-types = { path = "../pypi-types" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
data-encoding = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
|
|
@ -11,95 +11,88 @@ pub struct PrioritizedDist(Box<PrioritizedDistInner>);
|
|||
/// [`PrioritizedDist`] is boxed because [`Dist`] is large.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct PrioritizedDistInner {
|
||||
/// An arbitrary source distribution for the package version.
|
||||
source: Option<DistMetadata>,
|
||||
/// The most-compatible wheel distribution for the package version.
|
||||
wheel: Option<(DistMetadata, WheelCompatibility)>,
|
||||
/// The highest-priority source distribution. Between compatible source distributions this priority is arbitrary.
|
||||
source: Option<(Dist, SourceDistCompatibility)>,
|
||||
/// The highest-priority wheel.
|
||||
wheel: Option<(Dist, WheelCompatibility)>,
|
||||
/// The hashes for each distribution.
|
||||
hashes: Vec<Hashes>,
|
||||
/// If exclude newer filtered files from this distribution
|
||||
exclude_newer: bool,
|
||||
}
|
||||
|
||||
/// A distribution that can be used for both resolution and installation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompatibleDist<'a> {
|
||||
/// The distribution should be resolved and installed using a source distribution.
|
||||
SourceDist(&'a DistMetadata),
|
||||
SourceDist(&'a Dist),
|
||||
/// The distribution should be resolved and installed using a wheel distribution.
|
||||
CompatibleWheel(&'a DistMetadata, TagPriority),
|
||||
CompatibleWheel(&'a Dist, TagPriority),
|
||||
/// The distribution should be resolved using an incompatible wheel distribution, but
|
||||
/// installed using a source distribution.
|
||||
IncompatibleWheel {
|
||||
source_dist: &'a DistMetadata,
|
||||
wheel: &'a DistMetadata,
|
||||
source_dist: &'a Dist,
|
||||
wheel: &'a Dist,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum IncompatibleDist {
|
||||
/// An incompatible wheel is available.
|
||||
Wheel(IncompatibleWheel),
|
||||
/// An incompatible source distribution is available.
|
||||
Source(IncompatibleSource),
|
||||
/// No distributions are available
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WheelCompatibility {
|
||||
Incompatible(IncompatibleWheel),
|
||||
Compatible(TagPriority),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum IncompatibleWheel {
|
||||
ExcludeNewer(Option<i64>),
|
||||
Tag(IncompatibleTag),
|
||||
RequiresPython,
|
||||
RequiresPython(VersionSpecifiers),
|
||||
Yanked(Yanked),
|
||||
NoBinary,
|
||||
}
|
||||
|
||||
/// A [`Dist`] and metadata about it required for downstream filtering.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DistMetadata {
|
||||
/// The distribution.
|
||||
pub dist: Dist,
|
||||
/// The version of Python required by the distribution.
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
/// If the distribution file is yanked.
|
||||
pub yanked: Yanked,
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SourceDistCompatibility {
|
||||
Incompatible(IncompatibleSource),
|
||||
Compatible,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum IncompatibleSource {
|
||||
ExcludeNewer(Option<i64>),
|
||||
RequiresPython(VersionSpecifiers),
|
||||
Yanked(Yanked),
|
||||
NoBuild,
|
||||
}
|
||||
|
||||
impl PrioritizedDist {
|
||||
/// Create a new [`PrioritizedDist`] from the given wheel distribution.
|
||||
pub fn from_built(
|
||||
dist: Dist,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
yanked: Yanked,
|
||||
hash: Option<Hashes>,
|
||||
compatibility: WheelCompatibility,
|
||||
) -> Self {
|
||||
pub fn from_built(dist: Dist, hash: Option<Hashes>, compatibility: WheelCompatibility) -> Self {
|
||||
Self(Box::new(PrioritizedDistInner {
|
||||
wheel: Some((dist, compatibility)),
|
||||
source: None,
|
||||
wheel: Some((
|
||||
DistMetadata {
|
||||
dist,
|
||||
requires_python,
|
||||
yanked,
|
||||
},
|
||||
compatibility,
|
||||
)),
|
||||
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
|
||||
exclude_newer: false,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a new [`PrioritizedDist`] from the given source distribution.
|
||||
pub fn from_source(
|
||||
dist: Dist,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
yanked: Yanked,
|
||||
hash: Option<Hashes>,
|
||||
compatibility: SourceDistCompatibility,
|
||||
) -> Self {
|
||||
Self(Box::new(PrioritizedDistInner {
|
||||
source: Some(DistMetadata {
|
||||
dist,
|
||||
requires_python,
|
||||
yanked,
|
||||
}),
|
||||
wheel: None,
|
||||
source: Some((dist, compatibility)),
|
||||
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
|
||||
exclude_newer: false,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -107,32 +100,16 @@ impl PrioritizedDist {
|
|||
pub fn insert_built(
|
||||
&mut self,
|
||||
dist: Dist,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
yanked: Yanked,
|
||||
hash: Option<Hashes>,
|
||||
compatibility: WheelCompatibility,
|
||||
) {
|
||||
// Track the highest-priority wheel.
|
||||
if let Some((.., existing_compatibility)) = &self.0.wheel {
|
||||
if compatibility > *existing_compatibility {
|
||||
self.0.wheel = Some((
|
||||
DistMetadata {
|
||||
dist,
|
||||
requires_python,
|
||||
yanked,
|
||||
},
|
||||
compatibility,
|
||||
));
|
||||
if compatibility.is_more_compatible(existing_compatibility) {
|
||||
self.0.wheel = Some((dist, compatibility));
|
||||
}
|
||||
} else {
|
||||
self.0.wheel = Some((
|
||||
DistMetadata {
|
||||
dist,
|
||||
requires_python,
|
||||
yanked,
|
||||
},
|
||||
compatibility,
|
||||
));
|
||||
self.0.wheel = Some((dist, compatibility));
|
||||
}
|
||||
|
||||
if let Some(hash) = hash {
|
||||
|
@ -144,16 +121,16 @@ impl PrioritizedDist {
|
|||
pub fn insert_source(
|
||||
&mut self,
|
||||
dist: Dist,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
yanked: Yanked,
|
||||
hash: Option<Hashes>,
|
||||
compatibility: SourceDistCompatibility,
|
||||
) {
|
||||
if self.0.source.is_none() {
|
||||
self.0.source = Some(DistMetadata {
|
||||
dist,
|
||||
requires_python,
|
||||
yanked,
|
||||
});
|
||||
// Track the highest-priority source.
|
||||
if let Some((.., existing_compatibility)) = &self.0.source {
|
||||
if compatibility.is_more_compatible(existing_compatibility) {
|
||||
self.0.source = Some((dist, compatibility));
|
||||
}
|
||||
} else {
|
||||
self.0.source = Some((dist, compatibility));
|
||||
}
|
||||
|
||||
if let Some(hash) = hash {
|
||||
|
@ -172,22 +149,44 @@ impl PrioritizedDist {
|
|||
// wheel. We assume that all distributions have the same metadata for a given package
|
||||
// version. If a compatible source distribution exists, we assume we can build it, but
|
||||
// using the wheel is faster.
|
||||
(Some((wheel, _)), Some(source_dist)) => {
|
||||
Some(CompatibleDist::IncompatibleWheel { source_dist, wheel })
|
||||
}
|
||||
(
|
||||
Some((wheel, WheelCompatibility::Incompatible(_))),
|
||||
Some((source_dist, SourceDistCompatibility::Compatible)),
|
||||
) => Some(CompatibleDist::IncompatibleWheel { source_dist, wheel }),
|
||||
// Otherwise, if we have a source distribution, return it.
|
||||
(_, Some(source_dist)) => Some(CompatibleDist::SourceDist(source_dist)),
|
||||
(None, Some((source_dist, SourceDistCompatibility::Compatible))) => {
|
||||
Some(CompatibleDist::SourceDist(source_dist))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the source distribution, if any.
|
||||
pub fn source(&self) -> Option<&DistMetadata> {
|
||||
self.0.source.as_ref()
|
||||
/// Return the compatible source distribution, if any.
|
||||
pub fn compatible_source(&self) -> Option<&Dist> {
|
||||
self.0
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|(dist, compatibility)| match compatibility {
|
||||
SourceDistCompatibility::Compatible => Some(dist),
|
||||
SourceDistCompatibility::Incompatible(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the incompatible source distribution, if any.
|
||||
pub fn incompatible_source(&self) -> Option<(&Dist, &IncompatibleSource)> {
|
||||
self.0
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|(dist, compatibility)| match compatibility {
|
||||
SourceDistCompatibility::Compatible => None,
|
||||
SourceDistCompatibility::Incompatible(incompatibility) => {
|
||||
Some((dist, incompatibility))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the compatible built distribution, if any.
|
||||
pub fn compatible_wheel(&self) -> Option<(&DistMetadata, TagPriority)> {
|
||||
pub fn compatible_wheel(&self) -> Option<(&Dist, TagPriority)> {
|
||||
self.0
|
||||
.wheel
|
||||
.as_ref()
|
||||
|
@ -198,7 +197,7 @@ impl PrioritizedDist {
|
|||
}
|
||||
|
||||
/// Return the incompatible built distribution, if any.
|
||||
pub fn incompatible_wheel(&self) -> Option<(&DistMetadata, &IncompatibleWheel)> {
|
||||
pub fn incompatible_wheel(&self) -> Option<(&Dist, &IncompatibleWheel)> {
|
||||
self.0
|
||||
.wheel
|
||||
.as_ref()
|
||||
|
@ -208,16 +207,6 @@ impl PrioritizedDist {
|
|||
})
|
||||
}
|
||||
|
||||
/// Set the `exclude_newer` flag
|
||||
pub fn set_exclude_newer(&mut self) {
|
||||
self.0.exclude_newer = true;
|
||||
}
|
||||
|
||||
/// Check if any distributions were excluded by the `exclude_newer` option
|
||||
pub fn exclude_newer(&self) -> bool {
|
||||
self.0.exclude_newer
|
||||
}
|
||||
|
||||
/// Return the hashes for each distribution.
|
||||
pub fn hashes(&self) -> &[Hashes] {
|
||||
&self.0.hashes
|
||||
|
@ -231,8 +220,8 @@ impl PrioritizedDist {
|
|||
}
|
||||
|
||||
impl<'a> CompatibleDist<'a> {
|
||||
/// Return the [`DistMetadata`] to use during resolution.
|
||||
pub fn for_resolution(&self) -> &DistMetadata {
|
||||
/// Return the [`Dist`] to use during resolution.
|
||||
pub fn for_resolution(&self) -> &Dist {
|
||||
match *self {
|
||||
CompatibleDist::SourceDist(sdist) => sdist,
|
||||
CompatibleDist::CompatibleWheel(wheel, _) => wheel,
|
||||
|
@ -243,8 +232,8 @@ impl<'a> CompatibleDist<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the [`DistMetadata`] to use during installation.
|
||||
pub fn for_installation(&self) -> &DistMetadata {
|
||||
/// Return the [`Dist`] to use during installation.
|
||||
pub fn for_installation(&self) -> &Dist {
|
||||
match *self {
|
||||
CompatibleDist::SourceDist(sdist) => sdist,
|
||||
CompatibleDist::CompatibleWheel(wheel, _) => wheel,
|
||||
|
@ -254,42 +243,47 @@ impl<'a> CompatibleDist<'a> {
|
|||
} => source_dist,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Yanked`] status of the distribution.
|
||||
///
|
||||
/// 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 will treat the distribution is yanked if the file we will install with
|
||||
/// is yanked.
|
||||
///
|
||||
/// PEP 592: <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
|
||||
pub fn yanked(&self) -> &Yanked {
|
||||
&self.for_installation().yanked
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for WheelCompatibility {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(Self::Compatible(p_self), Self::Compatible(p_other)) => p_self.cmp(p_other),
|
||||
(Self::Incompatible(_), Self::Compatible(_)) => std::cmp::Ordering::Less,
|
||||
(Self::Compatible(_), Self::Incompatible(_)) => std::cmp::Ordering::Greater,
|
||||
(Self::Incompatible(t_self), Self::Incompatible(t_other)) => t_self.cmp(t_other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for WheelCompatibility {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(Self::cmp(self, other))
|
||||
}
|
||||
}
|
||||
|
||||
impl WheelCompatibility {
|
||||
pub fn is_compatible(&self) -> bool {
|
||||
matches!(self, Self::Compatible(_))
|
||||
}
|
||||
|
||||
/// Return `true` if the current compatibility is more compatible than another.
|
||||
///
|
||||
/// Compatible wheels are always higher more compatible than incompatible wheels.
|
||||
/// Compatible wheel ordering is determined by tag priority.
|
||||
pub fn is_more_compatible(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Compatible(_), Self::Incompatible(_)) => true,
|
||||
(Self::Compatible(tag_priority), Self::Compatible(other_tag_priority)) => {
|
||||
tag_priority > other_tag_priority
|
||||
}
|
||||
(Self::Incompatible(_), Self::Compatible(_)) => false,
|
||||
(Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
|
||||
incompatibility.is_more_compatible(other_incompatibility)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceDistCompatibility {
|
||||
/// Return the higher priority compatibility.
|
||||
///
|
||||
/// Compatible source distributions are always higher priority than incompatible source distributions.
|
||||
/// Compatible source distribution priority is arbitrary.
|
||||
/// Incompatible source distribution priority selects a source distribution that was "closest" to being usable.
|
||||
pub fn is_more_compatible(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Compatible, Self::Incompatible(_)) => true,
|
||||
(Self::Compatible, Self::Compatible) => false, // Arbitary
|
||||
(Self::Incompatible(_), Self::Compatible) => false,
|
||||
(Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
|
||||
incompatibility.is_more_compatible(other_incompatibility)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TagCompatibility> for WheelCompatibility {
|
||||
|
@ -300,3 +294,64 @@ impl From<TagCompatibility> for WheelCompatibility {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IncompatibleSource {
|
||||
fn is_more_compatible(&self, other: &Self) -> bool {
|
||||
match self {
|
||||
Self::ExcludeNewer(timestamp_self) => match other {
|
||||
// Smaller timestamps are closer to the cut-off time
|
||||
Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
|
||||
Self::NoBuild | Self::RequiresPython(_) | Self::Yanked(_) => true,
|
||||
},
|
||||
Self::RequiresPython(_) => match other {
|
||||
Self::ExcludeNewer(_) => false,
|
||||
// Version specifiers cannot be reasonably compared
|
||||
Self::RequiresPython(_) => false,
|
||||
Self::NoBuild | Self::Yanked(_) => true,
|
||||
},
|
||||
Self::Yanked(_) => match other {
|
||||
Self::ExcludeNewer(_) | Self::RequiresPython(_) => false,
|
||||
// Yanks with a reason are more helpful for errors
|
||||
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
|
||||
Self::NoBuild => true,
|
||||
},
|
||||
Self::NoBuild => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IncompatibleWheel {
|
||||
fn is_more_compatible(&self, other: &Self) -> bool {
|
||||
match self {
|
||||
Self::ExcludeNewer(timestamp_self) => match other {
|
||||
// Smaller timestamps are closer to the cut-off time
|
||||
Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
|
||||
(None, _) => true,
|
||||
(_, None) => false,
|
||||
(Some(timestamp_self), Some(timestamp_other)) => {
|
||||
timestamp_other < timestamp_self
|
||||
}
|
||||
},
|
||||
Self::NoBinary | Self::RequiresPython(_) | Self::Tag(_) | Self::Yanked(_) => true,
|
||||
},
|
||||
Self::Tag(tag_self) => match other {
|
||||
Self::ExcludeNewer(_) => false,
|
||||
Self::Tag(tag_other) => tag_other > tag_self,
|
||||
Self::NoBinary | Self::RequiresPython(_) | Self::Yanked(_) => true,
|
||||
},
|
||||
Self::RequiresPython(_) => match other {
|
||||
Self::ExcludeNewer(_) | Self::Tag(_) => false,
|
||||
// Version specifiers cannot be reasonably compared
|
||||
Self::RequiresPython(_) => false,
|
||||
Self::NoBinary | Self::Yanked(_) => true,
|
||||
},
|
||||
Self::Yanked(_) => match other {
|
||||
Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_) => false,
|
||||
// Yanks with a reason are more helpful for errors
|
||||
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
|
||||
Self::NoBinary => true,
|
||||
},
|
||||
Self::NoBinary => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,15 @@ impl DistInfoMetadata {
|
|||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
rkyv::Archive,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Serialize,
|
||||
)]
|
||||
#[archive(check_bytes)]
|
||||
#[archive_attr(derive(Debug))]
|
||||
|
|
|
@ -11,12 +11,12 @@ use url::Url;
|
|||
use distribution_filename::DistFilename;
|
||||
use distribution_types::{
|
||||
BuiltDist, Dist, File, FileLocation, FlatIndexLocation, IndexUrl, PrioritizedDist,
|
||||
RegistryBuiltDist, RegistrySourceDist, SourceDist,
|
||||
RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility,
|
||||
};
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::VerbatimUrl;
|
||||
use platform_tags::Tags;
|
||||
use pypi_types::{Hashes, Yanked};
|
||||
use pypi_types::Hashes;
|
||||
use uv_auth::safe_copy_url_auth;
|
||||
use uv_cache::{Cache, CacheBucket};
|
||||
use uv_normalize::PackageName;
|
||||
|
@ -307,20 +307,14 @@ impl FlatIndex {
|
|||
}));
|
||||
match distributions.0.entry(version) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().insert_built(
|
||||
dist,
|
||||
None,
|
||||
Yanked::default(),
|
||||
None,
|
||||
compatibility.into(),
|
||||
);
|
||||
entry
|
||||
.get_mut()
|
||||
.insert_built(dist, None, compatibility.into());
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(PrioritizedDist::from_built(
|
||||
dist,
|
||||
None,
|
||||
Yanked::default(),
|
||||
None,
|
||||
compatibility.into(),
|
||||
));
|
||||
}
|
||||
|
@ -334,16 +328,17 @@ impl FlatIndex {
|
|||
}));
|
||||
match distributions.0.entry(filename.version) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry
|
||||
.get_mut()
|
||||
.insert_source(dist, None, Yanked::default(), None);
|
||||
entry.get_mut().insert_source(
|
||||
dist,
|
||||
None,
|
||||
SourceDistCompatibility::Compatible,
|
||||
);
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(PrioritizedDist::from_source(
|
||||
dist,
|
||||
None,
|
||||
Yanked::default(),
|
||||
None,
|
||||
SourceDistCompatibility::Compatible,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,6 +122,7 @@ async fn install_chunk(
|
|||
venv.interpreter(),
|
||||
&FlatIndex::default(),
|
||||
&NoBinary::None,
|
||||
&NoBuild::None,
|
||||
)
|
||||
.resolve_stream(requirements)
|
||||
.collect()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -220,9 +220,15 @@ pub(crate) async fn pip_sync(
|
|||
} else {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let wheel_finder =
|
||||
uv_resolver::DistFinder::new(tags, &client, venv.interpreter(), &flat_index, no_binary)
|
||||
.with_reporter(FinderReporter::from(printer).with_length(remote.len() as u64));
|
||||
let wheel_finder = uv_resolver::DistFinder::new(
|
||||
tags,
|
||||
&client,
|
||||
venv.interpreter(),
|
||||
&flat_index,
|
||||
no_binary,
|
||||
no_build,
|
||||
)
|
||||
.with_reporter(FinderReporter::from(printer).with_length(remote.len() as u64));
|
||||
let resolution = wheel_finder.resolve(&remote).await?;
|
||||
|
||||
let s = if resolution.len() == 1 { "" } else { "s" };
|
||||
|
|
|
@ -3445,12 +3445,13 @@ fn no_wheels_no_build() {
|
|||
.arg("no-wheels-no-build-a")
|
||||
, @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to download and build: albatross==1.0.0
|
||||
Caused by: Building source distributions is disabled
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no wheels are usable and building from source is disabled, we can conclude that all versions of albatross cannot be used.
|
||||
And because you require albatross, we can conclude that the requirements are unsatisfiable.
|
||||
"###);
|
||||
|
||||
assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue