1use std::fmt::{Display, Formatter};
2
3use arcstr::ArcStr;
4use owo_colors::OwoColorize;
5use tracing::debug;
6
7use uv_distribution_filename::{BuildTag, WheelFilename};
8use uv_distribution_types::{
9 File, IndexUrl, InstalledDist, MinimumLibcVersion, Name, RegistryBuiltDist, RegistryBuiltWheel,
10 RegistrySourceDist, RequiresPython, VariantsJsonContent, VariantsJsonFilename,
11};
12use uv_normalize::PackageName;
13use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
14use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
15use uv_platform_tags::{
16 AbiTag, BinaryFormat, IncompatibleTag, LanguageTag, PlatformTag, TagPriority, Tags,
17};
18use uv_pypi_types::{HashDigest, Yanked};
19
20use crate::known_platform::KnownPlatform;
21use crate::resolved::ResolvedDistRef;
22
23#[derive(Debug, Default, Clone)]
25pub struct PrioritizedDist(Box<PrioritizedDistInner>);
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub(crate) struct RegistryVariantsJson {
30 pub(crate) filename: VariantsJsonFilename,
31 pub(crate) file: File,
32 pub(crate) index: IndexUrl,
33}
34
35#[derive(Debug, Clone)]
37struct PrioritizedDistInner {
38 source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
40 best_wheel_index: Option<usize>,
43 wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
45 variants_json: Vec<RegistryVariantsJson>,
47 hashes: Vec<HashDigest>,
49 python_markers: MarkerTree,
51 markers: [MarkerTree; 2],
55}
56
57impl Default for PrioritizedDistInner {
58 fn default() -> Self {
59 Self {
60 source: None,
61 best_wheel_index: None,
62 wheels: Vec::new(),
63 variants_json: Vec::new(),
64 hashes: Vec::new(),
65 python_markers: MarkerTree::FALSE,
66 markers: [MarkerTree::FALSE; 2],
67 }
68 }
69}
70
71#[derive(Debug, Copy, Clone)]
73pub(crate) enum CompatibleDist<'a> {
74 InstalledDist(&'a InstalledDist),
76 SourceDist {
78 sdist: &'a RegistrySourceDist,
80 prioritized: &'a PrioritizedDist,
82 },
83 CompatibleWheel {
85 wheel: &'a RegistryBuiltWheel,
87 prioritized: &'a PrioritizedDist,
89 },
90 IncompatibleWheel {
93 sdist: &'a RegistrySourceDist,
95 wheel: &'a RegistryBuiltWheel,
97 prioritized: &'a PrioritizedDist,
99 },
100}
101
102impl Name for CompatibleDist<'_> {
103 fn name(&self) -> &PackageName {
104 match self {
105 Self::InstalledDist(dist) => dist.name(),
106 Self::SourceDist {
107 sdist,
108 prioritized: _,
109 } => sdist.name(),
110 Self::CompatibleWheel {
111 wheel,
112 prioritized: _,
113 } => wheel.name(),
114 Self::IncompatibleWheel {
115 sdist,
116 wheel: _,
117 prioritized: _,
118 } => sdist.name(),
119 }
120 }
121}
122
123impl<'a> CompatibleDist<'a> {
124 pub(crate) fn matches_python_requirement(&self, requires_python: &RequiresPython) -> bool {
129 self.prioritized().is_none_or(|prioritized| {
130 !prioritized
131 .0
132 .python_markers
133 .is_disjoint(requires_python.to_marker_tree())
134 })
135 }
136
137 pub(crate) fn requires_python(&self) -> Option<&VersionSpecifiers> {
139 match self {
140 Self::InstalledDist(_) => None,
141 Self::SourceDist { sdist, .. } => sdist.file.requires_python.as_deref(),
142 Self::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_deref(),
143 Self::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_deref(),
144 }
145 }
146
147 pub(crate) fn prioritized(&self) -> Option<&'a PrioritizedDist> {
149 match self {
150 Self::InstalledDist(_) => None,
151 Self::SourceDist { prioritized, .. }
152 | Self::CompatibleWheel { prioritized, .. }
153 | Self::IncompatibleWheel { prioritized, .. } => Some(prioritized),
154 }
155 }
156
157 pub(crate) fn implied_markers(&self) -> MarkerTree {
159 match self.prioritized() {
160 Some(prioritized) => {
161 let [glibc, musl] = prioritized.0.markers;
162 glibc.and(musl)
163 }
164 None => MarkerTree::TRUE,
165 }
166 }
167}
168
169#[derive(Debug, PartialEq, Eq, Clone)]
170pub enum IncompatibleDist {
171 Wheel(IncompatibleWheel),
173 Source(IncompatibleSource),
175 Unavailable,
177}
178
179impl IncompatibleDist {
180 pub(crate) fn singular_message(&self) -> String {
181 match self {
182 Self::Wheel(incompatibility) => match incompatibility {
183 IncompatibleWheel::NoBinary => format!("has {self}"),
184 IncompatibleWheel::Tag(_) => format!("has {self}"),
185 IncompatibleWheel::Yanked(_) => format!("was {self}"),
186 IncompatibleWheel::ExcludeNewer(ts) => match ts {
187 Some(_) => format!("was {self}"),
188 None => format!("has {self}"),
189 },
190 IncompatibleWheel::RequiresPython(..) => format!("requires {self}"),
191 IncompatibleWheel::MissingPlatform(_) => format!("has {self}"),
192 },
193 Self::Source(incompatibility) => match incompatibility {
194 IncompatibleSource::NoBuild => format!("has {self}"),
195 IncompatibleSource::Yanked(_) => format!("was {self}"),
196 IncompatibleSource::ExcludeNewer(ts) => match ts {
197 Some(_) => format!("was {self}"),
198 None => format!("has {self}"),
199 },
200 IncompatibleSource::RequiresPython(..) => {
201 format!("requires {self}")
202 }
203 IncompatibleSource::NotPep625Filename => format!("has {self}"),
204 },
205 Self::Unavailable => format!("has {self}"),
206 }
207 }
208
209 pub(crate) fn plural_message(&self) -> String {
210 match self {
211 Self::Wheel(incompatibility) => match incompatibility {
212 IncompatibleWheel::NoBinary => format!("have {self}"),
213 IncompatibleWheel::Tag(_) => format!("have {self}"),
214 IncompatibleWheel::Yanked(_) => format!("were {self}"),
215 IncompatibleWheel::ExcludeNewer(ts) => match ts {
216 Some(_) => format!("were {self}"),
217 None => format!("have {self}"),
218 },
219 IncompatibleWheel::RequiresPython(..) => format!("require {self}"),
220 IncompatibleWheel::MissingPlatform(_) => format!("have {self}"),
221 },
222 Self::Source(incompatibility) => match incompatibility {
223 IncompatibleSource::NoBuild => format!("have {self}"),
224 IncompatibleSource::Yanked(_) => format!("were {self}"),
225 IncompatibleSource::ExcludeNewer(ts) => match ts {
226 Some(_) => format!("were {self}"),
227 None => format!("have {self}"),
228 },
229 IncompatibleSource::RequiresPython(..) => {
230 format!("require {self}")
231 }
232 IncompatibleSource::NotPep625Filename => format!("have {self}"),
233 },
234 Self::Unavailable => format!("have {self}"),
235 }
236 }
237
238 pub(crate) fn context_message(
239 &self,
240 tags: Option<&Tags>,
241 requires_python: Option<AbiTag>,
242 ) -> Option<String> {
243 match self {
244 Self::Wheel(incompatibility) => match incompatibility {
245 IncompatibleWheel::Tag(IncompatibleTag::Python) => {
246 let tag = tags?.python_tag().as_ref().map(ToString::to_string)?;
247 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
248 }
249 IncompatibleWheel::Tag(IncompatibleTag::Abi) => {
250 let tag = tags?.abi_tag().as_ref().map(ToString::to_string)?;
251 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
252 }
253 IncompatibleWheel::Tag(IncompatibleTag::FreethreadedAbi) => None,
254 IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => {
255 let tag = requires_python?;
256 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
257 }
258 IncompatibleWheel::Tag(IncompatibleTag::Platform) => {
259 let tag = tags?.platform_tag().map(ToString::to_string)?;
260 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
261 }
262 IncompatibleWheel::Tag(IncompatibleTag::Invalid) => None,
263 IncompatibleWheel::Tag(IncompatibleTag::Variant) => None,
264 IncompatibleWheel::NoBinary => None,
265 IncompatibleWheel::Yanked(..) => None,
266 IncompatibleWheel::ExcludeNewer(..) => None,
267 IncompatibleWheel::RequiresPython(..) => None,
268 IncompatibleWheel::MissingPlatform(..) => None,
269 },
270 Self::Source(..) => None,
271 Self::Unavailable => None,
272 }
273 }
274}
275
276impl Display for IncompatibleDist {
277 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
278 match self {
279 Self::Wheel(incompatibility) => match incompatibility {
280 IncompatibleWheel::NoBinary => f.write_str("no source distribution"),
281 IncompatibleWheel::Tag(tag) => match tag {
282 IncompatibleTag::Variant => f.write_str("only unsupported wheel variants"),
283 IncompatibleTag::Invalid => f.write_str("no wheels with valid tags"),
284 IncompatibleTag::Python => {
285 f.write_str("no wheels with a matching Python implementation tag")
286 }
287 IncompatibleTag::Abi => f.write_str("no wheels with a matching Python ABI tag"),
288 IncompatibleTag::FreethreadedAbi => {
289 f.write_str("no wheels with a free-threading compatible ABI tag")
290 }
291 IncompatibleTag::AbiPythonVersion => {
292 f.write_str("no wheels with a matching Python version tag")
293 }
294 IncompatibleTag::Platform => {
295 f.write_str("no wheels with a matching platform tag")
296 }
297 },
298 IncompatibleWheel::Yanked(yanked) => match yanked {
299 Yanked::Bool(_) => f.write_str("yanked"),
300 Yanked::Reason(reason) => write!(
301 f,
302 "yanked (reason: {})",
303 reason.trim().trim_end_matches('.')
304 ),
305 },
306 IncompatibleWheel::ExcludeNewer(ts) => match ts {
307 Some(_) => f.write_str("published after the exclude newer time"),
308 None => f.write_str("no publish time"),
309 },
310 IncompatibleWheel::RequiresPython(python, _) => {
311 write!(f, "Python {python}")
312 }
313 IncompatibleWheel::MissingPlatform(marker) => {
314 if let Some(platform) = KnownPlatform::from_marker(*marker) {
315 write!(f, "no {platform}-compatible wheels")
316 } else if let Some(marker) = marker.try_to_string() {
317 write!(f, "no `{marker}`-compatible wheels")
318 } else {
319 write!(f, "no compatible wheels")
320 }
321 }
322 },
323 Self::Source(incompatibility) => match incompatibility {
324 IncompatibleSource::NoBuild => f.write_str("no usable wheels"),
325 IncompatibleSource::Yanked(yanked) => match yanked {
326 Yanked::Bool(_) => f.write_str("yanked"),
327 Yanked::Reason(reason) => write!(
328 f,
329 "yanked (reason: {})",
330 reason.trim().trim_end_matches('.')
331 ),
332 },
333 IncompatibleSource::ExcludeNewer(ts) => match ts {
334 Some(_) => f.write_str("published after the exclude newer time"),
335 None => f.write_str("no publish time"),
336 },
337 IncompatibleSource::RequiresPython(python, _) => {
338 write!(f, "Python {python}")
339 }
340 IncompatibleSource::NotPep625Filename => {
341 f.write_str("a non-PEP 625-compliant source distribution filename")
342 }
343 },
344 Self::Unavailable => f.write_str("no available distributions"),
345 }
346 }
347}
348
349#[derive(Debug, PartialEq, Eq, Copy, Clone)]
350pub enum PythonRequirementKind {
351 Installed,
353 Target,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub(crate) enum WheelCompatibility {
361 Incompatible(IncompatibleWheel),
362 Variant(HashComparison, Option<TagPriority>, Option<BuildTag>),
364 Compatible(HashComparison, Option<TagPriority>, Option<BuildTag>),
365}
366
367#[derive(Debug, PartialEq, Eq, Clone)]
368pub enum IncompatibleWheel {
369 ExcludeNewer(Option<i64>),
371 Tag(IncompatibleTag),
373 RequiresPython(VersionSpecifiers, PythonRequirementKind),
375 Yanked(Yanked),
377 NoBinary,
379 MissingPlatform(MarkerTree),
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub enum SourceDistCompatibility {
385 Incompatible(IncompatibleSource),
386 Compatible(HashComparison),
387}
388
389#[derive(Debug, PartialEq, Eq, Clone)]
390pub enum IncompatibleSource {
391 ExcludeNewer(Option<i64>),
392 RequiresPython(VersionSpecifiers, PythonRequirementKind),
393 Yanked(Yanked),
394 NoBuild,
395 NotPep625Filename,
397}
398
399#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
400pub enum HashComparison {
401 Mismatched,
403 Missing,
405 Matched,
407}
408
409impl PrioritizedDist {
410 pub(crate) fn insert_variants_json(&mut self, variants_json: RegistryVariantsJson) {
411 if !self.0.variants_json.contains(&variants_json) {
413 self.0.variants_json.push(variants_json);
414 }
415 }
416
417 pub(crate) fn variants_json(&self) -> &[RegistryVariantsJson] {
418 &self.0.variants_json
419 }
420
421 pub(crate) fn variants_json_for_selection(
426 &self,
427 ) -> impl Iterator<Item = &RegistryVariantsJson> {
428 self.0.variants_json.iter().filter(|metadata| {
429 self.0.wheels.iter().any(|(wheel, compatibility)| {
430 wheel.index == metadata.index
431 && matches!(compatibility, WheelCompatibility::Variant(..))
432 })
433 })
434 }
435
436 pub(crate) fn prioritize_variants(
438 &self,
439 metadata: &[(&IndexUrl, &VariantsJsonContent)],
440 ) -> Option<Self> {
441 if metadata.is_empty() {
442 return None;
443 }
444 let metadata: Vec<_> = metadata
446 .iter()
447 .map(|(source, metadata)| (*source, *metadata, metadata.static_supported_properties()))
448 .collect();
449 let (best_index, (hash, _, priority, build_tag)) = self
454 .0
455 .wheels
456 .iter()
457 .enumerate()
458 .filter_map(|(index, (wheel, compatibility))| {
459 let WheelCompatibility::Variant(hash, priority, build_tag) = compatibility else {
460 return None;
461 };
462 let label = wheel.filename.variant()?;
463 let score = metadata
464 .iter()
465 .filter(|(source, _, _)| *source == &wheel.index)
466 .filter_map(|(_, metadata, supported_properties)| {
467 metadata.score_variant(label, supported_properties)
468 })
469 .max()?;
470 Some((index, (*hash, score, *priority, build_tag)))
471 })
472 .max_by(|(_, left), (_, right)| left.cmp(right))?;
473
474 if let Some((_, WheelCompatibility::Compatible(existing_hash, ..))) = self.best_wheel()
476 && *existing_hash > hash
477 {
478 return None;
479 }
480
481 let mut prioritized = self.clone();
482 prioritized.0.wheels[best_index].1 =
483 WheelCompatibility::Compatible(hash, priority, build_tag.clone());
484 prioritized.0.best_wheel_index = Some(best_index);
485 Some(prioritized)
486 }
487
488 pub(crate) fn insert_built(
490 &mut self,
491 dist: RegistryBuiltWheel,
492 hashes: impl IntoIterator<Item = HashDigest>,
493 compatibility: WheelCompatibility,
494 minimum_libc_version: Option<MinimumLibcVersion>,
495 ) {
496 if compatibility.is_compatible()
497 && (!self.0.python_markers.is_true()
498 || !self.0.markers.iter().all(|markers| markers.is_true()))
499 {
500 let python = implied_python_markers(&dist.filename);
501 self.0.python_markers = self.0.python_markers.or(python);
502 for (coverage, markers) in self.0.markers.iter_mut().zip(implied_libc_markers(
503 &dist.filename,
504 python,
505 minimum_libc_version,
506 )) {
507 *coverage = coverage.or(markers);
508 }
509 }
510 if !compatibility.is_excluded() {
512 self.0.hashes.extend(hashes);
513 }
514 if let Some((existing_wheel, existing_compatibility)) = self.best_wheel() {
517 let is_more_compatible =
518 match (dist.filename.variant(), existing_wheel.filename.variant()) {
519 (None, Some(_)) => true,
520 (Some(_), None) => false,
521 (None, None) | (Some(_), Some(_)) => {
522 compatibility.is_more_compatible(existing_compatibility)
523 }
524 };
525 if is_more_compatible {
526 self.0.best_wheel_index = Some(self.0.wheels.len());
527 }
528 } else {
529 self.0.best_wheel_index = Some(self.0.wheels.len());
530 }
531 self.0.wheels.push((dist, compatibility));
532 }
533
534 pub fn insert_source(
536 &mut self,
537 dist: RegistrySourceDist,
538 hashes: impl IntoIterator<Item = HashDigest>,
539 compatibility: SourceDistCompatibility,
540 ) {
541 if compatibility.is_compatible() {
543 self.0.python_markers = MarkerTree::TRUE;
544 self.0.markers = [MarkerTree::TRUE; 2];
545 }
546 if !compatibility.is_excluded() {
548 self.0.hashes.extend(hashes);
549 }
550 if let Some((.., existing_compatibility)) = &self.0.source {
552 if compatibility.is_more_compatible(existing_compatibility) {
553 self.0.source = Some((dist, compatibility));
554 }
555 } else {
556 self.0.source = Some((dist, compatibility));
557 }
558 }
559
560 pub(crate) fn get(&self) -> Option<CompatibleDist<'_>> {
562 let best_wheel = self.0.best_wheel_index.map(|i| &self.0.wheels[i]);
563 match (&best_wheel, &self.0.source) {
564 (
568 Some((wheel, WheelCompatibility::Compatible(wheel_hash, ..))),
569 Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))),
570 ) => {
571 if sdist_hash > wheel_hash {
572 Some(CompatibleDist::SourceDist {
573 sdist,
574 prioritized: self,
575 })
576 } else {
577 Some(CompatibleDist::CompatibleWheel {
578 wheel,
579 prioritized: self,
580 })
581 }
582 }
583 (Some((wheel, WheelCompatibility::Compatible(..))), _) => {
585 Some(CompatibleDist::CompatibleWheel {
586 wheel,
587 prioritized: self,
588 })
589 }
590 (
599 Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))),
600 Some((sdist, SourceDistCompatibility::Compatible(_))),
601 ) if !compatibility.is_excluded() && wheel.filename.variant().is_none() => {
602 Some(CompatibleDist::IncompatibleWheel {
603 sdist,
604 wheel,
605 prioritized: self,
606 })
607 }
608 (.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
610 Some(CompatibleDist::SourceDist {
611 sdist,
612 prioritized: self,
613 })
614 }
615 _ => None,
616 }
617 }
618
619 pub(crate) fn incompatible_source(&self) -> Option<&IncompatibleSource> {
621 self.0
622 .source
623 .as_ref()
624 .and_then(|(_, compatibility)| match compatibility {
625 SourceDistCompatibility::Compatible(_) => None,
626 SourceDistCompatibility::Incompatible(incompatibility) => Some(incompatibility),
627 })
628 }
629
630 pub(crate) fn incompatible_wheel(&self) -> Option<&IncompatibleWheel> {
632 self.0
633 .best_wheel_index
634 .map(|i| &self.0.wheels[i])
635 .and_then(|(_, compatibility)| match compatibility {
636 WheelCompatibility::Compatible(_, _, _) => None,
637 WheelCompatibility::Incompatible(incompatibility) => Some(incompatibility),
638 WheelCompatibility::Variant(..) => {
639 Some(&IncompatibleWheel::Tag(IncompatibleTag::Variant))
640 }
641 })
642 }
643
644 pub(crate) fn hashes(&self) -> &[HashDigest] {
646 &self.0.hashes
647 }
648
649 pub(crate) fn is_empty(&self) -> bool {
652 self.0.source.is_none() && self.0.wheels.is_empty()
653 }
654
655 pub(crate) fn built_dist(&self) -> Option<RegistryBuiltDist> {
658 let best_wheel_index = self.0.best_wheel_index?;
659
660 let mut adjusted_wheels = Vec::with_capacity(self.0.wheels.len());
663 let mut adjusted_best_index = 0;
664 for (i, (wheel, compatibility)) in self.0.wheels.iter().enumerate() {
665 if compatibility.is_excluded() {
666 continue;
667 }
668 if i == best_wheel_index {
669 adjusted_best_index = adjusted_wheels.len();
670 }
671 adjusted_wheels.push(wheel.clone());
672 }
673
674 let sdist = self
675 .0
676 .source
677 .as_ref()
678 .filter(|(_, compatibility)| !compatibility.is_excluded())
679 .map(|(sdist, _)| sdist.clone());
680 Some(RegistryBuiltDist {
681 wheels: adjusted_wheels,
682 best_wheel_index: adjusted_best_index,
683 sdist,
684 })
685 }
686
687 pub(crate) fn source_dist(&self) -> Option<RegistrySourceDist> {
690 let mut sdist = self
691 .0
692 .source
693 .as_ref()
694 .filter(|(_, compatibility)| !compatibility.is_excluded())
695 .map(|(sdist, _)| sdist.clone())?;
696 assert!(
697 sdist.wheels.is_empty(),
698 "source distribution should not have any wheels yet"
699 );
700 sdist.wheels = self
701 .0
702 .wheels
703 .iter()
704 .filter(|(_, compatibility)| !compatibility.is_excluded())
705 .map(|(wheel, _)| wheel.clone())
706 .collect();
707 Some(sdist)
708 }
709
710 pub(crate) fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
713 self.0.best_wheel_index.map(|i| &self.0.wheels[i])
714 }
715
716 pub(crate) fn files(&self) -> impl Iterator<Item = &File> {
718 self.0
719 .wheels
720 .iter()
721 .map(|(wheel, _)| wheel.file.as_ref())
722 .chain(
723 self.0
724 .source
725 .as_ref()
726 .map(|(source_dist, _)| source_dist.file.as_ref()),
727 )
728 }
729
730 pub(crate) fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
732 self.0
733 .wheels
734 .iter()
735 .flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied())
736 }
737
738 pub(crate) fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
740 self.0
741 .wheels
742 .iter()
743 .flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied())
744 }
745
746 pub(crate) fn platform_tags<'a>(
749 &'a self,
750 tags: &'a Tags,
751 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
752 self.0.wheels.iter().flat_map(move |(wheel, _)| {
753 if wheel.filename.python_tags().iter().any(|wheel_py| {
754 wheel
755 .filename
756 .abi_tags()
757 .iter()
758 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
759 }) {
760 wheel.filename.platform_tags().iter()
761 } else {
762 [].iter()
763 }
764 })
765 }
766}
767
768impl<'a> CompatibleDist<'a> {
769 pub(crate) fn for_resolution(&self) -> ResolvedDistRef<'a> {
771 match self {
772 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
773 Self::SourceDist { sdist, prioritized } => {
774 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
775 }
776 Self::CompatibleWheel {
777 wheel, prioritized, ..
778 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
779 Self::IncompatibleWheel {
780 wheel, prioritized, ..
781 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
782 }
783 }
784
785 pub(crate) fn for_installation(&self) -> ResolvedDistRef<'a> {
787 match self {
788 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
789 Self::SourceDist { sdist, prioritized } => {
790 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
791 }
792 Self::CompatibleWheel {
793 wheel, prioritized, ..
794 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
795 Self::IncompatibleWheel {
796 sdist, prioritized, ..
797 } => ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized },
798 }
799 }
800
801 pub(crate) fn wheel(&self) -> Option<&RegistryBuiltWheel> {
804 match self {
805 Self::InstalledDist(_) => None,
806 Self::SourceDist { .. } => None,
807 Self::CompatibleWheel { wheel, .. } => Some(wheel),
808 Self::IncompatibleWheel { wheel, .. } => Some(wheel),
809 }
810 }
811}
812
813impl WheelCompatibility {
814 pub(crate) fn from_tag_priority(
816 filename: &WheelFilename,
817 hash: HashComparison,
818 priority: Option<TagPriority>,
819 ) -> Self {
820 let build_tag = filename.build_tag().cloned();
821 if filename.variant().is_some() {
822 Self::Variant(hash, priority, build_tag)
823 } else {
824 Self::Compatible(hash, priority, build_tag)
825 }
826 }
827
828 fn is_compatible(&self) -> bool {
830 matches!(self, Self::Compatible(_, _, _))
831 }
832
833 fn is_excluded(&self) -> bool {
835 matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
836 }
837
838 fn is_more_compatible(&self, other: &Self) -> bool {
843 match (self, other) {
844 (Self::Compatible(_, _, _), Self::Incompatible(_) | Self::Variant(..)) => true,
845 (
846 Self::Compatible(hash, tag_priority, build_tag),
847 Self::Compatible(other_hash, other_tag_priority, other_build_tag),
848 ) => {
849 (hash, tag_priority, build_tag) > (other_hash, other_tag_priority, other_build_tag)
850 }
851 (Self::Incompatible(_) | Self::Variant(..), Self::Compatible(_, _, _)) => false,
852 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
853 incompatibility.is_more_compatible(other_incompatibility)
854 }
855 (Self::Variant(..), Self::Incompatible(other)) => {
856 IncompatibleWheel::Tag(IncompatibleTag::Variant).is_more_compatible(other)
857 }
858 (Self::Incompatible(incompatibility), Self::Variant(..)) => incompatibility
859 .is_more_compatible(&IncompatibleWheel::Tag(IncompatibleTag::Variant)),
860 (Self::Variant(..), Self::Variant(..)) => false,
861 }
862 }
863}
864
865impl SourceDistCompatibility {
866 fn is_compatible(&self) -> bool {
868 matches!(self, Self::Compatible(_))
869 }
870
871 fn is_excluded(&self) -> bool {
873 matches!(
874 self,
875 Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
876 )
877 }
878
879 fn is_more_compatible(&self, other: &Self) -> bool {
885 match (self, other) {
886 (Self::Compatible(_), Self::Incompatible(_)) => true,
887 (Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => {
888 compatibility > other_compatibility
889 }
890 (Self::Incompatible(_), Self::Compatible(_)) => false,
891 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
892 incompatibility.is_more_compatible(other_incompatibility)
893 }
894 }
895 }
896}
897
898impl IncompatibleSource {
899 fn is_more_compatible(&self, other: &Self) -> bool {
900 match self {
901 Self::ExcludeNewer(timestamp_self) => match other {
902 Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
904 Self::NoBuild
905 | Self::RequiresPython(_, _)
906 | Self::Yanked(_)
907 | Self::NotPep625Filename => true,
908 },
909 Self::RequiresPython(_, _) => match other {
910 Self::ExcludeNewer(_) => false,
911 Self::RequiresPython(_, _) => false,
913 Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true,
914 },
915 Self::Yanked(_) => match other {
916 Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => {
917 false
918 }
919 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
921 Self::NoBuild => true,
922 },
923 Self::NoBuild | Self::NotPep625Filename => false,
924 }
925 }
926}
927
928impl IncompatibleWheel {
929 fn is_more_compatible(&self, other: &Self) -> bool {
930 match self {
931 Self::ExcludeNewer(timestamp_self) => match other {
932 Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
934 (None, _) => true,
935 (_, None) => false,
936 (Some(timestamp_self), Some(timestamp_other)) => {
937 timestamp_other < timestamp_self
938 }
939 },
940 Self::MissingPlatform(_)
941 | Self::NoBinary
942 | Self::RequiresPython(_, _)
943 | Self::Tag(_)
944 | Self::Yanked(_) => true,
945 },
946 Self::Tag(tag_self) => match other {
947 Self::ExcludeNewer(_) => false,
948 Self::Tag(tag_other) => tag_self > tag_other,
949 Self::MissingPlatform(_)
950 | Self::NoBinary
951 | Self::RequiresPython(_, _)
952 | Self::Yanked(_) => true,
953 },
954 Self::RequiresPython(_, _) => match other {
955 Self::ExcludeNewer(_) | Self::Tag(_) => false,
956 Self::RequiresPython(_, _) => false,
958 Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true,
959 },
960 Self::Yanked(_) => match other {
961 Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false,
962 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
964 Self::MissingPlatform(_) | Self::NoBinary => true,
965 },
966 Self::NoBinary => match other {
967 Self::ExcludeNewer(_)
968 | Self::Tag(_)
969 | Self::RequiresPython(_, _)
970 | Self::Yanked(_) => false,
971 Self::NoBinary => false,
972 Self::MissingPlatform(_) => true,
973 },
974 Self::MissingPlatform(_) => false,
975 }
976 }
977}
978
979pub(crate) fn implied_markers(
983 filename: &WheelFilename,
984 minimum_libc_version: Option<MinimumLibcVersion>,
985) -> MarkerTree {
986 let python = implied_python_markers(filename);
987 let [glibc, musl] = implied_libc_markers(filename, python, minimum_libc_version);
988 glibc.and(musl)
989}
990
991fn implied_libc_markers(
993 filename: &WheelFilename,
994 python: MarkerTree,
995 minimum_libc_version: Option<MinimumLibcVersion>,
996) -> [MarkerTree; 2] {
997 let Some(minimum_libc_version) = minimum_libc_version else {
998 return [implied_platform_markers(filename.platform_tags()).and(python); 2];
999 };
1000 let mut markers = [MarkerTree::FALSE; 2];
1001 for tag in filename.platform_tags() {
1002 let platform = implied_platform_markers([tag]).and(python);
1003 for (markers, supported) in markers
1004 .iter_mut()
1005 .zip(minimum_libc_version.platform_coverage(tag))
1006 {
1007 if supported {
1008 *markers = markers.or(platform);
1009 }
1010 }
1011 }
1012 markers
1013}
1014
1015fn implied_platform_markers<'a>(
1017 platform_tags: impl IntoIterator<Item = &'a PlatformTag>,
1018) -> MarkerTree {
1019 let mut marker = MarkerTree::FALSE;
1020 for platform_tag in platform_tags {
1021 match platform_tag {
1022 PlatformTag::Any => {
1023 return MarkerTree::TRUE;
1024 }
1025
1026 PlatformTag::Win32 => {
1028 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1029 key: MarkerValueString::SysPlatform,
1030 operator: MarkerOperator::Equal,
1031 value: arcstr::literal!("win32"),
1032 });
1033 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1034 key: MarkerValueString::PlatformMachine,
1035 operator: MarkerOperator::Equal,
1036 value: arcstr::literal!("x86"),
1037 }));
1038 marker = marker.or(tag_marker);
1039 }
1040 PlatformTag::WinAmd64 => {
1041 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1042 key: MarkerValueString::SysPlatform,
1043 operator: MarkerOperator::Equal,
1044 value: arcstr::literal!("win32"),
1045 });
1046 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1047 key: MarkerValueString::PlatformMachine,
1048 operator: MarkerOperator::Equal,
1049 value: arcstr::literal!("AMD64"),
1050 }));
1051 marker = marker.or(tag_marker);
1052 }
1053 PlatformTag::WinArm64 => {
1054 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1055 key: MarkerValueString::SysPlatform,
1056 operator: MarkerOperator::Equal,
1057 value: arcstr::literal!("win32"),
1058 });
1059 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1060 key: MarkerValueString::PlatformMachine,
1061 operator: MarkerOperator::Equal,
1062 value: arcstr::literal!("ARM64"),
1063 }));
1064 marker = marker.or(tag_marker);
1065 }
1066
1067 PlatformTag::Macos {
1069 major,
1070 minor,
1071 binary_format,
1072 } => {
1073 let Some(release) = macos_darwin_release(*major, *minor) else {
1074 continue;
1075 };
1076 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1077 key: MarkerValueString::SysPlatform,
1078 operator: MarkerOperator::Equal,
1079 value: arcstr::literal!("darwin"),
1080 });
1081
1082 let mut arch_marker = MarkerTree::FALSE;
1084 for arch in binary_format.platform_machine() {
1085 let release = if *arch == BinaryFormat::Arm64 {
1088 release.clone().max(Version::new([20, 0, 0]))
1089 } else {
1090 release.clone()
1091 };
1092 let architecture = MarkerTree::expression(MarkerExpression::String {
1093 key: MarkerValueString::PlatformMachine,
1094 operator: MarkerOperator::Equal,
1095 value: ArcStr::from(arch.name()),
1096 });
1097 let release = MarkerTree::expression(MarkerExpression::String {
1098 key: MarkerValueString::PlatformRelease,
1099 operator: MarkerOperator::GreaterEqual,
1100 value: ArcStr::from(release.to_string()),
1101 });
1102 arch_marker = arch_marker.or(architecture.and(release));
1103 }
1104 tag_marker = tag_marker.and(arch_marker);
1105
1106 marker = marker.or(tag_marker);
1107 }
1108
1109 PlatformTag::Manylinux { arch, .. }
1111 | PlatformTag::Manylinux1 { arch, .. }
1112 | PlatformTag::Manylinux2010 { arch, .. }
1113 | PlatformTag::Manylinux2014 { arch, .. }
1114 | PlatformTag::Musllinux { arch, .. }
1115 | PlatformTag::Linux { arch } => {
1116 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1117 key: MarkerValueString::SysPlatform,
1118 operator: MarkerOperator::Equal,
1119 value: arcstr::literal!("linux"),
1120 });
1121 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1122 key: MarkerValueString::PlatformMachine,
1123 operator: MarkerOperator::Equal,
1124 value: ArcStr::from(arch.name()),
1125 }));
1126 marker = marker.or(tag_marker);
1127 }
1128
1129 PlatformTag::Android { abi, .. } => {
1134 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1135 key: MarkerValueString::SysPlatform,
1136 operator: MarkerOperator::Equal,
1137 value: arcstr::literal!("android"),
1138 });
1139 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1140 key: MarkerValueString::PlatformMachine,
1141 operator: MarkerOperator::Equal,
1142 value: ArcStr::from(abi.arch().name()),
1143 }));
1144 marker = marker.or(tag_marker);
1145 }
1146
1147 tag => {
1148 debug!("Unknown platform tag in wheel tag: {tag}");
1149 }
1150 }
1151 }
1152
1153 marker
1154}
1155
1156fn macos_darwin_release(major: u16, minor: u16) -> Option<Version> {
1162 let release = match (major, minor) {
1163 (10, 0) => [1, 3, 0],
1164 (10, 1) => [1, 4, 1],
1165 (10, 2..=16) => [u64::from(minor) + 4, 0, 0],
1166 (11..=15, 0) => [u64::from(major) + 9, 0, 0],
1167 (26, 0) => [25, 0, 0],
1168 (27.., 0) => [u64::from(major), 0, 0],
1169 _ => return None,
1170 };
1171 Some(Version::new(release))
1172}
1173
1174fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
1179 let mut marker = MarkerTree::FALSE;
1180
1181 let is_abi3 = filename.abi_tags().iter().any(|tag| tag.is_stable_abi());
1185
1186 for python_tag in filename.python_tags() {
1187 let mut tree = match python_tag {
1189 LanguageTag::None => {
1190 return MarkerTree::TRUE;
1192 }
1193 LanguageTag::Python { major, minor: None } | LanguageTag::CPythonMajor { major } => {
1194 if is_abi3 {
1195 MarkerTree::expression(MarkerExpression::Version {
1196 key: uv_pep508::MarkerValueVersion::PythonVersion,
1197 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1198 u64::from(*major),
1199 ])),
1200 })
1201 } else {
1202 MarkerTree::expression(MarkerExpression::Version {
1203 key: uv_pep508::MarkerValueVersion::PythonVersion,
1204 specifier: VersionSpecifier::equals_star_version(Version::new([
1205 u64::from(*major),
1206 ])),
1207 })
1208 }
1209 }
1210 LanguageTag::Python {
1211 major,
1212 minor: Some(minor),
1213 } => {
1214 MarkerTree::expression(MarkerExpression::Version {
1216 key: uv_pep508::MarkerValueVersion::PythonVersion,
1217 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1218 u64::from(*major),
1219 u64::from(*minor),
1220 ])),
1221 })
1222 .and(MarkerTree::expression(MarkerExpression::Version {
1223 key: uv_pep508::MarkerValueVersion::PythonVersion,
1224 specifier: VersionSpecifier::equals_star_version(Version::new([u64::from(
1225 *major,
1226 )])),
1227 }))
1228 }
1229 LanguageTag::CPython {
1230 python_version: (major, minor),
1231 }
1232 | LanguageTag::PyPy {
1233 python_version: (major, minor),
1234 }
1235 | LanguageTag::GraalPy {
1236 python_version: (major, minor),
1237 }
1238 | LanguageTag::Pyston {
1239 python_version: (major, minor),
1240 } => {
1241 if is_abi3 {
1242 MarkerTree::expression(MarkerExpression::Version {
1243 key: uv_pep508::MarkerValueVersion::PythonVersion,
1244 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1245 u64::from(*major),
1246 u64::from(*minor),
1247 ])),
1248 })
1249 } else {
1250 MarkerTree::expression(MarkerExpression::Version {
1251 key: uv_pep508::MarkerValueVersion::PythonVersion,
1252 specifier: VersionSpecifier::equals_star_version(Version::new([
1253 u64::from(*major),
1254 u64::from(*minor),
1255 ])),
1256 })
1257 }
1258 }
1259 };
1260
1261 match python_tag {
1263 LanguageTag::None | LanguageTag::Python { .. } => {
1264 }
1266 LanguageTag::CPython { .. } | LanguageTag::CPythonMajor { .. } => {
1267 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1268 key: MarkerValueString::PlatformPythonImplementation,
1269 operator: MarkerOperator::Equal,
1270 value: arcstr::literal!("CPython"),
1271 }));
1272 }
1273 LanguageTag::PyPy { .. } => {
1274 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1275 key: MarkerValueString::PlatformPythonImplementation,
1276 operator: MarkerOperator::Equal,
1277 value: arcstr::literal!("PyPy"),
1278 }));
1279 }
1280 LanguageTag::GraalPy { .. } => {
1281 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1282 key: MarkerValueString::PlatformPythonImplementation,
1283 operator: MarkerOperator::Equal,
1284 value: arcstr::literal!("GraalPy"),
1285 }));
1286 }
1287 LanguageTag::Pyston { .. } => {
1288 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1289 key: MarkerValueString::PlatformPythonImplementation,
1290 operator: MarkerOperator::Equal,
1291 value: arcstr::literal!("Pyston"),
1292 }));
1293 }
1294 }
1295
1296 marker = marker.or(tree);
1297 }
1298
1299 marker
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304 use std::str::FromStr;
1305
1306 use super::*;
1307
1308 #[track_caller]
1309 fn assert_platform_markers(filename: &str, expected: &str) {
1310 let filename = WheelFilename::from_str(filename).unwrap();
1311 assert_eq!(
1312 implied_platform_markers(filename.platform_tags()),
1313 expected.parse::<MarkerTree>().unwrap()
1314 );
1315 }
1316
1317 #[track_caller]
1318 fn assert_python_markers(filename: &str, expected: &str) {
1319 let filename = WheelFilename::from_str(filename).unwrap();
1320 assert_eq!(
1321 implied_python_markers(&filename),
1322 expected.parse::<MarkerTree>().unwrap()
1323 );
1324 }
1325
1326 #[track_caller]
1327 fn assert_implied_markers(filename: &str, expected: &str) {
1328 let filename = WheelFilename::from_str(filename).unwrap();
1329 assert_eq!(
1330 implied_markers(&filename, None),
1331 expected.parse::<MarkerTree>().unwrap()
1332 );
1333 }
1334
1335 #[test]
1336 fn test_implied_platform_markers() {
1337 let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
1338 assert_eq!(
1339 implied_platform_markers(filename.platform_tags()),
1340 MarkerTree::TRUE
1341 );
1342
1343 assert_platform_markers(
1344 "example-1.0-cp310-cp310-win32.whl",
1345 "sys_platform == 'win32' and platform_machine == 'x86'",
1346 );
1347 assert_platform_markers(
1348 "numpy-2.2.1-cp313-cp313t-win_amd64.whl",
1349 "sys_platform == 'win32' and platform_machine == 'AMD64'",
1350 );
1351 assert_platform_markers(
1352 "numpy-2.2.1-cp313-cp313t-win_arm64.whl",
1353 "sys_platform == 'win32' and platform_machine == 'ARM64'",
1354 );
1355 assert_platform_markers(
1356 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1357 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1358 );
1359 assert_platform_markers(
1360 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
1361 "sys_platform == 'linux' and platform_machine == 'x86_64'",
1362 );
1363 assert_platform_markers(
1364 "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",
1365 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1366 );
1367 assert_platform_markers(
1368 "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl",
1369 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '23.0.0'",
1370 );
1371 assert_platform_markers(
1372 "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl",
1373 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '13.0.0'",
1374 );
1375 assert_platform_markers(
1376 "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl",
1377 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '20.0.0'",
1378 );
1379 assert_platform_markers(
1380 "example-1.0-py3-none-android_24_arm64_v8a.whl",
1381 "sys_platform == 'android' and platform_machine == 'aarch64'",
1382 );
1383 assert_platform_markers(
1384 "example-1.0-py3-none-android_24_armeabi_v7a.whl",
1385 "sys_platform == 'android' and platform_machine == 'armv7l'",
1386 );
1387 assert_platform_markers(
1388 "example-1.0-py3-none-android_24_x86.whl",
1389 "sys_platform == 'android' and platform_machine == 'i686'",
1390 );
1391 assert_platform_markers(
1392 "example-1.0-py3-none-android_24_x86_64.whl",
1393 "sys_platform == 'android' and platform_machine == 'x86_64'",
1394 );
1395 }
1396
1397 #[test]
1398 fn test_implied_python_markers() {
1399 let filename = WheelFilename::from_str("example-1.0-none-none-any.whl").unwrap();
1400 assert_eq!(implied_python_markers(&filename), MarkerTree::TRUE);
1401
1402 assert_python_markers(
1403 "example-1.0-cp310-cp310-any.whl",
1404 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython'",
1405 );
1406 assert_python_markers(
1407 "example-1.0-cp311-cp311-any.whl",
1408 "python_full_version == '3.11.*' and platform_python_implementation == 'CPython'",
1409 );
1410 assert_python_markers(
1411 "example-1.0-cp312-cp312-any.whl",
1412 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython'",
1413 );
1414 assert_python_markers(
1415 "example-1.0-cp313-cp313-any.whl",
1416 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1417 );
1418 assert_python_markers(
1419 "example-1.0-cp313-cp313t-any.whl",
1420 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1421 );
1422 assert_python_markers(
1423 "example-1.0-pp310-pypy310_pp73-any.whl",
1424 "python_full_version == '3.10.*' and platform_python_implementation == 'PyPy'",
1425 );
1426 assert_python_markers(
1427 "example-1.0-py310-none-any.whl",
1428 "python_full_version >= '3.10' and python_full_version < '4'",
1429 );
1430 assert_python_markers(
1431 "example-1.0-py3-none-any.whl",
1432 "python_full_version >= '3' and python_full_version < '4'",
1433 );
1434 assert_python_markers(
1435 "example-1.0-py311.py312-none-any.whl",
1436 "python_full_version >= '3.11' and python_full_version < '4'",
1437 );
1438
1439 assert_python_markers(
1441 "example-1.0-cp39-abi3-any.whl",
1442 "python_full_version >= '3.9' and platform_python_implementation == 'CPython'",
1443 );
1444 assert_python_markers(
1445 "example-1.0-cp312-abi3-any.whl",
1446 "python_full_version >= '3.12' and platform_python_implementation == 'CPython'",
1447 );
1448 assert_python_markers(
1449 "example-1.0-cp315-abi3t-any.whl",
1450 "python_full_version >= '3.15' and platform_python_implementation == 'CPython'",
1451 );
1452 assert_python_markers(
1453 "example-1.0-cp3-abi3-any.whl",
1454 "python_full_version >= '3' and platform_python_implementation == 'CPython'",
1455 );
1456 }
1457
1458 #[test]
1459 fn test_macos_platform_markers() {
1460 for (tag, expected) in [
1461 (
1462 "macosx_10_5_x86_64",
1463 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '9.0.0'",
1464 ),
1465 (
1466 "macosx_10_9_universal2",
1467 "sys_platform == 'darwin' and ((platform_machine == 'x86_64' and platform_release >= '13.0.0') or (platform_machine == 'arm64' and platform_release >= '20.0.0'))",
1468 ),
1469 (
1470 "macosx_10_16_universal2",
1471 "sys_platform == 'darwin' and (platform_machine == 'arm64' or platform_machine == 'x86_64') and platform_release >= '20.0.0'",
1472 ),
1473 (
1474 "macosx_15_0_arm64",
1475 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '24.0.0'",
1476 ),
1477 (
1478 "macosx_26_0_arm64",
1479 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '25.0.0'",
1480 ),
1481 (
1482 "macosx_27_0_arm64",
1483 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '27.0.0'",
1484 ),
1485 (
1486 "macosx_26_0_arm64.macosx_15_0_x86_64",
1487 "sys_platform == 'darwin' and ((platform_machine == 'arm64' and platform_release >= '25.0.0') or (platform_machine == 'x86_64' and platform_release >= '24.0.0'))",
1488 ),
1489 ] {
1490 let filename =
1491 WheelFilename::from_str(&format!("example-1.0-py3-none-{tag}.whl")).unwrap();
1492 assert_eq!(
1493 implied_platform_markers(filename.platform_tags()),
1494 MarkerTree::from_str(expected).unwrap()
1495 );
1496 }
1497 }
1498
1499 #[test]
1500 fn test_implied_markers() {
1501 assert_implied_markers(
1502 "numpy-1.0-cp310-cp310-win32.whl",
1503 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'",
1504 );
1505 assert_implied_markers(
1506 "pywin32-311-cp314-cp314-win_arm64.whl",
1507 "python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'",
1508 );
1509 assert_implied_markers(
1510 "numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl",
1511 "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'",
1512 );
1513 assert_implied_markers(
1514 "numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1515 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'",
1516 );
1517 assert_implied_markers(
1518 "example-1.0-py3-none-any.whl",
1519 "python_full_version >= '3' and python_full_version < '4'",
1520 );
1521
1522 assert_implied_markers(
1524 "example-1.0-cp39-abi3-manylinux_2_28_x86_64.whl",
1525 "python_full_version >= '3.9' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1526 );
1527 assert_implied_markers(
1528 "example-1.0-cp315-abi3t-manylinux_2_28_x86_64.whl",
1529 "python_full_version >= '3.15' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1530 );
1531 }
1532}