Skip to content

Commit

Permalink
Respect all platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 16, 2024
1 parent 9ab7dd6 commit 4f81e4b
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 9 deletions.
86 changes: 82 additions & 4 deletions crates/uv-distribution-types/src/prioritized_distribution.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::fmt::{Display, Formatter};
use uv_distribution_filename::BuildTag;
use uv_distribution_filename::{BuildTag, WheelFilename};

use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{IncompatibleTag, TagPriority};
use uv_pypi_types::{HashDigest, Yanked};

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

/// [`PrioritizedDist`] is boxed because [`Dist`] is large.
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
struct PrioritizedDistInner {
/// The highest-priority source distribution. Between compatible source distributions this priority is arbitrary.
source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
Expand All @@ -25,6 +26,20 @@ struct PrioritizedDistInner {
wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
/// The hashes for each distribution.
hashes: Vec<HashDigest>,
/// The implied markers.
markers: MarkerTree,
}

impl Default for PrioritizedDistInner {
fn default() -> Self {
Self {
source: None,
best_wheel_index: None,
wheels: Vec::new(),
hashes: Vec::new(),
markers: MarkerTree::FALSE,
}
}
}

/// A distribution that can be used for both resolution and installation.
Expand Down Expand Up @@ -70,6 +85,15 @@ impl CompatibleDist<'_> {
CompatibleDist::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_ref(),
}
}

pub fn implied_markers(&self) -> MarkerTree {
match self {
CompatibleDist::InstalledDist(_) => MarkerTree::TRUE,
CompatibleDist::SourceDist { prioritized, .. } => prioritized.0.markers,
CompatibleDist::CompatibleWheel { prioritized, .. } => prioritized.0.markers,
CompatibleDist::IncompatibleWheel { prioritized, .. } => prioritized.0.markers,
}
}
}

#[derive(Debug, PartialEq, Eq, Clone)]
Expand Down Expand Up @@ -261,6 +285,7 @@ impl PrioritizedDist {
compatibility: WheelCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: implied_markers(&dist.filename),
best_wheel_index: Some(0),
wheels: vec![(dist, compatibility)],
source: None,
Expand All @@ -275,6 +300,7 @@ impl PrioritizedDist {
compatibility: SourceDistCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: MarkerTree::TRUE,
best_wheel_index: None,
wheels: vec![],
source: Some((dist, compatibility)),
Expand All @@ -297,8 +323,11 @@ impl PrioritizedDist {
} else {
self.0.best_wheel_index = Some(self.0.wheels.len());
}
self.0.wheels.push((dist, compatibility));
self.0.hashes.extend(hashes);
if !self.0.markers.is_true() {
self.0.markers.or(implied_markers(&dist.filename));
}
self.0.wheels.push((dist, compatibility));
}

/// Insert the given source distribution into the [`PrioritizedDist`].
Expand All @@ -316,7 +345,9 @@ impl PrioritizedDist {
} else {
self.0.source = Some((dist, compatibility));
}

if !self.0.markers.is_true() {
self.0.markers.or(MarkerTree::TRUE);
}
self.0.hashes.extend(hashes);
}

Expand Down Expand Up @@ -603,3 +634,50 @@ impl IncompatibleWheel {
}
}
}

/// Given a wheel filename, determine the set of supported platforms, in terms of their markers.
pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = MarkerTree::FALSE;
for platform_tag in &filename.platform_tag {
match platform_tag.as_str() {
"any" => marker.or(MarkerTree::TRUE),
tag if tag.starts_with("win") => {
marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "win32".to_string(),
}));
}
tag if tag.starts_with("manylinux") => {
marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "linux".to_string(),
}));
}
tag if tag.starts_with("musllinux") => {
marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "linux".to_string(),
}));
}
tag if tag.starts_with("linux") => {
marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "linux".to_string(),
}));
}
tag if tag.starts_with("macosx") => {
marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "darwin".to_string(),
}));
}
_ => {}
}
}
marker
}
47 changes: 46 additions & 1 deletion crates/uv-resolver/src/resolver/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ impl<'d> Forker<'d> {
}

/// Fork the resolver based on a `Requires-Python` specifier.
pub(crate) fn fork_python_requirement(
pub(crate) fn fork_version_by_python_requirement(
requires_python: &VersionSpecifiers,
python_requirement: &PythonRequirement,
env: &ResolverEnvironment,
Expand Down Expand Up @@ -555,6 +555,51 @@ pub(crate) fn fork_python_requirement(
envs
}

/// Fork the resolver based on a marker.
pub(crate) fn fork_version_by_marker(
env: &ResolverEnvironment,
marker: &MarkerTree,
) -> Vec<ResolverEnvironment> {
let Kind::Universal {
markers: ref env_marker,
..
} = env.kind
else {
panic!("resolver must be in universal mode for forking")
};

// Attempt to split based on the marker.
//
// For example, given `python_version >= '3.10'` and the split marker `sys_platform == 'linux'`,
// the result will be:
//
// `python_version >= '3.10' and sys_platform == 'linux'`
// `python_version >= '3.10' and sys_platform != 'linux'`
//
// If the marker is disjoint with the current environment, then we should return an empty list.
// If the marker complement is disjoint with the current environment, then we should also return
// an empty list.
//
// For example, given `python_version >= '3.10' and sys_platform == 'linux'` and the split marker
// `sys_platform == 'win32'`, return an empty list, since the following isn't satisfiable:
//
// python_version >= '3.10' and sys_platform == 'linux' and sys_platform == 'win32'

let mut envs = vec![];
if env_marker.is_disjoint(*marker) {
return vec![];
}
envs.push(env.narrow_environment(*marker));

let complement = marker.negate();
if env_marker.is_disjoint(complement) {
return vec![];
}
envs.push(env.narrow_environment(complement));

envs
}

#[cfg(test)]
mod tests {
use std::ops::Bound;
Expand Down
62 changes: 59 additions & 3 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use uv_distribution_types::{
use uv_git::GitResolver;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifiers, MIN_VERSION};
use uv_pep508::MarkerTree;
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::Tags;
use uv_pypi_types::{
ConflictItem, ConflictItemRef, Conflicts, Requirement, ResolutionMetadata, VerbatimParsedUrl,
Expand Down Expand Up @@ -61,7 +61,9 @@ pub(crate) use crate::resolver::availability::{
use crate::resolver::batch_prefetch::BatchPrefetcher;
pub use crate::resolver::derivation::DerivationChainBuilder;
pub use crate::resolver::environment::ResolverEnvironment;
use crate::resolver::environment::{fork_python_requirement, ForkingPossibility};
use crate::resolver::environment::{
fork_version_by_marker, fork_version_by_python_requirement, ForkingPossibility,
};
pub(crate) use crate::resolver::fork_map::{ForkMap, ForkSet};
pub(crate) use crate::resolver::urls::Urls;
use crate::universal_marker::{ConflictMarker, UniversalMarker};
Expand Down Expand Up @@ -1127,7 +1129,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
{
if matches!(self.options.fork_strategy, ForkStrategy::RequiresPython) {
if env.marker_environment().is_none() {
let forks = fork_python_requirement(requires_python, python_requirement, env);
let forks = fork_version_by_python_requirement(
requires_python,
python_requirement,
env,
);
if !forks.is_empty() {
debug!(
"Forking Python requirement `{}` on `{}` for {}=={} ({})",
Expand All @@ -1152,6 +1158,56 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
)));
}

// Check whether this version covers all supported platforms.
if !dist.implied_markers().is_true() {
for marker in [
MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "linux".to_string(),
}),
MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "win32".to_string(),
}),
MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: "darwin".to_string(),
}),
] {
// If the platform is part of the current environment...
if env.included_by_marker(marker) {
// But isn't supported by the distribution...
if dist.implied_markers().is_disjoint(marker) {
// Then we need to fork.
let forks = fork_version_by_marker(env, &marker);
return if forks.is_empty() {
Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel(
IncompatibleWheel::NoBinary,
)),
)))
} else {
debug!(
"Forking platform for {}=={} ({})",
name,
candidate.version(),
forks
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
);
Ok(Some(ResolverVersion::Forked(forks)))
};
}
}
}
}

let filename = match dist.for_installation() {
ResolvedDistRef::InstallableRegistrySourceDist { sdist, .. } => sdist
.filename()
Expand Down
Loading

0 comments on commit 4f81e4b

Please sign in to comment.