Skip to content

Commit 2c68dfd

Browse files
Backtrack to non-local versions when wheels are missing platform support (#10046)
## Summary This is yet another variation on #9928, with a few minor changes: 1. It only applies to local versions (e.g., `2.5.1+cpu`). 2. It only _considers_ the non-local version as an alternative (e.g., `2.5.1`). 3. It only _considers_ the non-local alternative if it _does_ support the unsupported platform. 4. Instead of failing, it falls back to using the local version. So, this is far less strict, and is effectively designed to solve PyTorch but nothing else. It's also not user-configurable, except by way of using `environments` to exclude platforms.
1 parent f3c5b63 commit 2c68dfd

File tree

9 files changed

+1202
-139
lines changed

9 files changed

+1202
-139
lines changed

crates/uv-distribution-types/src/prioritized_distribution.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::fmt::{Display, Formatter};
2-
use uv_distribution_filename::BuildTag;
2+
use uv_distribution_filename::{BuildTag, WheelFilename};
33

44
use uv_pep440::VersionSpecifiers;
5+
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
56
use uv_platform_tags::{IncompatibleTag, TagPriority};
67
use uv_pypi_types::{HashDigest, Yanked};
78

@@ -14,7 +15,7 @@ use crate::{
1415
pub struct PrioritizedDist(Box<PrioritizedDistInner>);
1516

1617
/// [`PrioritizedDist`] is boxed because [`Dist`] is large.
17-
#[derive(Debug, Default, Clone)]
18+
#[derive(Debug, Clone)]
1819
struct PrioritizedDistInner {
1920
/// The highest-priority source distribution. Between compatible source distributions this priority is arbitrary.
2021
source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
@@ -25,6 +26,20 @@ struct PrioritizedDistInner {
2526
wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
2627
/// The hashes for each distribution.
2728
hashes: Vec<HashDigest>,
29+
/// The set of supported platforms for the distribution, described in terms of their markers.
30+
markers: MarkerTree,
31+
}
32+
33+
impl Default for PrioritizedDistInner {
34+
fn default() -> Self {
35+
Self {
36+
source: None,
37+
best_wheel_index: None,
38+
wheels: Vec::new(),
39+
hashes: Vec::new(),
40+
markers: MarkerTree::FALSE,
41+
}
42+
}
2843
}
2944

3045
/// A distribution that can be used for both resolution and installation.
@@ -70,6 +85,16 @@ impl CompatibleDist<'_> {
7085
CompatibleDist::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_ref(),
7186
}
7287
}
88+
89+
/// Return the set of supported platform the distribution, in terms of their markers.
90+
pub fn implied_markers(&self) -> MarkerTree {
91+
match self {
92+
CompatibleDist::InstalledDist(_) => MarkerTree::TRUE,
93+
CompatibleDist::SourceDist { prioritized, .. } => prioritized.0.markers,
94+
CompatibleDist::CompatibleWheel { prioritized, .. } => prioritized.0.markers,
95+
CompatibleDist::IncompatibleWheel { prioritized, .. } => prioritized.0.markers,
96+
}
97+
}
7398
}
7499

75100
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -257,6 +282,7 @@ impl PrioritizedDist {
257282
compatibility: WheelCompatibility,
258283
) -> Self {
259284
Self(Box::new(PrioritizedDistInner {
285+
markers: implied_markers(&dist.filename),
260286
best_wheel_index: Some(0),
261287
wheels: vec![(dist, compatibility)],
262288
source: None,
@@ -271,6 +297,7 @@ impl PrioritizedDist {
271297
compatibility: SourceDistCompatibility,
272298
) -> Self {
273299
Self(Box::new(PrioritizedDistInner {
300+
markers: MarkerTree::TRUE,
274301
best_wheel_index: None,
275302
wheels: vec![],
276303
source: Some((dist, compatibility)),
@@ -293,8 +320,11 @@ impl PrioritizedDist {
293320
} else {
294321
self.0.best_wheel_index = Some(self.0.wheels.len());
295322
}
296-
self.0.wheels.push((dist, compatibility));
297323
self.0.hashes.extend(hashes);
324+
if !self.0.markers.is_true() {
325+
self.0.markers.or(implied_markers(&dist.filename));
326+
}
327+
self.0.wheels.push((dist, compatibility));
298328
}
299329

300330
/// Insert the given source distribution into the [`PrioritizedDist`].
@@ -312,7 +342,9 @@ impl PrioritizedDist {
312342
} else {
313343
self.0.source = Some((dist, compatibility));
314344
}
315-
345+
if !self.0.markers.is_true() {
346+
self.0.markers.or(MarkerTree::TRUE);
347+
}
316348
self.0.hashes.extend(hashes);
317349
}
318350

@@ -563,6 +595,7 @@ impl IncompatibleSource {
563595
}
564596

565597
impl IncompatibleWheel {
598+
#[allow(clippy::match_like_matches_macro)]
566599
fn is_more_compatible(&self, other: &Self) -> bool {
567600
match self {
568601
Self::ExcludeNewer(timestamp_self) => match other {
@@ -599,3 +632,39 @@ impl IncompatibleWheel {
599632
}
600633
}
601634
}
635+
636+
/// Given a wheel filename, determine the set of supported platforms, in terms of their markers.
637+
pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
638+
let mut marker = MarkerTree::FALSE;
639+
for platform_tag in &filename.platform_tag {
640+
match platform_tag.as_str() {
641+
"any" => marker.or(MarkerTree::TRUE),
642+
tag if tag.starts_with("win") => {
643+
marker.or(MarkerTree::expression(MarkerExpression::String {
644+
key: MarkerValueString::SysPlatform,
645+
operator: MarkerOperator::Equal,
646+
value: "win32".to_string(),
647+
}));
648+
}
649+
tag if tag.starts_with("macosx") => {
650+
marker.or(MarkerTree::expression(MarkerExpression::String {
651+
key: MarkerValueString::SysPlatform,
652+
operator: MarkerOperator::Equal,
653+
value: "darwin".to_string(),
654+
}));
655+
}
656+
tag if tag.starts_with("manylinux")
657+
|| tag.starts_with("musllinux")
658+
|| tag.starts_with("linux") =>
659+
{
660+
marker.or(MarkerTree::expression(MarkerExpression::String {
661+
key: MarkerValueString::SysPlatform,
662+
operator: MarkerOperator::Equal,
663+
value: "linux".to_string(),
664+
}));
665+
}
666+
_ => {}
667+
}
668+
}
669+
marker
670+
}

crates/uv-resolver/src/resolver/availability.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use std::fmt::{Display, Formatter};
33
use uv_distribution_types::IncompatibleDist;
44
use uv_pep440::{Version, VersionSpecifiers};
55

6-
use crate::resolver::MetadataUnavailable;
7-
use crate::ResolverEnvironment;
6+
use crate::resolver::{MetadataUnavailable, VersionFork};
87

98
/// The reason why a package or a version cannot be used.
109
#[derive(Debug, Clone, Eq, PartialEq)]
@@ -170,6 +169,6 @@ pub(crate) enum ResolverVersion {
170169
Unavailable(Version, UnavailableVersion),
171170
/// A usable version
172171
Unforked(Version),
173-
/// A set of forks.
174-
Forked(Vec<ResolverEnvironment>),
172+
/// A set of forks, optionally with resolved versions
173+
Forked(Vec<VersionFork>),
175174
}

crates/uv-resolver/src/resolver/environment.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ impl<'d> Forker<'d> {
512512
}
513513

514514
/// Fork the resolver based on a `Requires-Python` specifier.
515-
pub(crate) fn fork_python_requirement(
515+
pub(crate) fn fork_version_by_python_requirement(
516516
requires_python: &VersionSpecifiers,
517517
python_requirement: &PythonRequirement,
518518
env: &ResolverEnvironment,
@@ -555,6 +555,49 @@ pub(crate) fn fork_python_requirement(
555555
envs
556556
}
557557

558+
/// Fork the resolver based on a marker.
559+
pub(crate) fn fork_version_by_marker(
560+
env: &ResolverEnvironment,
561+
marker: MarkerTree,
562+
) -> Option<(ResolverEnvironment, ResolverEnvironment)> {
563+
let Kind::Universal {
564+
markers: ref env_marker,
565+
..
566+
} = env.kind
567+
else {
568+
panic!("resolver must be in universal mode for forking")
569+
};
570+
571+
// Attempt to split based on the marker.
572+
//
573+
// For example, given `python_version >= '3.10'` and the split marker `sys_platform == 'linux'`,
574+
// the result will be:
575+
//
576+
// `python_version >= '3.10' and sys_platform == 'linux'`
577+
// `python_version >= '3.10' and sys_platform != 'linux'`
578+
//
579+
// If the marker is disjoint with the current environment, then we should return an empty list.
580+
// If the marker complement is disjoint with the current environment, then we should also return
581+
// an empty list.
582+
//
583+
// For example, given `python_version >= '3.10' and sys_platform == 'linux'` and the split marker
584+
// `sys_platform == 'win32'`, return an empty list, since the following isn't satisfiable:
585+
//
586+
// python_version >= '3.10' and sys_platform == 'linux' and sys_platform == 'win32'
587+
if env_marker.is_disjoint(marker) {
588+
return None;
589+
}
590+
let with_marker = env.narrow_environment(marker);
591+
592+
let complement = marker.negate();
593+
if env_marker.is_disjoint(complement) {
594+
return None;
595+
}
596+
let without_marker = env.narrow_environment(complement);
597+
598+
Some((with_marker, without_marker))
599+
}
600+
558601
#[cfg(test)]
559602
mod tests {
560603
use std::ops::Bound;

0 commit comments

Comments
 (0)