use std::fmt::{Display, Formatter};
use arcstr::ArcStr;
use owo_colors::OwoColorize;
use tracing::debug;
use uv_distribution_filename::{BuildTag, WheelFilename};
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagPriority, Tags};
use uv_pypi_types::{HashDigest, Yanked};
use crate::{
File, InstalledDist, KnownPlatform, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist,
ResolvedDistRef,
};
#[derive(Debug, Default, Clone)]
pub struct PrioritizedDist(Box<PrioritizedDistInner>);
#[derive(Debug, Clone)]
struct PrioritizedDistInner {
source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
best_wheel_index: Option<usize>,
wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
hashes: Vec<HashDigest>,
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,
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum CompatibleDist<'a> {
InstalledDist(&'a InstalledDist),
SourceDist {
sdist: &'a RegistrySourceDist,
prioritized: &'a PrioritizedDist,
},
CompatibleWheel {
wheel: &'a RegistryBuiltWheel,
priority: Option<TagPriority>,
prioritized: &'a PrioritizedDist,
},
IncompatibleWheel {
sdist: &'a RegistrySourceDist,
wheel: &'a RegistryBuiltWheel,
prioritized: &'a PrioritizedDist,
},
}
impl CompatibleDist<'_> {
pub fn requires_python(&self) -> Option<&VersionSpecifiers> {
match self {
Self::InstalledDist(_) => None,
Self::SourceDist { sdist, .. } => sdist.file.requires_python.as_ref(),
Self::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_ref(),
Self::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_ref(),
}
}
pub fn prioritized(&self) -> Option<&PrioritizedDist> {
match self {
Self::InstalledDist(_) => None,
Self::SourceDist { prioritized, .. }
| Self::CompatibleWheel { prioritized, .. }
| Self::IncompatibleWheel { prioritized, .. } => Some(prioritized),
}
}
pub fn implied_markers(&self) -> MarkerTree {
match self.prioritized() {
Some(prioritized) => prioritized.0.markers,
None => MarkerTree::TRUE,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleDist {
Wheel(IncompatibleWheel),
Source(IncompatibleSource),
Unavailable,
}
impl IncompatibleDist {
pub 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}")
}
},
Self::Unavailable => format!("has {self}"),
}
}
pub 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}")
}
},
Self::Unavailable => format!("have {self}"),
}
}
pub 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::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::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::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::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}")
}
},
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 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,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum HashComparison {
Mismatched,
Missing,
Matched,
}
impl PrioritizedDist {
pub fn from_built(
dist: RegistryBuiltWheel,
hashes: Vec<HashDigest>,
compatibility: WheelCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: implied_markers(&dist.filename),
best_wheel_index: Some(0),
wheels: vec![(dist, compatibility)],
source: None,
hashes,
}))
}
pub fn from_source(
dist: RegistrySourceDist,
hashes: Vec<HashDigest>,
compatibility: SourceDistCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: MarkerTree::TRUE,
best_wheel_index: None,
wheels: vec![],
source: Some((dist, compatibility)),
hashes,
}))
}
pub fn insert_built(
&mut self,
dist: RegistryBuiltWheel,
hashes: impl IntoIterator<Item = HashDigest>,
compatibility: WheelCompatibility,
) {
if compatibility.is_compatible() {
if !self.0.markers.is_true() {
self.0.markers.or(implied_markers(&dist.filename));
}
}
if !compatibility.is_excluded() {
self.0.hashes.extend(hashes);
}
if let Some((.., existing_compatibility)) = self.best_wheel() {
if compatibility.is_more_compatible(existing_compatibility) {
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.markers = MarkerTree::TRUE;
}
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 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, tag_priority, ..))),
Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))),
) => {
if sdist_hash > wheel_hash {
Some(CompatibleDist::SourceDist {
sdist,
prioritized: self,
})
} else {
Some(CompatibleDist::CompatibleWheel {
wheel,
priority: *tag_priority,
prioritized: self,
})
}
}
(Some((wheel, WheelCompatibility::Compatible(_, tag_priority, ..))), _) => {
Some(CompatibleDist::CompatibleWheel {
wheel,
priority: *tag_priority,
prioritized: self,
})
}
(
Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))),
Some((sdist, SourceDistCompatibility::Compatible(_))),
) if !compatibility.is_excluded() => Some(CompatibleDist::IncompatibleWheel {
sdist,
wheel,
prioritized: self,
}),
(.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
Some(CompatibleDist::SourceDist {
sdist,
prioritized: self,
})
}
_ => None,
}
}
pub 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 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 fn hashes(&self) -> &[HashDigest] {
&self.0.hashes
}
pub fn is_empty(&self) -> bool {
self.0.source.is_none() && self.0.wheels.is_empty()
}
pub 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().map(|(sdist, _)| sdist.clone());
Some(RegistryBuiltDist {
wheels: adjusted_wheels,
best_wheel_index: adjusted_best_index,
sdist,
})
}
pub 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()
.map(|(wheel, _)| wheel.clone())
.collect();
Some(sdist)
}
pub fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
self.0.best_wheel_index.map(|i| &self.0.wheels[i])
}
pub 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 fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied())
}
pub fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied())
}
pub 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 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 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 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 {
pub fn is_compatible(&self) -> bool {
matches!(self, Self::Compatible(_, _, _))
}
pub fn is_excluded(&self) -> bool {
matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
}
pub 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 {
pub fn is_compatible(&self) -> bool {
matches!(self, Self::Compatible(_))
}
pub fn is_excluded(&self) -> bool {
matches!(
self,
Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
)
}
pub 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(_) => true,
},
Self::RequiresPython(_, _) => match other {
Self::ExcludeNewer(_) => false,
Self::RequiresPython(_, _) => false,
Self::NoBuild | Self::Yanked(_) => true,
},
Self::Yanked(_) => match other {
Self::ExcludeNewer(_) | Self::RequiresPython(_, _) => false,
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
Self::NoBuild => true,
},
Self::NoBuild => false,
}
}
}
impl IncompatibleWheel {
#[allow(clippy::match_like_matches_macro)]
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 fn implied_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = implied_platform_markers(filename);
marker.and(implied_python_markers(filename));
marker
}
fn implied_platform_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = MarkerTree::FALSE;
for platform_tag in filename.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.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: arcstr::literal!("x86"),
}));
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.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: arcstr::literal!("AMD64"),
}));
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.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: arcstr::literal!("ARM64"),
}));
marker.or(tag_marker);
}
PlatformTag::Macos { binary_format, .. } => {
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() {
arch_marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: ArcStr::from(arch.name()),
}));
}
tag_marker.and(arch_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.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: ArcStr::from(arch.name()),
}));
marker.or(tag_marker);
}
tag => {
debug!("Unknown platform tag in wheel tag: {tag}");
}
}
}
marker
}
fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = MarkerTree::FALSE;
for python_tag in filename.python_tags() {
let mut tree = match python_tag {
LanguageTag::None => {
return MarkerTree::TRUE;
}
LanguageTag::Python { major, minor: None } => {
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),
}
| LanguageTag::CPython {
python_version: (major, minor),
}
| LanguageTag::PyPy {
python_version: (major, minor),
}
| LanguageTag::GraalPy {
python_version: (major, minor),
}
| LanguageTag::Pyston {
python_version: (major, minor),
} => 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 { .. } => {
tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("CPython"),
}));
}
LanguageTag::PyPy { .. } => {
tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("PyPy"),
}));
}
LanguageTag::GraalPy { .. } => {
tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("GraalPy"),
}));
}
LanguageTag::Pyston { .. } => {
tree.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformPythonImplementation,
operator: MarkerOperator::Equal,
value: arcstr::literal!("Pyston"),
}));
}
}
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),
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),
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), 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'",
);
assert_platform_markers(
"numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl",
"sys_platform == 'darwin' and platform_machine == 'x86_64'",
);
assert_platform_markers(
"numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl",
"sys_platform == 'darwin' and platform_machine == 'arm64'",
);
}
#[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 < '3.11'",
);
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 < '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'",
);
}
}