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, InstalledDist, MinimumLibcVersion, Name, RegistryBuiltDist, RegistryBuiltWheel,
10 RegistrySourceDist, RequiresPython,
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)]
29struct PrioritizedDistInner {
30 source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
32 best_wheel_index: Option<usize>,
35 wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
37 hashes: Vec<HashDigest>,
39 python_markers: MarkerTree,
41 markers: [MarkerTree; 2],
45}
46
47impl Default for PrioritizedDistInner {
48 fn default() -> Self {
49 Self {
50 source: None,
51 best_wheel_index: None,
52 wheels: Vec::new(),
53 hashes: Vec::new(),
54 python_markers: MarkerTree::FALSE,
55 markers: [MarkerTree::FALSE; 2],
56 }
57 }
58}
59
60#[derive(Debug, Copy, Clone)]
62pub(crate) enum CompatibleDist<'a> {
63 InstalledDist(&'a InstalledDist),
65 SourceDist {
67 sdist: &'a RegistrySourceDist,
69 prioritized: &'a PrioritizedDist,
71 },
72 CompatibleWheel {
74 wheel: &'a RegistryBuiltWheel,
76 prioritized: &'a PrioritizedDist,
78 },
79 IncompatibleWheel {
82 sdist: &'a RegistrySourceDist,
84 wheel: &'a RegistryBuiltWheel,
86 prioritized: &'a PrioritizedDist,
88 },
89}
90
91impl Name for CompatibleDist<'_> {
92 fn name(&self) -> &PackageName {
93 match self {
94 Self::InstalledDist(dist) => dist.name(),
95 Self::SourceDist {
96 sdist,
97 prioritized: _,
98 } => sdist.name(),
99 Self::CompatibleWheel {
100 wheel,
101 prioritized: _,
102 } => wheel.name(),
103 Self::IncompatibleWheel {
104 sdist,
105 wheel: _,
106 prioritized: _,
107 } => sdist.name(),
108 }
109 }
110}
111
112impl CompatibleDist<'_> {
113 pub(crate) fn matches_python_requirement(&self, requires_python: &RequiresPython) -> bool {
118 self.prioritized().is_none_or(|prioritized| {
119 !prioritized
120 .0
121 .python_markers
122 .is_disjoint(requires_python.to_marker_tree())
123 })
124 }
125
126 pub(crate) fn requires_python(&self) -> Option<&VersionSpecifiers> {
128 match self {
129 Self::InstalledDist(_) => None,
130 Self::SourceDist { sdist, .. } => sdist.file.requires_python.as_deref(),
131 Self::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_deref(),
132 Self::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_deref(),
133 }
134 }
135
136 pub(crate) fn prioritized(&self) -> Option<&PrioritizedDist> {
138 match self {
139 Self::InstalledDist(_) => None,
140 Self::SourceDist { prioritized, .. }
141 | Self::CompatibleWheel { prioritized, .. }
142 | Self::IncompatibleWheel { prioritized, .. } => Some(prioritized),
143 }
144 }
145
146 pub(crate) fn implied_markers(&self) -> MarkerTree {
148 match self.prioritized() {
149 Some(prioritized) => {
150 let [glibc, musl] = prioritized.0.markers;
151 glibc.and(musl)
152 }
153 None => MarkerTree::TRUE,
154 }
155 }
156}
157
158#[derive(Debug, PartialEq, Eq, Clone)]
159pub enum IncompatibleDist {
160 Wheel(IncompatibleWheel),
162 Source(IncompatibleSource),
164 Unavailable,
166}
167
168impl IncompatibleDist {
169 pub(crate) fn singular_message(&self) -> String {
170 match self {
171 Self::Wheel(incompatibility) => match incompatibility {
172 IncompatibleWheel::NoBinary => format!("has {self}"),
173 IncompatibleWheel::Tag(_) => format!("has {self}"),
174 IncompatibleWheel::Yanked(_) => format!("was {self}"),
175 IncompatibleWheel::ExcludeNewer(ts) => match ts {
176 Some(_) => format!("was {self}"),
177 None => format!("has {self}"),
178 },
179 IncompatibleWheel::RequiresPython(..) => format!("requires {self}"),
180 IncompatibleWheel::MissingPlatform(_) => format!("has {self}"),
181 },
182 Self::Source(incompatibility) => match incompatibility {
183 IncompatibleSource::NoBuild => format!("has {self}"),
184 IncompatibleSource::Yanked(_) => format!("was {self}"),
185 IncompatibleSource::ExcludeNewer(ts) => match ts {
186 Some(_) => format!("was {self}"),
187 None => format!("has {self}"),
188 },
189 IncompatibleSource::RequiresPython(..) => {
190 format!("requires {self}")
191 }
192 IncompatibleSource::NotPep625Filename => format!("has {self}"),
193 },
194 Self::Unavailable => format!("has {self}"),
195 }
196 }
197
198 pub(crate) fn plural_message(&self) -> String {
199 match self {
200 Self::Wheel(incompatibility) => match incompatibility {
201 IncompatibleWheel::NoBinary => format!("have {self}"),
202 IncompatibleWheel::Tag(_) => format!("have {self}"),
203 IncompatibleWheel::Yanked(_) => format!("were {self}"),
204 IncompatibleWheel::ExcludeNewer(ts) => match ts {
205 Some(_) => format!("were {self}"),
206 None => format!("have {self}"),
207 },
208 IncompatibleWheel::RequiresPython(..) => format!("require {self}"),
209 IncompatibleWheel::MissingPlatform(_) => format!("have {self}"),
210 },
211 Self::Source(incompatibility) => match incompatibility {
212 IncompatibleSource::NoBuild => format!("have {self}"),
213 IncompatibleSource::Yanked(_) => format!("were {self}"),
214 IncompatibleSource::ExcludeNewer(ts) => match ts {
215 Some(_) => format!("were {self}"),
216 None => format!("have {self}"),
217 },
218 IncompatibleSource::RequiresPython(..) => {
219 format!("require {self}")
220 }
221 IncompatibleSource::NotPep625Filename => format!("have {self}"),
222 },
223 Self::Unavailable => format!("have {self}"),
224 }
225 }
226
227 pub(crate) fn context_message(
228 &self,
229 tags: Option<&Tags>,
230 requires_python: Option<AbiTag>,
231 ) -> Option<String> {
232 match self {
233 Self::Wheel(incompatibility) => match incompatibility {
234 IncompatibleWheel::Tag(IncompatibleTag::Python) => {
235 let tag = tags?.python_tag().as_ref().map(ToString::to_string)?;
236 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
237 }
238 IncompatibleWheel::Tag(IncompatibleTag::Abi) => {
239 let tag = tags?.abi_tag().as_ref().map(ToString::to_string)?;
240 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
241 }
242 IncompatibleWheel::Tag(IncompatibleTag::FreethreadedAbi) => None,
243 IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => {
244 let tag = requires_python?;
245 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
246 }
247 IncompatibleWheel::Tag(IncompatibleTag::Platform) => {
248 let tag = tags?.platform_tag().map(ToString::to_string)?;
249 Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
250 }
251 IncompatibleWheel::Tag(IncompatibleTag::Invalid) => None,
252 IncompatibleWheel::Tag(IncompatibleTag::Variant) => None,
253 IncompatibleWheel::NoBinary => None,
254 IncompatibleWheel::Yanked(..) => None,
255 IncompatibleWheel::ExcludeNewer(..) => None,
256 IncompatibleWheel::RequiresPython(..) => None,
257 IncompatibleWheel::MissingPlatform(..) => None,
258 },
259 Self::Source(..) => None,
260 Self::Unavailable => None,
261 }
262 }
263}
264
265impl Display for IncompatibleDist {
266 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
267 match self {
268 Self::Wheel(incompatibility) => match incompatibility {
269 IncompatibleWheel::NoBinary => f.write_str("no source distribution"),
270 IncompatibleWheel::Tag(tag) => match tag {
271 IncompatibleTag::Variant => f.write_str("only unsupported wheel variants"),
272 IncompatibleTag::Invalid => f.write_str("no wheels with valid tags"),
273 IncompatibleTag::Python => {
274 f.write_str("no wheels with a matching Python implementation tag")
275 }
276 IncompatibleTag::Abi => f.write_str("no wheels with a matching Python ABI tag"),
277 IncompatibleTag::FreethreadedAbi => {
278 f.write_str("no wheels with a free-threading compatible ABI tag")
279 }
280 IncompatibleTag::AbiPythonVersion => {
281 f.write_str("no wheels with a matching Python version tag")
282 }
283 IncompatibleTag::Platform => {
284 f.write_str("no wheels with a matching platform tag")
285 }
286 },
287 IncompatibleWheel::Yanked(yanked) => match yanked {
288 Yanked::Bool(_) => f.write_str("yanked"),
289 Yanked::Reason(reason) => write!(
290 f,
291 "yanked (reason: {})",
292 reason.trim().trim_end_matches('.')
293 ),
294 },
295 IncompatibleWheel::ExcludeNewer(ts) => match ts {
296 Some(_) => f.write_str("published after the exclude newer time"),
297 None => f.write_str("no publish time"),
298 },
299 IncompatibleWheel::RequiresPython(python, _) => {
300 write!(f, "Python {python}")
301 }
302 IncompatibleWheel::MissingPlatform(marker) => {
303 if let Some(platform) = KnownPlatform::from_marker(*marker) {
304 write!(f, "no {platform}-compatible wheels")
305 } else if let Some(marker) = marker.try_to_string() {
306 write!(f, "no `{marker}`-compatible wheels")
307 } else {
308 write!(f, "no compatible wheels")
309 }
310 }
311 },
312 Self::Source(incompatibility) => match incompatibility {
313 IncompatibleSource::NoBuild => f.write_str("no usable wheels"),
314 IncompatibleSource::Yanked(yanked) => match yanked {
315 Yanked::Bool(_) => f.write_str("yanked"),
316 Yanked::Reason(reason) => write!(
317 f,
318 "yanked (reason: {})",
319 reason.trim().trim_end_matches('.')
320 ),
321 },
322 IncompatibleSource::ExcludeNewer(ts) => match ts {
323 Some(_) => f.write_str("published after the exclude newer time"),
324 None => f.write_str("no publish time"),
325 },
326 IncompatibleSource::RequiresPython(python, _) => {
327 write!(f, "Python {python}")
328 }
329 IncompatibleSource::NotPep625Filename => {
330 f.write_str("a non-PEP 625-compliant source distribution filename")
331 }
332 },
333 Self::Unavailable => f.write_str("no available distributions"),
334 }
335 }
336}
337
338#[derive(Debug, PartialEq, Eq, Copy, Clone)]
339pub enum PythonRequirementKind {
340 Installed,
342 Target,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
349pub(crate) enum WheelCompatibility {
350 Incompatible(IncompatibleWheel),
351 Compatible(HashComparison, Option<TagPriority>, Option<BuildTag>),
352}
353
354#[derive(Debug, PartialEq, Eq, Clone)]
355pub enum IncompatibleWheel {
356 ExcludeNewer(Option<i64>),
358 Tag(IncompatibleTag),
360 RequiresPython(VersionSpecifiers, PythonRequirementKind),
362 Yanked(Yanked),
364 NoBinary,
366 MissingPlatform(MarkerTree),
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
371pub enum SourceDistCompatibility {
372 Incompatible(IncompatibleSource),
373 Compatible(HashComparison),
374}
375
376#[derive(Debug, PartialEq, Eq, Clone)]
377pub enum IncompatibleSource {
378 ExcludeNewer(Option<i64>),
379 RequiresPython(VersionSpecifiers, PythonRequirementKind),
380 Yanked(Yanked),
381 NoBuild,
382 NotPep625Filename,
384}
385
386#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
387pub enum HashComparison {
388 Mismatched,
390 Missing,
392 Matched,
394}
395
396impl PrioritizedDist {
397 pub(crate) fn insert_built(
399 &mut self,
400 dist: RegistryBuiltWheel,
401 hashes: impl IntoIterator<Item = HashDigest>,
402 compatibility: WheelCompatibility,
403 minimum_libc_version: Option<MinimumLibcVersion>,
404 ) {
405 if compatibility.is_compatible()
406 && (!self.0.python_markers.is_true()
407 || !self.0.markers.iter().all(|markers| markers.is_true()))
408 {
409 let python = implied_python_markers(&dist.filename);
410 self.0.python_markers = self.0.python_markers.or(python);
411 for (coverage, markers) in self.0.markers.iter_mut().zip(implied_libc_markers(
412 &dist.filename,
413 python,
414 minimum_libc_version,
415 )) {
416 *coverage = coverage.or(markers);
417 }
418 }
419 if !compatibility.is_excluded() {
421 self.0.hashes.extend(hashes);
422 }
423 if let Some((existing_wheel, existing_compatibility)) = self.best_wheel() {
426 let is_more_compatible =
427 match (dist.filename.variant(), existing_wheel.filename.variant()) {
428 (None, Some(_)) => true,
429 (Some(_), None) => false,
430 (None, None) | (Some(_), Some(_)) => {
431 compatibility.is_more_compatible(existing_compatibility)
432 }
433 };
434 if is_more_compatible {
435 self.0.best_wheel_index = Some(self.0.wheels.len());
436 }
437 } else {
438 self.0.best_wheel_index = Some(self.0.wheels.len());
439 }
440 self.0.wheels.push((dist, compatibility));
441 }
442
443 pub fn insert_source(
445 &mut self,
446 dist: RegistrySourceDist,
447 hashes: impl IntoIterator<Item = HashDigest>,
448 compatibility: SourceDistCompatibility,
449 ) {
450 if compatibility.is_compatible() {
452 self.0.python_markers = MarkerTree::TRUE;
453 self.0.markers = [MarkerTree::TRUE; 2];
454 }
455 if !compatibility.is_excluded() {
457 self.0.hashes.extend(hashes);
458 }
459 if let Some((.., existing_compatibility)) = &self.0.source {
461 if compatibility.is_more_compatible(existing_compatibility) {
462 self.0.source = Some((dist, compatibility));
463 }
464 } else {
465 self.0.source = Some((dist, compatibility));
466 }
467 }
468
469 pub(crate) fn get(&self) -> Option<CompatibleDist<'_>> {
471 let best_wheel = self.0.best_wheel_index.map(|i| &self.0.wheels[i]);
472 match (&best_wheel, &self.0.source) {
473 (
477 Some((wheel, WheelCompatibility::Compatible(wheel_hash, ..))),
478 Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))),
479 ) => {
480 if sdist_hash > wheel_hash {
481 Some(CompatibleDist::SourceDist {
482 sdist,
483 prioritized: self,
484 })
485 } else {
486 Some(CompatibleDist::CompatibleWheel {
487 wheel,
488 prioritized: self,
489 })
490 }
491 }
492 (Some((wheel, WheelCompatibility::Compatible(..))), _) => {
494 Some(CompatibleDist::CompatibleWheel {
495 wheel,
496 prioritized: self,
497 })
498 }
499 (
507 Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))),
508 Some((sdist, SourceDistCompatibility::Compatible(_))),
509 ) if !compatibility.is_excluded() && wheel.filename.variant().is_none() => {
510 Some(CompatibleDist::IncompatibleWheel {
511 sdist,
512 wheel,
513 prioritized: self,
514 })
515 }
516 (.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
518 Some(CompatibleDist::SourceDist {
519 sdist,
520 prioritized: self,
521 })
522 }
523 _ => None,
524 }
525 }
526
527 pub(crate) fn incompatible_source(&self) -> Option<&IncompatibleSource> {
529 self.0
530 .source
531 .as_ref()
532 .and_then(|(_, compatibility)| match compatibility {
533 SourceDistCompatibility::Compatible(_) => None,
534 SourceDistCompatibility::Incompatible(incompatibility) => Some(incompatibility),
535 })
536 }
537
538 pub(crate) fn incompatible_wheel(&self) -> Option<&IncompatibleWheel> {
540 self.0
541 .best_wheel_index
542 .map(|i| &self.0.wheels[i])
543 .and_then(|(_, compatibility)| match compatibility {
544 WheelCompatibility::Compatible(_, _, _) => None,
545 WheelCompatibility::Incompatible(incompatibility) => Some(incompatibility),
546 })
547 }
548
549 pub(crate) fn hashes(&self) -> &[HashDigest] {
551 &self.0.hashes
552 }
553
554 pub(crate) fn is_empty(&self) -> bool {
557 self.0.source.is_none() && self.0.wheels.is_empty()
558 }
559
560 pub(crate) fn built_dist(&self) -> Option<RegistryBuiltDist> {
563 let best_wheel_index = self.0.best_wheel_index?;
564
565 let mut adjusted_wheels = Vec::with_capacity(self.0.wheels.len());
568 let mut adjusted_best_index = 0;
569 for (i, (wheel, compatibility)) in self.0.wheels.iter().enumerate() {
570 if compatibility.is_excluded() {
571 continue;
572 }
573 if i == best_wheel_index {
574 adjusted_best_index = adjusted_wheels.len();
575 }
576 adjusted_wheels.push(wheel.clone());
577 }
578
579 let sdist = self
580 .0
581 .source
582 .as_ref()
583 .filter(|(_, compatibility)| !compatibility.is_excluded())
584 .map(|(sdist, _)| sdist.clone());
585 Some(RegistryBuiltDist {
586 wheels: adjusted_wheels,
587 best_wheel_index: adjusted_best_index,
588 sdist,
589 })
590 }
591
592 pub(crate) fn source_dist(&self) -> Option<RegistrySourceDist> {
595 let mut sdist = self
596 .0
597 .source
598 .as_ref()
599 .filter(|(_, compatibility)| !compatibility.is_excluded())
600 .map(|(sdist, _)| sdist.clone())?;
601 assert!(
602 sdist.wheels.is_empty(),
603 "source distribution should not have any wheels yet"
604 );
605 sdist.wheels = self
606 .0
607 .wheels
608 .iter()
609 .filter(|(_, compatibility)| !compatibility.is_excluded())
610 .map(|(wheel, _)| wheel.clone())
611 .collect();
612 Some(sdist)
613 }
614
615 pub(crate) fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
618 self.0.best_wheel_index.map(|i| &self.0.wheels[i])
619 }
620
621 pub(crate) fn files(&self) -> impl Iterator<Item = &File> {
623 self.0
624 .wheels
625 .iter()
626 .map(|(wheel, _)| wheel.file.as_ref())
627 .chain(
628 self.0
629 .source
630 .as_ref()
631 .map(|(source_dist, _)| source_dist.file.as_ref()),
632 )
633 }
634
635 pub(crate) fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
637 self.0
638 .wheels
639 .iter()
640 .flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied())
641 }
642
643 pub(crate) fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
645 self.0
646 .wheels
647 .iter()
648 .flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied())
649 }
650
651 pub(crate) fn platform_tags<'a>(
654 &'a self,
655 tags: &'a Tags,
656 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
657 self.0.wheels.iter().flat_map(move |(wheel, _)| {
658 if wheel.filename.python_tags().iter().any(|wheel_py| {
659 wheel
660 .filename
661 .abi_tags()
662 .iter()
663 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
664 }) {
665 wheel.filename.platform_tags().iter()
666 } else {
667 [].iter()
668 }
669 })
670 }
671}
672
673impl<'a> CompatibleDist<'a> {
674 pub(crate) fn for_resolution(&self) -> ResolvedDistRef<'a> {
676 match self {
677 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
678 Self::SourceDist { sdist, prioritized } => {
679 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
680 }
681 Self::CompatibleWheel {
682 wheel, prioritized, ..
683 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
684 Self::IncompatibleWheel {
685 wheel, prioritized, ..
686 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
687 }
688 }
689
690 pub(crate) fn for_installation(&self) -> ResolvedDistRef<'a> {
692 match self {
693 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
694 Self::SourceDist { sdist, prioritized } => {
695 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
696 }
697 Self::CompatibleWheel {
698 wheel, prioritized, ..
699 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
700 Self::IncompatibleWheel {
701 sdist, prioritized, ..
702 } => ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized },
703 }
704 }
705
706 pub(crate) fn wheel(&self) -> Option<&RegistryBuiltWheel> {
709 match self {
710 Self::InstalledDist(_) => None,
711 Self::SourceDist { .. } => None,
712 Self::CompatibleWheel { wheel, .. } => Some(wheel),
713 Self::IncompatibleWheel { wheel, .. } => Some(wheel),
714 }
715 }
716}
717
718impl WheelCompatibility {
719 fn is_compatible(&self) -> bool {
721 matches!(self, Self::Compatible(_, _, _))
722 }
723
724 fn is_excluded(&self) -> bool {
726 matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
727 }
728
729 fn is_more_compatible(&self, other: &Self) -> bool {
734 match (self, other) {
735 (Self::Compatible(_, _, _), Self::Incompatible(_)) => true,
736 (
737 Self::Compatible(hash, tag_priority, build_tag),
738 Self::Compatible(other_hash, other_tag_priority, other_build_tag),
739 ) => {
740 (hash, tag_priority, build_tag) > (other_hash, other_tag_priority, other_build_tag)
741 }
742 (Self::Incompatible(_), Self::Compatible(_, _, _)) => false,
743 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
744 incompatibility.is_more_compatible(other_incompatibility)
745 }
746 }
747 }
748}
749
750impl SourceDistCompatibility {
751 fn is_compatible(&self) -> bool {
753 matches!(self, Self::Compatible(_))
754 }
755
756 fn is_excluded(&self) -> bool {
758 matches!(
759 self,
760 Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
761 )
762 }
763
764 fn is_more_compatible(&self, other: &Self) -> bool {
770 match (self, other) {
771 (Self::Compatible(_), Self::Incompatible(_)) => true,
772 (Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => {
773 compatibility > other_compatibility
774 }
775 (Self::Incompatible(_), Self::Compatible(_)) => false,
776 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
777 incompatibility.is_more_compatible(other_incompatibility)
778 }
779 }
780 }
781}
782
783impl IncompatibleSource {
784 fn is_more_compatible(&self, other: &Self) -> bool {
785 match self {
786 Self::ExcludeNewer(timestamp_self) => match other {
787 Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
789 Self::NoBuild
790 | Self::RequiresPython(_, _)
791 | Self::Yanked(_)
792 | Self::NotPep625Filename => true,
793 },
794 Self::RequiresPython(_, _) => match other {
795 Self::ExcludeNewer(_) => false,
796 Self::RequiresPython(_, _) => false,
798 Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true,
799 },
800 Self::Yanked(_) => match other {
801 Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => {
802 false
803 }
804 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
806 Self::NoBuild => true,
807 },
808 Self::NoBuild | Self::NotPep625Filename => false,
809 }
810 }
811}
812
813impl IncompatibleWheel {
814 fn is_more_compatible(&self, other: &Self) -> bool {
815 match self {
816 Self::ExcludeNewer(timestamp_self) => match other {
817 Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
819 (None, _) => true,
820 (_, None) => false,
821 (Some(timestamp_self), Some(timestamp_other)) => {
822 timestamp_other < timestamp_self
823 }
824 },
825 Self::MissingPlatform(_)
826 | Self::NoBinary
827 | Self::RequiresPython(_, _)
828 | Self::Tag(_)
829 | Self::Yanked(_) => true,
830 },
831 Self::Tag(tag_self) => match other {
832 Self::ExcludeNewer(_) => false,
833 Self::Tag(tag_other) => tag_self > tag_other,
834 Self::MissingPlatform(_)
835 | Self::NoBinary
836 | Self::RequiresPython(_, _)
837 | Self::Yanked(_) => true,
838 },
839 Self::RequiresPython(_, _) => match other {
840 Self::ExcludeNewer(_) | Self::Tag(_) => false,
841 Self::RequiresPython(_, _) => false,
843 Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true,
844 },
845 Self::Yanked(_) => match other {
846 Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false,
847 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
849 Self::MissingPlatform(_) | Self::NoBinary => true,
850 },
851 Self::NoBinary => match other {
852 Self::ExcludeNewer(_)
853 | Self::Tag(_)
854 | Self::RequiresPython(_, _)
855 | Self::Yanked(_) => false,
856 Self::NoBinary => false,
857 Self::MissingPlatform(_) => true,
858 },
859 Self::MissingPlatform(_) => false,
860 }
861 }
862}
863
864pub(crate) fn implied_markers(
868 filename: &WheelFilename,
869 minimum_libc_version: Option<MinimumLibcVersion>,
870) -> MarkerTree {
871 let python = implied_python_markers(filename);
872 let [glibc, musl] = implied_libc_markers(filename, python, minimum_libc_version);
873 glibc.and(musl)
874}
875
876fn implied_libc_markers(
878 filename: &WheelFilename,
879 python: MarkerTree,
880 minimum_libc_version: Option<MinimumLibcVersion>,
881) -> [MarkerTree; 2] {
882 let Some(minimum_libc_version) = minimum_libc_version else {
883 return [implied_platform_markers(filename.platform_tags()).and(python); 2];
884 };
885 let mut markers = [MarkerTree::FALSE; 2];
886 for tag in filename.platform_tags() {
887 let platform = implied_platform_markers([tag]).and(python);
888 for (markers, supported) in markers
889 .iter_mut()
890 .zip(minimum_libc_version.platform_coverage(tag))
891 {
892 if supported {
893 *markers = markers.or(platform);
894 }
895 }
896 }
897 markers
898}
899
900fn implied_platform_markers<'a>(
902 platform_tags: impl IntoIterator<Item = &'a PlatformTag>,
903) -> MarkerTree {
904 let mut marker = MarkerTree::FALSE;
905 for platform_tag in platform_tags {
906 match platform_tag {
907 PlatformTag::Any => {
908 return MarkerTree::TRUE;
909 }
910
911 PlatformTag::Win32 => {
913 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
914 key: MarkerValueString::SysPlatform,
915 operator: MarkerOperator::Equal,
916 value: arcstr::literal!("win32"),
917 });
918 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
919 key: MarkerValueString::PlatformMachine,
920 operator: MarkerOperator::Equal,
921 value: arcstr::literal!("x86"),
922 }));
923 marker = marker.or(tag_marker);
924 }
925 PlatformTag::WinAmd64 => {
926 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
927 key: MarkerValueString::SysPlatform,
928 operator: MarkerOperator::Equal,
929 value: arcstr::literal!("win32"),
930 });
931 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
932 key: MarkerValueString::PlatformMachine,
933 operator: MarkerOperator::Equal,
934 value: arcstr::literal!("AMD64"),
935 }));
936 marker = marker.or(tag_marker);
937 }
938 PlatformTag::WinArm64 => {
939 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
940 key: MarkerValueString::SysPlatform,
941 operator: MarkerOperator::Equal,
942 value: arcstr::literal!("win32"),
943 });
944 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
945 key: MarkerValueString::PlatformMachine,
946 operator: MarkerOperator::Equal,
947 value: arcstr::literal!("ARM64"),
948 }));
949 marker = marker.or(tag_marker);
950 }
951
952 PlatformTag::Macos {
954 major,
955 minor,
956 binary_format,
957 } => {
958 let Some(release) = macos_darwin_release(*major, *minor) else {
959 continue;
960 };
961 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
962 key: MarkerValueString::SysPlatform,
963 operator: MarkerOperator::Equal,
964 value: arcstr::literal!("darwin"),
965 });
966
967 let mut arch_marker = MarkerTree::FALSE;
969 for arch in binary_format.platform_machine() {
970 let release = if *arch == BinaryFormat::Arm64 {
973 release.clone().max(Version::new([20, 0, 0]))
974 } else {
975 release.clone()
976 };
977 let architecture = MarkerTree::expression(MarkerExpression::String {
978 key: MarkerValueString::PlatformMachine,
979 operator: MarkerOperator::Equal,
980 value: ArcStr::from(arch.name()),
981 });
982 let release = MarkerTree::expression(MarkerExpression::String {
983 key: MarkerValueString::PlatformRelease,
984 operator: MarkerOperator::GreaterEqual,
985 value: ArcStr::from(release.to_string()),
986 });
987 arch_marker = arch_marker.or(architecture.and(release));
988 }
989 tag_marker = tag_marker.and(arch_marker);
990
991 marker = marker.or(tag_marker);
992 }
993
994 PlatformTag::Manylinux { arch, .. }
996 | PlatformTag::Manylinux1 { arch, .. }
997 | PlatformTag::Manylinux2010 { arch, .. }
998 | PlatformTag::Manylinux2014 { arch, .. }
999 | PlatformTag::Musllinux { arch, .. }
1000 | PlatformTag::Linux { arch } => {
1001 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1002 key: MarkerValueString::SysPlatform,
1003 operator: MarkerOperator::Equal,
1004 value: arcstr::literal!("linux"),
1005 });
1006 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1007 key: MarkerValueString::PlatformMachine,
1008 operator: MarkerOperator::Equal,
1009 value: ArcStr::from(arch.name()),
1010 }));
1011 marker = marker.or(tag_marker);
1012 }
1013
1014 tag => {
1015 debug!("Unknown platform tag in wheel tag: {tag}");
1016 }
1017 }
1018 }
1019
1020 marker
1021}
1022
1023fn macos_darwin_release(major: u16, minor: u16) -> Option<Version> {
1029 let release = match (major, minor) {
1030 (10, 0) => [1, 3, 0],
1031 (10, 1) => [1, 4, 1],
1032 (10, 2..=16) => [u64::from(minor) + 4, 0, 0],
1033 (11..=15, 0) => [u64::from(major) + 9, 0, 0],
1034 (26, 0) => [25, 0, 0],
1035 (27.., 0) => [u64::from(major), 0, 0],
1036 _ => return None,
1037 };
1038 Some(Version::new(release))
1039}
1040
1041fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
1046 let mut marker = MarkerTree::FALSE;
1047
1048 let is_abi3 = filename.abi_tags().iter().any(|tag| tag.is_stable_abi());
1052
1053 for python_tag in filename.python_tags() {
1054 let mut tree = match python_tag {
1056 LanguageTag::None => {
1057 return MarkerTree::TRUE;
1059 }
1060 LanguageTag::Python { major, minor: None } | LanguageTag::CPythonMajor { major } => {
1061 if is_abi3 {
1062 MarkerTree::expression(MarkerExpression::Version {
1063 key: uv_pep508::MarkerValueVersion::PythonVersion,
1064 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1065 u64::from(*major),
1066 ])),
1067 })
1068 } else {
1069 MarkerTree::expression(MarkerExpression::Version {
1070 key: uv_pep508::MarkerValueVersion::PythonVersion,
1071 specifier: VersionSpecifier::equals_star_version(Version::new([
1072 u64::from(*major),
1073 ])),
1074 })
1075 }
1076 }
1077 LanguageTag::Python {
1078 major,
1079 minor: Some(minor),
1080 } => {
1081 MarkerTree::expression(MarkerExpression::Version {
1083 key: uv_pep508::MarkerValueVersion::PythonVersion,
1084 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1085 u64::from(*major),
1086 u64::from(*minor),
1087 ])),
1088 })
1089 .and(MarkerTree::expression(MarkerExpression::Version {
1090 key: uv_pep508::MarkerValueVersion::PythonVersion,
1091 specifier: VersionSpecifier::equals_star_version(Version::new([u64::from(
1092 *major,
1093 )])),
1094 }))
1095 }
1096 LanguageTag::CPython {
1097 python_version: (major, minor),
1098 }
1099 | LanguageTag::PyPy {
1100 python_version: (major, minor),
1101 }
1102 | LanguageTag::GraalPy {
1103 python_version: (major, minor),
1104 }
1105 | LanguageTag::Pyston {
1106 python_version: (major, minor),
1107 } => {
1108 if is_abi3 {
1109 MarkerTree::expression(MarkerExpression::Version {
1110 key: uv_pep508::MarkerValueVersion::PythonVersion,
1111 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1112 u64::from(*major),
1113 u64::from(*minor),
1114 ])),
1115 })
1116 } else {
1117 MarkerTree::expression(MarkerExpression::Version {
1118 key: uv_pep508::MarkerValueVersion::PythonVersion,
1119 specifier: VersionSpecifier::equals_star_version(Version::new([
1120 u64::from(*major),
1121 u64::from(*minor),
1122 ])),
1123 })
1124 }
1125 }
1126 };
1127
1128 match python_tag {
1130 LanguageTag::None | LanguageTag::Python { .. } => {
1131 }
1133 LanguageTag::CPython { .. } | LanguageTag::CPythonMajor { .. } => {
1134 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1135 key: MarkerValueString::PlatformPythonImplementation,
1136 operator: MarkerOperator::Equal,
1137 value: arcstr::literal!("CPython"),
1138 }));
1139 }
1140 LanguageTag::PyPy { .. } => {
1141 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1142 key: MarkerValueString::PlatformPythonImplementation,
1143 operator: MarkerOperator::Equal,
1144 value: arcstr::literal!("PyPy"),
1145 }));
1146 }
1147 LanguageTag::GraalPy { .. } => {
1148 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1149 key: MarkerValueString::PlatformPythonImplementation,
1150 operator: MarkerOperator::Equal,
1151 value: arcstr::literal!("GraalPy"),
1152 }));
1153 }
1154 LanguageTag::Pyston { .. } => {
1155 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1156 key: MarkerValueString::PlatformPythonImplementation,
1157 operator: MarkerOperator::Equal,
1158 value: arcstr::literal!("Pyston"),
1159 }));
1160 }
1161 }
1162
1163 marker = marker.or(tree);
1164 }
1165
1166 marker
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171 use std::str::FromStr;
1172
1173 use super::*;
1174
1175 #[track_caller]
1176 fn assert_platform_markers(filename: &str, expected: &str) {
1177 let filename = WheelFilename::from_str(filename).unwrap();
1178 assert_eq!(
1179 implied_platform_markers(filename.platform_tags()),
1180 expected.parse::<MarkerTree>().unwrap()
1181 );
1182 }
1183
1184 #[track_caller]
1185 fn assert_python_markers(filename: &str, expected: &str) {
1186 let filename = WheelFilename::from_str(filename).unwrap();
1187 assert_eq!(
1188 implied_python_markers(&filename),
1189 expected.parse::<MarkerTree>().unwrap()
1190 );
1191 }
1192
1193 #[track_caller]
1194 fn assert_implied_markers(filename: &str, expected: &str) {
1195 let filename = WheelFilename::from_str(filename).unwrap();
1196 assert_eq!(
1197 implied_markers(&filename, None),
1198 expected.parse::<MarkerTree>().unwrap()
1199 );
1200 }
1201
1202 #[test]
1203 fn test_implied_platform_markers() {
1204 let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
1205 assert_eq!(
1206 implied_platform_markers(filename.platform_tags()),
1207 MarkerTree::TRUE
1208 );
1209
1210 assert_platform_markers(
1211 "example-1.0-cp310-cp310-win32.whl",
1212 "sys_platform == 'win32' and platform_machine == 'x86'",
1213 );
1214 assert_platform_markers(
1215 "numpy-2.2.1-cp313-cp313t-win_amd64.whl",
1216 "sys_platform == 'win32' and platform_machine == 'AMD64'",
1217 );
1218 assert_platform_markers(
1219 "numpy-2.2.1-cp313-cp313t-win_arm64.whl",
1220 "sys_platform == 'win32' and platform_machine == 'ARM64'",
1221 );
1222 assert_platform_markers(
1223 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1224 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1225 );
1226 assert_platform_markers(
1227 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
1228 "sys_platform == 'linux' and platform_machine == 'x86_64'",
1229 );
1230 assert_platform_markers(
1231 "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",
1232 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1233 );
1234 assert_platform_markers(
1235 "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl",
1236 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '23.0.0'",
1237 );
1238 assert_platform_markers(
1239 "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl",
1240 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '13.0.0'",
1241 );
1242 assert_platform_markers(
1243 "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl",
1244 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '20.0.0'",
1245 );
1246 }
1247
1248 #[test]
1249 fn test_implied_python_markers() {
1250 let filename = WheelFilename::from_str("example-1.0-none-none-any.whl").unwrap();
1251 assert_eq!(implied_python_markers(&filename), MarkerTree::TRUE);
1252
1253 assert_python_markers(
1254 "example-1.0-cp310-cp310-any.whl",
1255 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython'",
1256 );
1257 assert_python_markers(
1258 "example-1.0-cp311-cp311-any.whl",
1259 "python_full_version == '3.11.*' and platform_python_implementation == 'CPython'",
1260 );
1261 assert_python_markers(
1262 "example-1.0-cp312-cp312-any.whl",
1263 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython'",
1264 );
1265 assert_python_markers(
1266 "example-1.0-cp313-cp313-any.whl",
1267 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1268 );
1269 assert_python_markers(
1270 "example-1.0-cp313-cp313t-any.whl",
1271 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1272 );
1273 assert_python_markers(
1274 "example-1.0-pp310-pypy310_pp73-any.whl",
1275 "python_full_version == '3.10.*' and platform_python_implementation == 'PyPy'",
1276 );
1277 assert_python_markers(
1278 "example-1.0-py310-none-any.whl",
1279 "python_full_version >= '3.10' and python_full_version < '4'",
1280 );
1281 assert_python_markers(
1282 "example-1.0-py3-none-any.whl",
1283 "python_full_version >= '3' and python_full_version < '4'",
1284 );
1285 assert_python_markers(
1286 "example-1.0-py311.py312-none-any.whl",
1287 "python_full_version >= '3.11' and python_full_version < '4'",
1288 );
1289
1290 assert_python_markers(
1292 "example-1.0-cp39-abi3-any.whl",
1293 "python_full_version >= '3.9' and platform_python_implementation == 'CPython'",
1294 );
1295 assert_python_markers(
1296 "example-1.0-cp312-abi3-any.whl",
1297 "python_full_version >= '3.12' and platform_python_implementation == 'CPython'",
1298 );
1299 assert_python_markers(
1300 "example-1.0-cp315-abi3t-any.whl",
1301 "python_full_version >= '3.15' and platform_python_implementation == 'CPython'",
1302 );
1303 assert_python_markers(
1304 "example-1.0-cp3-abi3-any.whl",
1305 "python_full_version >= '3' and platform_python_implementation == 'CPython'",
1306 );
1307 }
1308
1309 #[test]
1310 fn test_macos_platform_markers() {
1311 for (tag, expected) in [
1312 (
1313 "macosx_10_5_x86_64",
1314 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '9.0.0'",
1315 ),
1316 (
1317 "macosx_10_9_universal2",
1318 "sys_platform == 'darwin' and ((platform_machine == 'x86_64' and platform_release >= '13.0.0') or (platform_machine == 'arm64' and platform_release >= '20.0.0'))",
1319 ),
1320 (
1321 "macosx_10_16_universal2",
1322 "sys_platform == 'darwin' and (platform_machine == 'arm64' or platform_machine == 'x86_64') and platform_release >= '20.0.0'",
1323 ),
1324 (
1325 "macosx_15_0_arm64",
1326 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '24.0.0'",
1327 ),
1328 (
1329 "macosx_26_0_arm64",
1330 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '25.0.0'",
1331 ),
1332 (
1333 "macosx_27_0_arm64",
1334 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '27.0.0'",
1335 ),
1336 (
1337 "macosx_26_0_arm64.macosx_15_0_x86_64",
1338 "sys_platform == 'darwin' and ((platform_machine == 'arm64' and platform_release >= '25.0.0') or (platform_machine == 'x86_64' and platform_release >= '24.0.0'))",
1339 ),
1340 ] {
1341 let filename =
1342 WheelFilename::from_str(&format!("example-1.0-py3-none-{tag}.whl")).unwrap();
1343 assert_eq!(
1344 implied_platform_markers(filename.platform_tags()),
1345 MarkerTree::from_str(expected).unwrap()
1346 );
1347 }
1348 }
1349
1350 #[test]
1351 fn test_implied_markers() {
1352 assert_implied_markers(
1353 "numpy-1.0-cp310-cp310-win32.whl",
1354 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'",
1355 );
1356 assert_implied_markers(
1357 "pywin32-311-cp314-cp314-win_arm64.whl",
1358 "python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'",
1359 );
1360 assert_implied_markers(
1361 "numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl",
1362 "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'",
1363 );
1364 assert_implied_markers(
1365 "numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1366 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'",
1367 );
1368 assert_implied_markers(
1369 "example-1.0-py3-none-any.whl",
1370 "python_full_version >= '3' and python_full_version < '4'",
1371 );
1372
1373 assert_implied_markers(
1375 "example-1.0-cp39-abi3-manylinux_2_28_x86_64.whl",
1376 "python_full_version >= '3.9' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1377 );
1378 assert_implied_markers(
1379 "example-1.0-cp315-abi3t-manylinux_2_28_x86_64.whl",
1380 "python_full_version >= '3.15' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1381 );
1382 }
1383}