use std::fmt::{Display, Formatter};
use arcstr::ArcStr;
use owo_colors::OwoColorize;
use tracing::debug;
use uv_distribution_filename::{BuildTag, WheelFilename};
use uv_distribution_types::{
File, IndexUrl, InstalledDist, MinimumLibcVersion, Name, RegistryBuiltDist, RegistryBuiltWheel,
RegistrySourceDist, RequiresPython, VariantsJsonFilename,
};
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{
AbiTag, BinaryFormat, IncompatibleTag, LanguageTag, PlatformTag, TagPriority, Tags,
};
use uv_pypi_types::{HashDigest, Yanked};
use crate::known_platform::KnownPlatform;
use crate::resolved::ResolvedDistRef;
#[derive(Debug, Default, Clone)]
pub struct PrioritizedDist(Box<PrioritizedDistInner>);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct RegistryVariantsJson {
pub(crate) filename: VariantsJsonFilename,
pub(crate) file: File,
pub(crate) index: IndexUrl,
}
#[derive(Debug, Clone)]
struct PrioritizedDistInner {
source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
best_wheel_index: Option<usize>,
wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
variants_json: Vec<RegistryVariantsJson>,
hashes: Vec<HashDigest>,
python_markers: MarkerTree,
markers: [MarkerTree; 2],
}
impl Default for PrioritizedDistInner {
fn default() -> Self {
Self {
source: None,
best_wheel_index: None,
wheels: Vec::new(),
variants_json: Vec::new(),
hashes: Vec::new(),
python_markers: MarkerTree::FALSE,
markers: [MarkerTree::FALSE; 2],
}
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum CompatibleDist<'a> {
InstalledDist(&'a InstalledDist),
SourceDist {
sdist: &'a RegistrySourceDist,
prioritized: &'a PrioritizedDist,
},
CompatibleWheel {
wheel: &'a RegistryBuiltWheel,
prioritized: &'a PrioritizedDist,
},
IncompatibleWheel {
sdist: &'a RegistrySourceDist,
wheel: &'a RegistryBuiltWheel,
prioritized: &'a PrioritizedDist,
},
}
impl Name for CompatibleDist<'_> {
fn name(&self) -> &PackageName {
match self {
Self::InstalledDist(dist) => dist.name(),
Self::SourceDist {
sdist,
prioritized: _,
} => sdist.name(),
Self::CompatibleWheel {
wheel,
prioritized: _,
} => wheel.name(),
Self::IncompatibleWheel {
sdist,
wheel: _,
prioritized: _,
} => sdist.name(),
}
}
}
impl CompatibleDist<'_> {
pub(crate) fn matches_python_requirement(&self, requires_python: &RequiresPython) -> bool {
self.prioritized().is_none_or(|prioritized| {
!prioritized
.0
.python_markers
.is_disjoint(requires_python.to_marker_tree())
})
}
pub(crate) fn requires_python(&self) -> Option<&VersionSpecifiers> {
match self {
Self::InstalledDist(_) => None,
Self::SourceDist { sdist, .. } => sdist.file.requires_python.as_deref(),
Self::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_deref(),
Self::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_deref(),
}
}
pub(crate) fn prioritized(&self) -> Option<&PrioritizedDist> {
match self {
Self::InstalledDist(_) => None,
Self::SourceDist { prioritized, .. }
| Self::CompatibleWheel { prioritized, .. }
| Self::IncompatibleWheel { prioritized, .. } => Some(prioritized),
}
}
pub(crate) fn implied_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => {
let [glibc, musl] = prioritized.0.markers;
glibc.and(musl)
}
None => MarkerTree::TRUE,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleDist {
Wheel(IncompatibleWheel),
Source(IncompatibleSource),
Unavailable,
}
impl IncompatibleDist {
pub(crate) fn singular_message(&self) -> String {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::NoBinary => format!("has {self}"),
IncompatibleWheel::Tag(_) => format!("has {self}"),
IncompatibleWheel::Yanked(_) => format!("was {self}"),
IncompatibleWheel::ExcludeNewer(ts) => match ts {
Some(_) => format!("was {self}"),
None => format!("has {self}"),
},
IncompatibleWheel::RequiresPython(..) => format!("requires {self}"),
IncompatibleWheel::MissingPlatform(_) => format!("has {self}"),
},
Self::Source(incompatibility) => match incompatibility {
IncompatibleSource::NoBuild => format!("has {self}"),
IncompatibleSource::Yanked(_) => format!("was {self}"),
IncompatibleSource::ExcludeNewer(ts) => match ts {
Some(_) => format!("was {self}"),
None => format!("has {self}"),
},
IncompatibleSource::RequiresPython(..) => {
format!("requires {self}")
}
IncompatibleSource::NotPep625Filename => format!("has {self}"),
},
Self::Unavailable => format!("has {self}"),
}
}
pub(crate) fn plural_message(&self) -> String {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::NoBinary => format!("have {self}"),
IncompatibleWheel::Tag(_) => format!("have {self}"),
IncompatibleWheel::Yanked(_) => format!("were {self}"),
IncompatibleWheel::ExcludeNewer(ts) => match ts {
Some(_) => format!("were {self}"),
None => format!("have {self}"),
},
IncompatibleWheel::RequiresPython(..) => format!("require {self}"),
IncompatibleWheel::MissingPlatform(_) => format!("have {self}"),
},
Self::Source(incompatibility) => match incompatibility {
IncompatibleSource::NoBuild => format!("have {self}"),
IncompatibleSource::Yanked(_) => format!("were {self}"),
IncompatibleSource::ExcludeNewer(ts) => match ts {
Some(_) => format!("were {self}"),
None => format!("have {self}"),
},
IncompatibleSource::RequiresPython(..) => {
format!("require {self}")
}
IncompatibleSource::NotPep625Filename => format!("have {self}"),
},
Self::Unavailable => format!("have {self}"),
}
}
pub(crate) fn context_message(
&self,
tags: Option<&Tags>,
requires_python: Option<AbiTag>,
) -> Option<String> {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::Tag(IncompatibleTag::Python) => {
let tag = tags?.python_tag().as_ref().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Abi) => {
let tag = tags?.abi_tag().as_ref().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::FreethreadedAbi) => None,
IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => {
let tag = requires_python?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Platform) => {
let tag = tags?.platform_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Invalid) => None,
IncompatibleWheel::Tag(IncompatibleTag::Variant) => None,
IncompatibleWheel::NoBinary => None,
IncompatibleWheel::Yanked(..) => None,
IncompatibleWheel::ExcludeNewer(..) => None,
IncompatibleWheel::RequiresPython(..) => None,
IncompatibleWheel::MissingPlatform(..) => None,
},
Self::Source(..) => None,
Self::Unavailable => None,
}
}
}
impl Display for IncompatibleDist {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::NoBinary => f.write_str("no source distribution"),
IncompatibleWheel::Tag(tag) => match tag {
IncompatibleTag::Variant => f.write_str("only unsupported wheel variants"),
IncompatibleTag::Invalid => f.write_str("no wheels with valid tags"),
IncompatibleTag::Python => {
f.write_str("no wheels with a matching Python implementation tag")
}
IncompatibleTag::Abi => f.write_str("no wheels with a matching Python ABI tag"),
IncompatibleTag::FreethreadedAbi => {
f.write_str("no wheels with a free-threading compatible ABI tag")
}
IncompatibleTag::AbiPythonVersion => {
f.write_str("no wheels with a matching Python version tag")
}
IncompatibleTag::Platform => {
f.write_str("no wheels with a matching platform tag")
}
},
IncompatibleWheel::Yanked(yanked) => match yanked {
Yanked::Bool(_) => f.write_str("yanked"),
Yanked::Reason(reason) => write!(
f,
"yanked (reason: {})",
reason.trim().trim_end_matches('.')
),
},
IncompatibleWheel::ExcludeNewer(ts) => match ts {
Some(_) => f.write_str("published after the exclude newer time"),
None => f.write_str("no publish time"),
},
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"),
IncompatibleSource::Yanked(yanked) => match yanked {
Yanked::Bool(_) => f.write_str("yanked"),
Yanked::Reason(reason) => write!(
f,
"yanked (reason: {})",
reason.trim().trim_end_matches('.')
),
},
IncompatibleSource::ExcludeNewer(ts) => match ts {
Some(_) => f.write_str("published after the exclude newer time"),
None => f.write_str("no publish time"),
},
IncompatibleSource::RequiresPython(python, _) => {
write!(f, "Python {python}")
}
IncompatibleSource::NotPep625Filename => {
f.write_str("a non-PEP 625-compliant source distribution filename")
}
},
Self::Unavailable => f.write_str("no available distributions"),
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum PythonRequirementKind {
Installed,
Target,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum WheelCompatibility {
Incompatible(IncompatibleWheel),
Compatible(HashComparison, Option<TagPriority>, Option<BuildTag>),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleWheel {
ExcludeNewer(Option<i64>),
Tag(IncompatibleTag),
RequiresPython(VersionSpecifiers, PythonRequirementKind),
Yanked(Yanked),
NoBinary,
MissingPlatform(MarkerTree),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceDistCompatibility {
Incompatible(IncompatibleSource),
Compatible(HashComparison),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleSource {
ExcludeNewer(Option<i64>),
RequiresPython(VersionSpecifiers, PythonRequirementKind),
Yanked(Yanked),
NoBuild,
NotPep625Filename,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum HashComparison {
Mismatched,
Missing,
Matched,
}
impl PrioritizedDist {
pub(crate) fn insert_variants_json(&mut self, variants_json: RegistryVariantsJson) {
if !self.0.variants_json.contains(&variants_json) {
self.0.variants_json.push(variants_json);
}
}
pub(crate) fn variants_json(&self) -> &[RegistryVariantsJson] {
&self.0.variants_json
}
pub(crate) fn insert_built(
&mut self,
dist: RegistryBuiltWheel,
hashes: impl IntoIterator<Item = HashDigest>,
compatibility: WheelCompatibility,
minimum_libc_version: Option<MinimumLibcVersion>,
) {
if compatibility.is_compatible()
&& (!self.0.python_markers.is_true()
|| !self.0.markers.iter().all(|markers| markers.is_true()))
{
let python = implied_python_markers(&dist.filename);
self.0.python_markers = self.0.python_markers.or(python);
for (coverage, markers) in self.0.markers.iter_mut().zip(implied_libc_markers(
&dist.filename,
python,
minimum_libc_version,
)) {
*coverage = coverage.or(markers);
}
}
if !compatibility.is_excluded() {
self.0.hashes.extend(hashes);
}
if let Some((existing_wheel, existing_compatibility)) = self.best_wheel() {
let is_more_compatible =
match (dist.filename.variant(), existing_wheel.filename.variant()) {
(None, Some(_)) => true,
(Some(_), None) => false,
(None, None) | (Some(_), Some(_)) => {
compatibility.is_more_compatible(existing_compatibility)
}
};
if is_more_compatible {
self.0.best_wheel_index = Some(self.0.wheels.len());
}
} else {
self.0.best_wheel_index = Some(self.0.wheels.len());
}
self.0.wheels.push((dist, compatibility));
}
pub fn insert_source(
&mut self,
dist: RegistrySourceDist,
hashes: impl IntoIterator<Item = HashDigest>,
compatibility: SourceDistCompatibility,
) {
if compatibility.is_compatible() {
self.0.python_markers = MarkerTree::TRUE;
self.0.markers = [MarkerTree::TRUE; 2];
}
if !compatibility.is_excluded() {
self.0.hashes.extend(hashes);
}
if let Some((.., existing_compatibility)) = &self.0.source {
if compatibility.is_more_compatible(existing_compatibility) {
self.0.source = Some((dist, compatibility));
}
} else {
self.0.source = Some((dist, compatibility));
}
}
pub(crate) fn get(&self) -> Option<CompatibleDist<'_>> {
let best_wheel = self.0.best_wheel_index.map(|i| &self.0.wheels[i]);
match (&best_wheel, &self.0.source) {
(
Some((wheel, WheelCompatibility::Compatible(wheel_hash, ..))),
Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))),
) => {
if sdist_hash > wheel_hash {
Some(CompatibleDist::SourceDist {
sdist,
prioritized: self,
})
} else {
Some(CompatibleDist::CompatibleWheel {
wheel,
prioritized: self,
})
}
}
(Some((wheel, WheelCompatibility::Compatible(..))), _) => {
Some(CompatibleDist::CompatibleWheel {
wheel,
prioritized: self,
})
}
(
Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))),
Some((sdist, SourceDistCompatibility::Compatible(_))),
) if !compatibility.is_excluded() && wheel.filename.variant().is_none() => {
Some(CompatibleDist::IncompatibleWheel {
sdist,
wheel,
prioritized: self,
})
}
(.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
Some(CompatibleDist::SourceDist {
sdist,
prioritized: self,
})
}
_ => None,
}
}
pub(crate) fn incompatible_source(&self) -> Option<&IncompatibleSource> {
self.0
.source
.as_ref()
.and_then(|(_, compatibility)| match compatibility {
SourceDistCompatibility::Compatible(_) => None,
SourceDistCompatibility::Incompatible(incompatibility) => Some(incompatibility),
})
}
pub(crate) fn incompatible_wheel(&self) -> Option<&IncompatibleWheel> {
self.0
.best_wheel_index
.map(|i| &self.0.wheels[i])
.and_then(|(_, compatibility)| match compatibility {
WheelCompatibility::Compatible(_, _, _) => None,
WheelCompatibility::Incompatible(incompatibility) => Some(incompatibility),
})
}
pub(crate) fn hashes(&self) -> &[HashDigest] {
&self.0.hashes
}
pub(crate) fn is_empty(&self) -> bool {
self.0.source.is_none() && self.0.wheels.is_empty()
}
pub(crate) fn built_dist(&self) -> Option<RegistryBuiltDist> {
let best_wheel_index = self.0.best_wheel_index?;
let mut adjusted_wheels = Vec::with_capacity(self.0.wheels.len());
let mut adjusted_best_index = 0;
for (i, (wheel, compatibility)) in self.0.wheels.iter().enumerate() {
if compatibility.is_excluded() {
continue;
}
if i == best_wheel_index {
adjusted_best_index = adjusted_wheels.len();
}
adjusted_wheels.push(wheel.clone());
}
let sdist = self
.0
.source
.as_ref()
.filter(|(_, compatibility)| !compatibility.is_excluded())
.map(|(sdist, _)| sdist.clone());
Some(RegistryBuiltDist {
wheels: adjusted_wheels,
best_wheel_index: adjusted_best_index,
sdist,
})
}
pub(crate) fn source_dist(&self) -> Option<RegistrySourceDist> {
let mut sdist = self
.0
.source
.as_ref()
.filter(|(_, compatibility)| !compatibility.is_excluded())
.map(|(sdist, _)| sdist.clone())?;
assert!(
sdist.wheels.is_empty(),
"source distribution should not have any wheels yet"
);
sdist.wheels = self
.0
.wheels
.iter()
.filter(|(_, compatibility)| !compatibility.is_excluded())
.map(|(wheel, _)| wheel.clone())
.collect();
Some(sdist)
}
pub(crate) fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
self.0.best_wheel_index.map(|i| &self.0.wheels[i])
}
pub(crate) fn files(&self) -> impl Iterator<Item = &File> {
self.0
.wheels
.iter()
.map(|(wheel, _)| wheel.file.as_ref())
.chain(
self.0
.source
.as_ref()
.map(|(source_dist, _)| source_dist.file.as_ref()),
)
}
pub(crate) fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied())
}
pub(crate) fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied())
}
pub(crate) fn platform_tags<'a>(
&'a self,
tags: &'a Tags,
) -> impl Iterator<Item = &'a PlatformTag> + 'a {
self.0.wheels.iter().flat_map(move |(wheel, _)| {
if wheel.filename.python_tags().iter().any(|wheel_py| {
wheel
.filename
.abi_tags()
.iter()
.any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
}) {
wheel.filename.platform_tags().iter()
} else {
[].iter()
}
})
}
}
impl<'a> CompatibleDist<'a> {
pub(crate) fn for_resolution(&self) -> ResolvedDistRef<'a> {
match self {
Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
Self::SourceDist { sdist, prioritized } => {
ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
}
Self::CompatibleWheel {
wheel, prioritized, ..
} => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
Self::IncompatibleWheel {
wheel, prioritized, ..
} => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
}
}
pub(crate) fn for_installation(&self) -> ResolvedDistRef<'a> {
match self {
Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
Self::SourceDist { sdist, prioritized } => {
ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
}
Self::CompatibleWheel {
wheel, prioritized, ..
} => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
Self::IncompatibleWheel {
sdist, prioritized, ..
} => ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized },
}
}
pub(crate) fn wheel(&self) -> Option<&RegistryBuiltWheel> {
match self {
Self::InstalledDist(_) => None,
Self::SourceDist { .. } => None,
Self::CompatibleWheel { wheel, .. } => Some(wheel),
Self::IncompatibleWheel { wheel, .. } => Some(wheel),
}
}
}
impl WheelCompatibility {
fn is_compatible(&self) -> bool {
matches!(self, Self::Compatible(_, _, _))
}
fn is_excluded(&self) -> bool {
matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
}
fn is_more_compatible(&self, other: &Self) -> bool {
match (self, other) {
(Self::Compatible(_, _, _), Self::Incompatible(_)) => true,
(
Self::Compatible(hash, tag_priority, build_tag),
Self::Compatible(other_hash, other_tag_priority, other_build_tag),
) => {
(hash, tag_priority, build_tag) > (other_hash, other_tag_priority, other_build_tag)
}
(Self::Incompatible(_), Self::Compatible(_, _, _)) => false,
(Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
incompatibility.is_more_compatible(other_incompatibility)
}
}
}
}
impl SourceDistCompatibility {
fn is_compatible(&self) -> bool {
matches!(self, Self::Compatible(_))
}
fn is_excluded(&self) -> bool {
matches!(
self,
Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
)
}
fn is_more_compatible(&self, other: &Self) -> bool {
match (self, other) {
(Self::Compatible(_), Self::Incompatible(_)) => true,
(Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => {
compatibility > other_compatibility
}
(Self::Incompatible(_), Self::Compatible(_)) => false,
(Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
incompatibility.is_more_compatible(other_incompatibility)
}
}
}
}
impl IncompatibleSource {
fn is_more_compatible(&self, other: &Self) -> bool {
match self {
Self::ExcludeNewer(timestamp_self) => match other {
Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
Self::NoBuild
| Self::RequiresPython(_, _)
| Self::Yanked(_)
| Self::NotPep625Filename => true,
},
Self::RequiresPython(_, _) => match other {
Self::ExcludeNewer(_) => false,
Self::RequiresPython(_, _) => false,
Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true,
},
Self::Yanked(_) => match other {
Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => {
false
}
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
Self::NoBuild => true,
},
Self::NoBuild | Self::NotPep625Filename => false,
}
}
}
impl IncompatibleWheel {
fn is_more_compatible(&self, other: &Self) -> bool {
match self {
Self::ExcludeNewer(timestamp_self) => match other {
Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
(None, _) => true,
(_, None) => false,
(Some(timestamp_self), Some(timestamp_other)) => {
timestamp_other < timestamp_self
}
},
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::MissingPlatform(_)
| Self::NoBinary
| Self::RequiresPython(_, _)
| Self::Yanked(_) => true,
},
Self::RequiresPython(_, _) => match other {
Self::ExcludeNewer(_) | Self::Tag(_) => false,
Self::RequiresPython(_, _) => false,
Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true,
},
Self::Yanked(_) => match other {
Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false,
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
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::MissingPlatform(_) => false,
}
}
}
pub(crate) fn implied_markers(
filename: &WheelFilename,
minimum_libc_version: Option<MinimumLibcVersion>,
) -> MarkerTree {
let python = implied_python_markers(filename);
let [glibc, musl] = implied_libc_markers(filename, python, minimum_libc_version);
glibc.and(musl)
}
fn implied_libc_markers(
filename: &WheelFilename,
python: MarkerTree,
minimum_libc_version: Option<MinimumLibcVersion>,
) -> [MarkerTree; 2] {
let Some(minimum_libc_version) = minimum_libc_version else {
return [implied_platform_markers(filename.platform_tags()).and(python); 2];
};
let mut markers = [MarkerTree::FALSE; 2];
for tag in filename.platform_tags() {
let platform = implied_platform_markers([tag]).and(python);
for (markers, supported) in markers
.iter_mut()
.zip(minimum_libc_version.platform_coverage(tag))
{
if supported {
*markers = markers.or(platform);
}
}
}
markers
}
fn implied_platform_markers<'a>(
platform_tags: impl IntoIterator<Item = &'a PlatformTag>,
) -> MarkerTree {
let mut marker = MarkerTree::FALSE;
for platform_tag in platform_tags {
match platform_tag {
PlatformTag::Any => {
return MarkerTree::TRUE;
}
PlatformTag::Win32 => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("win32"),
});
tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: arcstr::literal!("x86"),
}));
marker = marker.or(tag_marker);
}
PlatformTag::WinAmd64 => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("win32"),
});
tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: arcstr::literal!("AMD64"),
}));
marker = marker.or(tag_marker);
}
PlatformTag::WinArm64 => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("win32"),
});
tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: arcstr::literal!("ARM64"),
}));
marker = marker.or(tag_marker);
}
PlatformTag::Macos {
major,
minor,
binary_format,
} => {
let Some(release) = macos_darwin_release(*major, *minor) else {
continue;
};
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("darwin"),
});
let mut arch_marker = MarkerTree::FALSE;
for arch in binary_format.platform_machine() {
let release = if *arch == BinaryFormat::Arm64 {
release.clone().max(Version::new([20, 0, 0]))
} else {
release.clone()
};
let architecture = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: ArcStr::from(arch.name()),
});
let release = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformRelease,
operator: MarkerOperator::GreaterEqual,
value: ArcStr::from(release.to_string()),
});
arch_marker = arch_marker.or(architecture.and(release));
}
tag_marker = tag_marker.and(arch_marker);
marker = marker.or(tag_marker);
}
PlatformTag::Manylinux { arch, .. }
| PlatformTag::Manylinux1 { arch, .. }
| PlatformTag::Manylinux2010 { arch, .. }
| PlatformTag::Manylinux2014 { arch, .. }
| PlatformTag::Musllinux { arch, .. }
| PlatformTag::Linux { arch } => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("linux"),
});
tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: ArcStr::from(arch.name()),
}));
marker = marker.or(tag_marker);
}
tag => {
debug!("Unknown platform tag in wheel tag: {tag}");
}
}
}
marker
}
fn macos_darwin_release(major: u16, minor: u16) -> Option<Version> {
let release = match (major, minor) {
(10, 0) => [1, 3, 0],
(10, 1) => [1, 4, 1],
(10, 2..=16) => [u64::from(minor) + 4, 0, 0],
(11..=15, 0) => [u64::from(major) + 9, 0, 0],
(26, 0) => [25, 0, 0],
(27.., 0) => [u64::from(major), 0, 0],
_ => return None,
};
Some(Version::new(release))
}
fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = MarkerTree::FALSE;
let is_abi3 = filename.abi_tags().iter().any(|tag| tag.is_stable_abi());
for python_tag in filename.python_tags() {
let mut tree = match python_tag {
LanguageTag::None => {
return MarkerTree::TRUE;
}
LanguageTag::Python { major, minor: None } | LanguageTag::CPythonMajor { major } => {
if is_abi3 {
MarkerTree::expression(MarkerExpression::Version {
key: uv_pep508::MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::greater_than_equal_version(Version::new([
u64::from(*major),
])),
})
} else {
MarkerTree::expression(MarkerExpression::Version {
key: uv_pep508::MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::equals_star_version(Version::new([
u64::from(*major),
])),
})
}
}
LanguageTag::Python {
major,
minor: Some(minor),
} => {
MarkerTree::expression(MarkerExpression::Version {
key: uv_pep508::MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::greater_than_equal_version(Version::new([
u64::from(*major),
u64::from(*minor),
])),
})
.and(MarkerTree::expression(MarkerExpression::Version {
key: uv_pep508::MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::equals_star_version(Version::new([u64::from(
*major,
)])),
}))
}
LanguageTag::CPython {
python_version: (major, minor),
}
| LanguageTag::PyPy {
python_version: (major, minor),
}
| LanguageTag::GraalPy {
python_version: (major, minor),
}
| LanguageTag::Pyston {
python_version: (major, minor),
} => {
if is_abi3 {
MarkerTree::expression(MarkerExpression::Version {
key: uv_pep508::MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::greater_than_equal_version(Version::new([
u64::from(*major),
u64::from(*minor),
])),
})
} else {
MarkerTree::expression(MarkerExpression::Version {
key: uv_pep508::MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::equals_star_version(Version::new([
u64::from(*major),
u64::from(*minor),
])),
})
}
}
};
match python_tag {
LanguageTag::None | LanguageTag::Python { .. } => {
}
LanguageTag::CPython { .. } | LanguageTag::CPythonMajor { .. } => {
tree = tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("CPython"),
}));
}
LanguageTag::PyPy { .. } => {
tree = tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("PyPy"),
}));
}
LanguageTag::GraalPy { .. } => {
tree = tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("GraalPy"),
}));
}
LanguageTag::Pyston { .. } => {
tree = tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("Pyston"),
}));
}
}
marker = marker.or(tree);
}
marker
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[track_caller]
fn assert_platform_markers(filename: &str, expected: &str) {
let filename = WheelFilename::from_str(filename).unwrap();
assert_eq!(
implied_platform_markers(filename.platform_tags()),
expected.parse::<MarkerTree>().unwrap()
);
}
#[track_caller]
fn assert_python_markers(filename: &str, expected: &str) {
let filename = WheelFilename::from_str(filename).unwrap();
assert_eq!(
implied_python_markers(&filename),
expected.parse::<MarkerTree>().unwrap()
);
}
#[track_caller]
fn assert_implied_markers(filename: &str, expected: &str) {
let filename = WheelFilename::from_str(filename).unwrap();
assert_eq!(
implied_markers(&filename, None),
expected.parse::<MarkerTree>().unwrap()
);
}
#[test]
fn test_implied_platform_markers() {
let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
assert_eq!(
implied_platform_markers(filename.platform_tags()),
MarkerTree::TRUE
);
assert_platform_markers(
"example-1.0-cp310-cp310-win32.whl",
"sys_platform == 'win32' and platform_machine == 'x86'",
);
assert_platform_markers(
"numpy-2.2.1-cp313-cp313t-win_amd64.whl",
"sys_platform == 'win32' and platform_machine == 'AMD64'",
);
assert_platform_markers(
"numpy-2.2.1-cp313-cp313t-win_arm64.whl",
"sys_platform == 'win32' and platform_machine == 'ARM64'",
);
assert_platform_markers(
"numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
"sys_platform == 'linux' and platform_machine == 'aarch64'",
);
assert_platform_markers(
"numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"sys_platform == 'linux' and platform_machine == 'x86_64'",
);
assert_platform_markers(
"numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",
"sys_platform == 'linux' and platform_machine == 'aarch64'",
);
assert_platform_markers(
"numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl",
"sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '23.0.0'",
);
assert_platform_markers(
"numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl",
"sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '13.0.0'",
);
assert_platform_markers(
"numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl",
"sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '20.0.0'",
);
}
#[test]
fn test_implied_python_markers() {
let filename = WheelFilename::from_str("example-1.0-none-none-any.whl").unwrap();
assert_eq!(implied_python_markers(&filename), MarkerTree::TRUE);
assert_python_markers(
"example-1.0-cp310-cp310-any.whl",
"python_full_version == '3.10.*' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp311-cp311-any.whl",
"python_full_version == '3.11.*' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp312-cp312-any.whl",
"python_full_version == '3.12.*' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp313-cp313-any.whl",
"python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp313-cp313t-any.whl",
"python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-pp310-pypy310_pp73-any.whl",
"python_full_version == '3.10.*' and platform_python_implementation == 'PyPy'",
);
assert_python_markers(
"example-1.0-py310-none-any.whl",
"python_full_version >= '3.10' and python_full_version < '4'",
);
assert_python_markers(
"example-1.0-py3-none-any.whl",
"python_full_version >= '3' and python_full_version < '4'",
);
assert_python_markers(
"example-1.0-py311.py312-none-any.whl",
"python_full_version >= '3.11' and python_full_version < '4'",
);
assert_python_markers(
"example-1.0-cp39-abi3-any.whl",
"python_full_version >= '3.9' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp312-abi3-any.whl",
"python_full_version >= '3.12' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp315-abi3t-any.whl",
"python_full_version >= '3.15' and platform_python_implementation == 'CPython'",
);
assert_python_markers(
"example-1.0-cp3-abi3-any.whl",
"python_full_version >= '3' and platform_python_implementation == 'CPython'",
);
}
#[test]
fn test_macos_platform_markers() {
for (tag, expected) in [
(
"macosx_10_5_x86_64",
"sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '9.0.0'",
),
(
"macosx_10_9_universal2",
"sys_platform == 'darwin' and ((platform_machine == 'x86_64' and platform_release >= '13.0.0') or (platform_machine == 'arm64' and platform_release >= '20.0.0'))",
),
(
"macosx_10_16_universal2",
"sys_platform == 'darwin' and (platform_machine == 'arm64' or platform_machine == 'x86_64') and platform_release >= '20.0.0'",
),
(
"macosx_15_0_arm64",
"sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '24.0.0'",
),
(
"macosx_26_0_arm64",
"sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '25.0.0'",
),
(
"macosx_27_0_arm64",
"sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '27.0.0'",
),
(
"macosx_26_0_arm64.macosx_15_0_x86_64",
"sys_platform == 'darwin' and ((platform_machine == 'arm64' and platform_release >= '25.0.0') or (platform_machine == 'x86_64' and platform_release >= '24.0.0'))",
),
] {
let filename =
WheelFilename::from_str(&format!("example-1.0-py3-none-{tag}.whl")).unwrap();
assert_eq!(
implied_platform_markers(filename.platform_tags()),
MarkerTree::from_str(expected).unwrap()
);
}
}
#[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' and platform_release >= '13.0.0'",
);
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'",
);
assert_implied_markers(
"example-1.0-cp39-abi3-manylinux_2_28_x86_64.whl",
"python_full_version >= '3.9' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
);
assert_implied_markers(
"example-1.0-cp315-abi3t-manylinux_2_28_x86_64.whl",
"python_full_version >= '3.15' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
);
}
}