Skip to content

Commit 172305a

Browse files
Allow users to mark platforms as "required" for wheel coverage (#10067)
## Summary This PR revives #10017, which might be viable now that we _don't_ enforce any platforms by default. The basic idea here is that users can mark certain platforms as required (empty, by default). When resolving, we ensure that the specified platforms have wheel coverage, backtracking if not. For example, to require that we include a version of PyTorch that supports Intel macOS: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.11" dependencies = ["torch>1.13"] [tool.uv] required-platforms = [ "sys_platform == 'darwin' and platform_machine == 'x86_64'" ] ``` Other than that, the forking is identical to past iterations of this PR. This would give users a way to resolve the tail of issues in #9711, but with manual opt-in to supporting specific platforms.
1 parent 9cdfad1 commit 172305a

29 files changed

+1231
-376
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::fmt::{Display, Formatter};
2+
3+
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
4+
5+
/// A platform for which the resolver is solving.
6+
#[derive(Debug, Clone, Copy)]
7+
pub enum KnownPlatform {
8+
Linux,
9+
Windows,
10+
MacOS,
11+
}
12+
13+
impl KnownPlatform {
14+
/// Return the platform's `sys.platform` value.
15+
pub fn sys_platform(self) -> &'static str {
16+
match self {
17+
KnownPlatform::Linux => "linux",
18+
KnownPlatform::Windows => "win32",
19+
KnownPlatform::MacOS => "darwin",
20+
}
21+
}
22+
23+
/// Return a [`MarkerTree`] for the platform.
24+
pub fn marker(self) -> MarkerTree {
25+
MarkerTree::expression(MarkerExpression::String {
26+
key: MarkerValueString::SysPlatform,
27+
operator: MarkerOperator::Equal,
28+
value: match self {
29+
KnownPlatform::Linux => arcstr::literal!("linux"),
30+
KnownPlatform::Windows => arcstr::literal!("win32"),
31+
KnownPlatform::MacOS => arcstr::literal!("darwin"),
32+
},
33+
})
34+
}
35+
36+
/// Determine the [`KnownPlatform`] from a marker tree.
37+
pub fn from_marker(marker: MarkerTree) -> Option<KnownPlatform> {
38+
if marker == KnownPlatform::Linux.marker() {
39+
Some(KnownPlatform::Linux)
40+
} else if marker == KnownPlatform::Windows.marker() {
41+
Some(KnownPlatform::Windows)
42+
} else if marker == KnownPlatform::MacOS.marker() {
43+
Some(KnownPlatform::MacOS)
44+
} else {
45+
None
46+
}
47+
}
48+
}
49+
50+
impl Display for KnownPlatform {
51+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
52+
match self {
53+
KnownPlatform::Linux => write!(f, "Linux"),
54+
KnownPlatform::Windows => write!(f, "Windows"),
55+
KnownPlatform::MacOS => write!(f, "macOS"),
56+
}
57+
}
58+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub use crate::index::*;
6666
pub use crate::index_name::*;
6767
pub use crate::index_url::*;
6868
pub use crate::installed::*;
69+
pub use crate::known_platform::*;
6970
pub use crate::origin::*;
7071
pub use crate::pip_index::*;
7172
pub use crate::prioritized_distribution::*;
@@ -90,6 +91,7 @@ mod index;
9091
mod index_name;
9192
mod index_url;
9293
mod installed;
94+
mod known_platform;
9395
mod origin;
9496
mod pip_index;
9597
mod prioritized_distribution;

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagPri
1111
use uv_pypi_types::{HashDigest, Yanked};
1212

1313
use crate::{
14-
InstalledDist, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, ResolvedDistRef,
14+
InstalledDist, KnownPlatform, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist,
15+
ResolvedDistRef,
1516
};
1617

1718
/// A collection of distributions that have been filtered by relevance.
@@ -123,6 +124,7 @@ impl IncompatibleDist {
123124
None => format!("has {self}"),
124125
},
125126
IncompatibleWheel::RequiresPython(..) => format!("requires {self}"),
127+
IncompatibleWheel::MissingPlatform(_) => format!("has {self}"),
126128
},
127129
Self::Source(incompatibility) => match incompatibility {
128130
IncompatibleSource::NoBuild => format!("has {self}"),
@@ -150,6 +152,7 @@ impl IncompatibleDist {
150152
None => format!("have {self}"),
151153
},
152154
IncompatibleWheel::RequiresPython(..) => format!("require {self}"),
155+
IncompatibleWheel::MissingPlatform(_) => format!("have {self}"),
153156
},
154157
Self::Source(incompatibility) => match incompatibility {
155158
IncompatibleSource::NoBuild => format!("have {self}"),
@@ -194,6 +197,7 @@ impl IncompatibleDist {
194197
IncompatibleWheel::Yanked(..) => None,
195198
IncompatibleWheel::ExcludeNewer(..) => None,
196199
IncompatibleWheel::RequiresPython(..) => None,
200+
IncompatibleWheel::MissingPlatform(..) => None,
197201
},
198202
Self::Source(..) => None,
199203
Self::Unavailable => None,
@@ -234,6 +238,15 @@ impl Display for IncompatibleDist {
234238
IncompatibleWheel::RequiresPython(python, _) => {
235239
write!(f, "Python {python}")
236240
}
241+
IncompatibleWheel::MissingPlatform(marker) => {
242+
if let Some(platform) = KnownPlatform::from_marker(*marker) {
243+
write!(f, "no {platform}-compatible wheels")
244+
} else if let Some(marker) = marker.try_to_string() {
245+
write!(f, "no `{marker}`-compatible wheels")
246+
} else {
247+
write!(f, "no compatible wheels")
248+
}
249+
}
237250
},
238251
Self::Source(incompatibility) => match incompatibility {
239252
IncompatibleSource::NoBuild => f.write_str("no usable wheels"),
@@ -288,6 +301,8 @@ pub enum IncompatibleWheel {
288301
Yanked(Yanked),
289302
/// The use of binary wheels is disabled.
290303
NoBinary,
304+
/// Wheels are not available for the current platform.
305+
MissingPlatform(MarkerTree),
291306
}
292307

293308
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -694,28 +709,41 @@ impl IncompatibleWheel {
694709
timestamp_other < timestamp_self
695710
}
696711
},
697-
Self::NoBinary | Self::RequiresPython(_, _) | Self::Tag(_) | Self::Yanked(_) => {
698-
true
699-
}
712+
Self::MissingPlatform(_)
713+
| Self::NoBinary
714+
| Self::RequiresPython(_, _)
715+
| Self::Tag(_)
716+
| Self::Yanked(_) => true,
700717
},
701718
Self::Tag(tag_self) => match other {
702719
Self::ExcludeNewer(_) => false,
703720
Self::Tag(tag_other) => tag_self > tag_other,
704-
Self::NoBinary | Self::RequiresPython(_, _) | Self::Yanked(_) => true,
721+
Self::MissingPlatform(_)
722+
| Self::NoBinary
723+
| Self::RequiresPython(_, _)
724+
| Self::Yanked(_) => true,
705725
},
706726
Self::RequiresPython(_, _) => match other {
707727
Self::ExcludeNewer(_) | Self::Tag(_) => false,
708728
// Version specifiers cannot be reasonably compared
709729
Self::RequiresPython(_, _) => false,
710-
Self::NoBinary | Self::Yanked(_) => true,
730+
Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true,
711731
},
712732
Self::Yanked(_) => match other {
713733
Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false,
714734
// Yanks with a reason are more helpful for errors
715735
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
716-
Self::NoBinary => true,
736+
Self::MissingPlatform(_) | Self::NoBinary => true,
737+
},
738+
Self::NoBinary => match other {
739+
Self::ExcludeNewer(_)
740+
| Self::Tag(_)
741+
| Self::RequiresPython(_, _)
742+
| Self::Yanked(_) => false,
743+
Self::NoBinary => false,
744+
Self::MissingPlatform(_) => true,
717745
},
718-
Self::NoBinary => false,
746+
Self::MissingPlatform(_) => false,
719747
}
720748
}
721749
}

crates/uv-pypi-types/src/supported_environments.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ use uv_pep508::MarkerTree;
99
pub struct SupportedEnvironments(Vec<MarkerTree>);
1010

1111
impl SupportedEnvironments {
12+
/// Create a new [`SupportedEnvironments`] struct from a list of marker trees.
13+
pub fn from_markers(markers: Vec<MarkerTree>) -> Self {
14+
SupportedEnvironments(markers)
15+
}
16+
1217
/// Return the list of marker trees.
1318
pub fn as_markers(&self) -> &[MarkerTree] {
1419
&self.0
@@ -18,6 +23,19 @@ impl SupportedEnvironments {
1823
pub fn into_markers(self) -> Vec<MarkerTree> {
1924
self.0
2025
}
26+
27+
/// Returns an iterator over the marker trees.
28+
pub fn iter(&self) -> std::slice::Iter<MarkerTree> {
29+
self.0.iter()
30+
}
31+
}
32+
33+
impl<'a> IntoIterator for &'a SupportedEnvironments {
34+
type IntoIter = std::slice::Iter<'a, MarkerTree>;
35+
type Item = &'a MarkerTree;
36+
fn into_iter(self) -> Self::IntoIter {
37+
self.iter()
38+
}
2139
}
2240

2341
/// Serialize a [`SupportedEnvironments`] struct into a list of marker strings.

crates/uv-resolver/src/lock/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ pub struct Lock {
126126
conflicts: Conflicts,
127127
/// The list of supported environments specified by the user.
128128
supported_environments: Vec<MarkerTree>,
129+
/// The list of required platforms specified by the user.
130+
required_environments: Vec<MarkerTree>,
129131
/// The range of supported Python versions.
130132
requires_python: RequiresPython,
131133
/// We discard the lockfile if these options don't match.
@@ -286,6 +288,7 @@ impl Lock {
286288
ResolverManifest::default(),
287289
Conflicts::empty(),
288290
vec![],
291+
vec![],
289292
resolution.fork_markers.clone(),
290293
)?;
291294
Ok(lock)
@@ -372,6 +375,7 @@ impl Lock {
372375
manifest: ResolverManifest,
373376
conflicts: Conflicts,
374377
supported_environments: Vec<MarkerTree>,
378+
required_environments: Vec<MarkerTree>,
375379
fork_markers: Vec<UniversalMarker>,
376380
) -> Result<Self, LockError> {
377381
// Put all dependencies for each package in a canonical order and
@@ -523,6 +527,7 @@ impl Lock {
523527
fork_markers,
524528
conflicts,
525529
supported_environments,
530+
required_environments,
526531
requires_python,
527532
options,
528533
packages,
@@ -565,6 +570,16 @@ impl Lock {
565570
self
566571
}
567572

573+
/// Record the required platforms that were used to generate this lock.
574+
#[must_use]
575+
pub fn with_required_environments(mut self, required_environments: Vec<MarkerTree>) -> Self {
576+
self.required_environments = required_environments
577+
.into_iter()
578+
.map(|marker| self.requires_python.complexify_markers(marker))
579+
.collect();
580+
self
581+
}
582+
568583
/// Returns the lockfile version.
569584
pub fn version(&self) -> u32 {
570585
self.version
@@ -625,6 +640,11 @@ impl Lock {
625640
&self.supported_environments
626641
}
627642

643+
/// Returns the required platforms that were used to generate this lock.
644+
pub fn required_environments(&self) -> &[MarkerTree] {
645+
&self.required_environments
646+
}
647+
628648
/// Returns the workspace members that were used to generate this lock.
629649
pub fn members(&self) -> &BTreeSet<PackageName> {
630650
&self.manifest.members
@@ -667,6 +687,16 @@ impl Lock {
667687
.collect()
668688
}
669689

690+
/// Returns the required platforms that were used to generate this
691+
/// lock.
692+
pub fn simplified_required_environments(&self) -> Vec<MarkerTree> {
693+
self.required_environments()
694+
.iter()
695+
.copied()
696+
.map(|marker| self.simplify_environment(marker))
697+
.collect()
698+
}
699+
670700
/// Simplify the given marker environment with respect to the lockfile's
671701
/// `requires-python` setting.
672702
pub fn simplify_environment(&self, marker: MarkerTree) -> MarkerTree {
@@ -712,6 +742,17 @@ impl Lock {
712742
doc.insert("supported-markers", value(supported_environments));
713743
}
714744

745+
if !self.required_environments.is_empty() {
746+
let required_environments = each_element_on_its_line_array(
747+
self.required_environments
748+
.iter()
749+
.copied()
750+
.map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
751+
.filter_map(SimplifiedMarkerTree::try_to_string),
752+
);
753+
doc.insert("required-markers", value(required_environments));
754+
}
755+
715756
if !self.conflicts.is_empty() {
716757
let mut list = Array::new();
717758
for set in self.conflicts.iter() {
@@ -1698,6 +1739,8 @@ struct LockWire {
16981739
fork_markers: Vec<SimplifiedMarkerTree>,
16991740
#[serde(rename = "supported-markers", default)]
17001741
supported_environments: Vec<SimplifiedMarkerTree>,
1742+
#[serde(rename = "required-markers", default)]
1743+
required_environments: Vec<SimplifiedMarkerTree>,
17011744
#[serde(rename = "conflicts", default)]
17021745
conflicts: Option<Conflicts>,
17031746
/// We discard the lockfile if these options match.
@@ -1740,6 +1783,11 @@ impl TryFrom<LockWire> for Lock {
17401783
.into_iter()
17411784
.map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
17421785
.collect();
1786+
let required_environments = wire
1787+
.required_environments
1788+
.into_iter()
1789+
.map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
1790+
.collect();
17431791
let fork_markers = wire
17441792
.fork_markers
17451793
.into_iter()
@@ -1755,6 +1803,7 @@ impl TryFrom<LockWire> for Lock {
17551803
wire.manifest,
17561804
wire.conflicts.unwrap_or_else(Conflicts::empty),
17571805
supported_environments,
1806+
required_environments,
17581807
fork_markers,
17591808
)?;
17601809

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Ok(
1111
[],
1212
),
1313
supported_environments: [],
14+
required_environments: [],
1415
requires_python: RequiresPython {
1516
specifiers: VersionSpecifiers(
1617
[

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Ok(
1111
[],
1212
),
1313
supported_environments: [],
14+
required_environments: [],
1415
requires_python: RequiresPython {
1516
specifiers: VersionSpecifiers(
1617
[

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Ok(
1111
[],
1212
),
1313
supported_environments: [],
14+
required_environments: [],
1415
requires_python: RequiresPython {
1516
specifiers: VersionSpecifiers(
1617
[

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Ok(
1111
[],
1212
),
1313
supported_environments: [],
14+
required_environments: [],
1415
requires_python: RequiresPython {
1516
specifiers: VersionSpecifiers(
1617
[

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Ok(
1111
[],
1212
),
1313
supported_environments: [],
14+
required_environments: [],
1415
requires_python: RequiresPython {
1516
specifiers: VersionSpecifiers(
1617
[

0 commit comments

Comments
 (0)