Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 44 additions & 50 deletions crates/uv-distribution-types/src/prioritized_distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ struct PrioritizedDistInner {
/// The hashes for each distribution.
hashes: Vec<HashDigest>,
/// The set of supported platforms for the distribution, described in terms of their markers.
markers: MarkerTree,
platform_markers: MarkerTree,
/// The set of supported Python versions for the distribution, described in terms of their
/// markers.
python_markers: MarkerTree,
}

impl Default for PrioritizedDistInner {
Expand All @@ -42,7 +45,8 @@ impl Default for PrioritizedDistInner {
best_wheel_index: None,
wheels: Vec::new(),
hashes: Vec::new(),
markers: MarkerTree::FALSE,
platform_markers: MarkerTree::FALSE,
python_markers: MarkerTree::FALSE,
}
}
}
Expand Down Expand Up @@ -101,10 +105,31 @@ impl CompatibleDist<'_> {
}
}

/// Return the set of supported platform the distribution, in terms of their markers.
/// Return the set of supported platforms the distribution, in terms of their markers.
pub fn implied_platform_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => prioritized.0.platform_markers,
None => MarkerTree::TRUE,
}
}

/// Return the set of supported Python versions for the distribution, in terms of their markers.
pub fn implied_python_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => prioritized.0.python_markers,
None => MarkerTree::TRUE,
}
}

/// Return the combined set of supported markers for the distribution.
pub fn implied_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => prioritized.0.markers,
Some(prioritized) => {
let mut markers = MarkerTree::TRUE;
markers.and(prioritized.0.platform_markers);
markers.and(prioritized.0.python_markers);
markers
}
None => MarkerTree::TRUE,
}
}
Expand Down Expand Up @@ -343,7 +368,8 @@ impl PrioritizedDist {
compatibility: WheelCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: implied_markers(&dist.filename),
platform_markers: implied_platform_markers(&dist.filename),
python_markers: implied_python_markers(&dist.filename),
best_wheel_index: Some(0),
wheels: vec![(dist, compatibility)],
source: None,
Expand All @@ -358,7 +384,8 @@ impl PrioritizedDist {
compatibility: SourceDistCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: MarkerTree::TRUE,
python_markers: MarkerTree::TRUE,
platform_markers: MarkerTree::TRUE,
best_wheel_index: None,
wheels: vec![],
source: Some((dist, compatibility)),
Expand All @@ -375,8 +402,15 @@ impl PrioritizedDist {
) {
// Track the implied markers.
if compatibility.is_compatible() {
if !self.0.markers.is_true() {
self.0.markers.or(implied_markers(&dist.filename));
if !self.0.python_markers.is_true() {
self.0
.python_markers
.or(implied_python_markers(&dist.filename));
}
if !self.0.platform_markers.is_true() {
self.0
.platform_markers
.or(implied_platform_markers(&dist.filename));
}
}
// Track the hashes.
Expand All @@ -403,7 +437,8 @@ impl PrioritizedDist {
) {
// Track the implied markers.
if compatibility.is_compatible() {
self.0.markers = MarkerTree::TRUE;
self.0.python_markers = MarkerTree::TRUE;
self.0.platform_markers = MarkerTree::TRUE;
}
// Track the hashes.
if !compatibility.is_excluded() {
Expand Down Expand Up @@ -804,14 +839,6 @@ impl IncompatibleWheel {
}
}

/// Given a wheel filename, determine the set of supported markers.
pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = implied_platform_markers(filename);
marker.and(implied_python_markers(filename));

marker
}

/// Given a wheel filename, determine the set of supported platforms, in terms of their markers.
///
/// This is roughly the inverse of platform tag generation: given a tag, we want to infer the
Expand Down Expand Up @@ -1027,15 +1054,6 @@ mod tests {
);
}

#[track_caller]
fn assert_implied_markers(filename: &str, expected: &str) {
let filename = WheelFilename::from_str(filename).unwrap();
assert_eq!(
implied_markers(&filename),
expected.parse::<MarkerTree>().unwrap()
);
}

#[test]
fn test_implied_platform_markers() {
let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
Expand Down Expand Up @@ -1121,28 +1139,4 @@ mod tests {
"python_full_version >= '3.11' and python_full_version < '3.13'",
);
}

#[test]
fn test_implied_markers() {
assert_implied_markers(
"numpy-1.0-cp310-cp310-win32.whl",
"python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'",
);
assert_implied_markers(
"pywin32-311-cp314-cp314-win_arm64.whl",
"python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'",
);
assert_implied_markers(
"numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl",
"python_full_version == '3.11.*' and platform_python_implementation == 'CPython' and sys_platform == 'darwin' and platform_machine == 'x86_64'",
);
assert_implied_markers(
"numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
"python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'",
);
assert_implied_markers(
"example-1.0-py3-none-any.whl",
"python_full_version >= '3' and python_full_version < '4'",
);
}
}
45 changes: 45 additions & 0 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,51 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Ok(None);
}

// If the package's Python markers are incompatible with the current environment, we need to
// fork. We do not require users to explicitly add Python versions to their
// `required-environments`.
let dist_python_markers = dist.implied_python_markers();
if !env.included_by_marker(dist_python_markers) {
// Then we need to fork.
let Some((left, right)) = fork_version_by_marker(env, dist_python_markers) else {
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel(
// TODO(zanieb): Consider adding a Python-specific variant
IncompatibleWheel::MissingPlatform(dist_python_markers),
)),
)));
};

// TODO(zanieb): Consider a message that's focused on Python versions here
debug!(
"Forking on required Python `{}` for {}=={} ({})",
dist_python_markers
.try_to_string()
.unwrap_or_else(|| "true".to_string()),
name,
candidate.version(),
[&left, &right]
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
);
let forks = vec![
VersionFork {
env: left,
id,
version: None,
},
VersionFork {
env: right,
id,
version: None,
},
];
return Ok(Some(ResolverVersion::Forked(forks)));
}

// If the user explicitly marked a platform as required, ensure it has coverage.
for marker in self.options.required_environments.iter().copied() {
// If the platform is part of the current environment...
Expand Down
Loading