Skip to content

Commit

Permalink
Allow users to mark platforms as "required" for wheel coverage (#10067)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
charliermarsh authored Feb 14, 2025
1 parent 9cdfad1 commit 172305a
Show file tree
Hide file tree
Showing 29 changed files with 1,231 additions and 376 deletions.
58 changes: 58 additions & 0 deletions crates/uv-distribution-types/src/known_platform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use std::fmt::{Display, Formatter};

use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};

/// A platform for which the resolver is solving.
#[derive(Debug, Clone, Copy)]
pub enum KnownPlatform {
Linux,
Windows,
MacOS,
}

impl KnownPlatform {
/// Return the platform's `sys.platform` value.
pub fn sys_platform(self) -> &'static str {
match self {
KnownPlatform::Linux => "linux",
KnownPlatform::Windows => "win32",
KnownPlatform::MacOS => "darwin",
}
}

/// Return a [`MarkerTree`] for the platform.
pub fn marker(self) -> MarkerTree {
MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: match self {
KnownPlatform::Linux => arcstr::literal!("linux"),
KnownPlatform::Windows => arcstr::literal!("win32"),
KnownPlatform::MacOS => arcstr::literal!("darwin"),
},
})
}

/// Determine the [`KnownPlatform`] from a marker tree.
pub fn from_marker(marker: MarkerTree) -> Option<KnownPlatform> {
if marker == KnownPlatform::Linux.marker() {
Some(KnownPlatform::Linux)
} else if marker == KnownPlatform::Windows.marker() {
Some(KnownPlatform::Windows)
} else if marker == KnownPlatform::MacOS.marker() {
Some(KnownPlatform::MacOS)
} else {
None
}
}
}

impl Display for KnownPlatform {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KnownPlatform::Linux => write!(f, "Linux"),
KnownPlatform::Windows => write!(f, "Windows"),
KnownPlatform::MacOS => write!(f, "macOS"),
}
}
}
2 changes: 2 additions & 0 deletions crates/uv-distribution-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub use crate::index::*;
pub use crate::index_name::*;
pub use crate::index_url::*;
pub use crate::installed::*;
pub use crate::known_platform::*;
pub use crate::origin::*;
pub use crate::pip_index::*;
pub use crate::prioritized_distribution::*;
Expand All @@ -90,6 +91,7 @@ mod index;
mod index_name;
mod index_url;
mod installed;
mod known_platform;
mod origin;
mod pip_index;
mod prioritized_distribution;
Expand Down
44 changes: 36 additions & 8 deletions crates/uv-distribution-types/src/prioritized_distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagPri
use uv_pypi_types::{HashDigest, Yanked};

use crate::{
InstalledDist, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, ResolvedDistRef,
InstalledDist, KnownPlatform, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist,
ResolvedDistRef,
};

/// A collection of distributions that have been filtered by relevance.
Expand Down Expand Up @@ -123,6 +124,7 @@ impl IncompatibleDist {
None => format!("has {self}"),
},
IncompatibleWheel::RequiresPython(..) => format!("requires {self}"),
IncompatibleWheel::MissingPlatform(_) => format!("has {self}"),
},
Self::Source(incompatibility) => match incompatibility {
IncompatibleSource::NoBuild => format!("has {self}"),
Expand Down Expand Up @@ -150,6 +152,7 @@ impl IncompatibleDist {
None => format!("have {self}"),
},
IncompatibleWheel::RequiresPython(..) => format!("require {self}"),
IncompatibleWheel::MissingPlatform(_) => format!("have {self}"),
},
Self::Source(incompatibility) => match incompatibility {
IncompatibleSource::NoBuild => format!("have {self}"),
Expand Down Expand Up @@ -194,6 +197,7 @@ impl IncompatibleDist {
IncompatibleWheel::Yanked(..) => None,
IncompatibleWheel::ExcludeNewer(..) => None,
IncompatibleWheel::RequiresPython(..) => None,
IncompatibleWheel::MissingPlatform(..) => None,
},
Self::Source(..) => None,
Self::Unavailable => None,
Expand Down Expand Up @@ -234,6 +238,15 @@ impl Display for IncompatibleDist {
IncompatibleWheel::RequiresPython(python, _) => {
write!(f, "Python {python}")
}
IncompatibleWheel::MissingPlatform(marker) => {
if let Some(platform) = KnownPlatform::from_marker(*marker) {
write!(f, "no {platform}-compatible wheels")
} else if let Some(marker) = marker.try_to_string() {
write!(f, "no `{marker}`-compatible wheels")
} else {
write!(f, "no compatible wheels")
}
}
},
Self::Source(incompatibility) => match incompatibility {
IncompatibleSource::NoBuild => f.write_str("no usable wheels"),
Expand Down Expand Up @@ -288,6 +301,8 @@ pub enum IncompatibleWheel {
Yanked(Yanked),
/// The use of binary wheels is disabled.
NoBinary,
/// Wheels are not available for the current platform.
MissingPlatform(MarkerTree),
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -694,28 +709,41 @@ impl IncompatibleWheel {
timestamp_other < timestamp_self
}
},
Self::NoBinary | Self::RequiresPython(_, _) | Self::Tag(_) | Self::Yanked(_) => {
true
}
Self::MissingPlatform(_)
| Self::NoBinary
| Self::RequiresPython(_, _)
| Self::Tag(_)
| Self::Yanked(_) => true,
},
Self::Tag(tag_self) => match other {
Self::ExcludeNewer(_) => false,
Self::Tag(tag_other) => tag_self > tag_other,
Self::NoBinary | Self::RequiresPython(_, _) | Self::Yanked(_) => true,
Self::MissingPlatform(_)
| 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::MissingPlatform(_) | 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::MissingPlatform(_) | Self::NoBinary => true,
},
Self::NoBinary => match other {
Self::ExcludeNewer(_)
| Self::Tag(_)
| Self::RequiresPython(_, _)
| Self::Yanked(_) => false,
Self::NoBinary => false,
Self::MissingPlatform(_) => true,
},
Self::NoBinary => false,
Self::MissingPlatform(_) => false,
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions crates/uv-pypi-types/src/supported_environments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ use uv_pep508::MarkerTree;
pub struct SupportedEnvironments(Vec<MarkerTree>);

impl SupportedEnvironments {
/// Create a new [`SupportedEnvironments`] struct from a list of marker trees.
pub fn from_markers(markers: Vec<MarkerTree>) -> Self {
SupportedEnvironments(markers)
}

/// Return the list of marker trees.
pub fn as_markers(&self) -> &[MarkerTree] {
&self.0
Expand All @@ -18,6 +23,19 @@ impl SupportedEnvironments {
pub fn into_markers(self) -> Vec<MarkerTree> {
self.0
}

/// Returns an iterator over the marker trees.
pub fn iter(&self) -> std::slice::Iter<MarkerTree> {
self.0.iter()
}
}

impl<'a> IntoIterator for &'a SupportedEnvironments {
type IntoIter = std::slice::Iter<'a, MarkerTree>;
type Item = &'a MarkerTree;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

/// Serialize a [`SupportedEnvironments`] struct into a list of marker strings.
Expand Down
49 changes: 49 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ pub struct Lock {
conflicts: Conflicts,
/// The list of supported environments specified by the user.
supported_environments: Vec<MarkerTree>,
/// The list of required platforms specified by the user.
required_environments: Vec<MarkerTree>,
/// The range of supported Python versions.
requires_python: RequiresPython,
/// We discard the lockfile if these options don't match.
Expand Down Expand Up @@ -286,6 +288,7 @@ impl Lock {
ResolverManifest::default(),
Conflicts::empty(),
vec![],
vec![],
resolution.fork_markers.clone(),
)?;
Ok(lock)
Expand Down Expand Up @@ -372,6 +375,7 @@ impl Lock {
manifest: ResolverManifest,
conflicts: Conflicts,
supported_environments: Vec<MarkerTree>,
required_environments: Vec<MarkerTree>,
fork_markers: Vec<UniversalMarker>,
) -> Result<Self, LockError> {
// Put all dependencies for each package in a canonical order and
Expand Down Expand Up @@ -523,6 +527,7 @@ impl Lock {
fork_markers,
conflicts,
supported_environments,
required_environments,
requires_python,
options,
packages,
Expand Down Expand Up @@ -565,6 +570,16 @@ impl Lock {
self
}

/// Record the required platforms that were used to generate this lock.
#[must_use]
pub fn with_required_environments(mut self, required_environments: Vec<MarkerTree>) -> Self {
self.required_environments = required_environments
.into_iter()
.map(|marker| self.requires_python.complexify_markers(marker))
.collect();
self
}

/// Returns the lockfile version.
pub fn version(&self) -> u32 {
self.version
Expand Down Expand Up @@ -625,6 +640,11 @@ impl Lock {
&self.supported_environments
}

/// Returns the required platforms that were used to generate this lock.
pub fn required_environments(&self) -> &[MarkerTree] {
&self.required_environments
}

/// Returns the workspace members that were used to generate this lock.
pub fn members(&self) -> &BTreeSet<PackageName> {
&self.manifest.members
Expand Down Expand Up @@ -667,6 +687,16 @@ impl Lock {
.collect()
}

/// Returns the required platforms that were used to generate this
/// lock.
pub fn simplified_required_environments(&self) -> Vec<MarkerTree> {
self.required_environments()
.iter()
.copied()
.map(|marker| self.simplify_environment(marker))
.collect()
}

/// Simplify the given marker environment with respect to the lockfile's
/// `requires-python` setting.
pub fn simplify_environment(&self, marker: MarkerTree) -> MarkerTree {
Expand Down Expand Up @@ -712,6 +742,17 @@ impl Lock {
doc.insert("supported-markers", value(supported_environments));
}

if !self.required_environments.is_empty() {
let required_environments = each_element_on_its_line_array(
self.required_environments
.iter()
.copied()
.map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
.filter_map(SimplifiedMarkerTree::try_to_string),
);
doc.insert("required-markers", value(required_environments));
}

if !self.conflicts.is_empty() {
let mut list = Array::new();
for set in self.conflicts.iter() {
Expand Down Expand Up @@ -1698,6 +1739,8 @@ struct LockWire {
fork_markers: Vec<SimplifiedMarkerTree>,
#[serde(rename = "supported-markers", default)]
supported_environments: Vec<SimplifiedMarkerTree>,
#[serde(rename = "required-markers", default)]
required_environments: Vec<SimplifiedMarkerTree>,
#[serde(rename = "conflicts", default)]
conflicts: Option<Conflicts>,
/// We discard the lockfile if these options match.
Expand Down Expand Up @@ -1740,6 +1783,11 @@ impl TryFrom<LockWire> for Lock {
.into_iter()
.map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
.collect();
let required_environments = wire
.required_environments
.into_iter()
.map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
.collect();
let fork_markers = wire
.fork_markers
.into_iter()
Expand All @@ -1755,6 +1803,7 @@ impl TryFrom<LockWire> for Lock {
wire.manifest,
wire.conflicts.unwrap_or_else(Conflicts::empty),
supported_environments,
required_environments,
fork_markers,
)?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Ok(
[],
),
supported_environments: [],
required_environments: [],
requires_python: RequiresPython {
specifiers: VersionSpecifiers(
[
Expand Down
Loading

0 comments on commit 172305a

Please sign in to comment.