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::NoBinary => None,
253 IncompatibleWheel::Yanked(..) => None,
254 IncompatibleWheel::ExcludeNewer(..) => None,
255 IncompatibleWheel::RequiresPython(..) => None,
256 IncompatibleWheel::MissingPlatform(..) => None,
257 },
258 Self::Source(..) => None,
259 Self::Unavailable => None,
260 }
261 }
262}
263
264impl Display for IncompatibleDist {
265 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
266 match self {
267 Self::Wheel(incompatibility) => match incompatibility {
268 IncompatibleWheel::NoBinary => f.write_str("no source distribution"),
269 IncompatibleWheel::Tag(tag) => match tag {
270 IncompatibleTag::Invalid => f.write_str("no wheels with valid tags"),
271 IncompatibleTag::Python => {
272 f.write_str("no wheels with a matching Python implementation tag")
273 }
274 IncompatibleTag::Abi => f.write_str("no wheels with a matching Python ABI tag"),
275 IncompatibleTag::FreethreadedAbi => {
276 f.write_str("no wheels with a free-threading compatible ABI tag")
277 }
278 IncompatibleTag::AbiPythonVersion => {
279 f.write_str("no wheels with a matching Python version tag")
280 }
281 IncompatibleTag::Platform => {
282 f.write_str("no wheels with a matching platform tag")
283 }
284 },
285 IncompatibleWheel::Yanked(yanked) => match yanked {
286 Yanked::Bool(_) => f.write_str("yanked"),
287 Yanked::Reason(reason) => write!(
288 f,
289 "yanked (reason: {})",
290 reason.trim().trim_end_matches('.')
291 ),
292 },
293 IncompatibleWheel::ExcludeNewer(ts) => match ts {
294 Some(_) => f.write_str("published after the exclude newer time"),
295 None => f.write_str("no publish time"),
296 },
297 IncompatibleWheel::RequiresPython(python, _) => {
298 write!(f, "Python {python}")
299 }
300 IncompatibleWheel::MissingPlatform(marker) => {
301 if let Some(platform) = KnownPlatform::from_marker(*marker) {
302 write!(f, "no {platform}-compatible wheels")
303 } else if let Some(marker) = marker.try_to_string() {
304 write!(f, "no `{marker}`-compatible wheels")
305 } else {
306 write!(f, "no compatible wheels")
307 }
308 }
309 },
310 Self::Source(incompatibility) => match incompatibility {
311 IncompatibleSource::NoBuild => f.write_str("no usable wheels"),
312 IncompatibleSource::Yanked(yanked) => match yanked {
313 Yanked::Bool(_) => f.write_str("yanked"),
314 Yanked::Reason(reason) => write!(
315 f,
316 "yanked (reason: {})",
317 reason.trim().trim_end_matches('.')
318 ),
319 },
320 IncompatibleSource::ExcludeNewer(ts) => match ts {
321 Some(_) => f.write_str("published after the exclude newer time"),
322 None => f.write_str("no publish time"),
323 },
324 IncompatibleSource::RequiresPython(python, _) => {
325 write!(f, "Python {python}")
326 }
327 IncompatibleSource::NotPep625Filename => {
328 f.write_str("a non-PEP 625-compliant source distribution filename")
329 }
330 },
331 Self::Unavailable => f.write_str("no available distributions"),
332 }
333 }
334}
335
336#[derive(Debug, PartialEq, Eq, Copy, Clone)]
337pub enum PythonRequirementKind {
338 Installed,
340 Target,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub(crate) enum WheelCompatibility {
348 Incompatible(IncompatibleWheel),
349 Compatible(HashComparison, Option<TagPriority>, Option<BuildTag>),
350}
351
352#[derive(Debug, PartialEq, Eq, Clone)]
353pub enum IncompatibleWheel {
354 ExcludeNewer(Option<i64>),
356 Tag(IncompatibleTag),
358 RequiresPython(VersionSpecifiers, PythonRequirementKind),
360 Yanked(Yanked),
362 NoBinary,
364 MissingPlatform(MarkerTree),
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub enum SourceDistCompatibility {
370 Incompatible(IncompatibleSource),
371 Compatible(HashComparison),
372}
373
374#[derive(Debug, PartialEq, Eq, Clone)]
375pub enum IncompatibleSource {
376 ExcludeNewer(Option<i64>),
377 RequiresPython(VersionSpecifiers, PythonRequirementKind),
378 Yanked(Yanked),
379 NoBuild,
380 NotPep625Filename,
382}
383
384#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
385pub enum HashComparison {
386 Mismatched,
388 Missing,
390 Matched,
392}
393
394impl PrioritizedDist {
395 pub(crate) fn insert_built(
397 &mut self,
398 dist: RegistryBuiltWheel,
399 hashes: impl IntoIterator<Item = HashDigest>,
400 compatibility: WheelCompatibility,
401 minimum_libc_version: Option<MinimumLibcVersion>,
402 ) {
403 if compatibility.is_compatible()
404 && (!self.0.python_markers.is_true()
405 || !self.0.markers.iter().all(|markers| markers.is_true()))
406 {
407 let python = implied_python_markers(&dist.filename);
408 self.0.python_markers = self.0.python_markers.or(python);
409 for (coverage, markers) in self.0.markers.iter_mut().zip(implied_libc_markers(
410 &dist.filename,
411 python,
412 minimum_libc_version,
413 )) {
414 *coverage = coverage.or(markers);
415 }
416 }
417 if !compatibility.is_excluded() {
419 self.0.hashes.extend(hashes);
420 }
421 if let Some((.., existing_compatibility)) = self.best_wheel() {
423 if compatibility.is_more_compatible(existing_compatibility) {
424 self.0.best_wheel_index = Some(self.0.wheels.len());
425 }
426 } else {
427 self.0.best_wheel_index = Some(self.0.wheels.len());
428 }
429 self.0.wheels.push((dist, compatibility));
430 }
431
432 pub fn insert_source(
434 &mut self,
435 dist: RegistrySourceDist,
436 hashes: impl IntoIterator<Item = HashDigest>,
437 compatibility: SourceDistCompatibility,
438 ) {
439 if compatibility.is_compatible() {
441 self.0.python_markers = MarkerTree::TRUE;
442 self.0.markers = [MarkerTree::TRUE; 2];
443 }
444 if !compatibility.is_excluded() {
446 self.0.hashes.extend(hashes);
447 }
448 if let Some((.., existing_compatibility)) = &self.0.source {
450 if compatibility.is_more_compatible(existing_compatibility) {
451 self.0.source = Some((dist, compatibility));
452 }
453 } else {
454 self.0.source = Some((dist, compatibility));
455 }
456 }
457
458 pub(crate) fn get(&self) -> Option<CompatibleDist<'_>> {
460 let best_wheel = self.0.best_wheel_index.map(|i| &self.0.wheels[i]);
461 match (&best_wheel, &self.0.source) {
462 (
466 Some((wheel, WheelCompatibility::Compatible(wheel_hash, ..))),
467 Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))),
468 ) => {
469 if sdist_hash > wheel_hash {
470 Some(CompatibleDist::SourceDist {
471 sdist,
472 prioritized: self,
473 })
474 } else {
475 Some(CompatibleDist::CompatibleWheel {
476 wheel,
477 prioritized: self,
478 })
479 }
480 }
481 (Some((wheel, WheelCompatibility::Compatible(..))), _) => {
483 Some(CompatibleDist::CompatibleWheel {
484 wheel,
485 prioritized: self,
486 })
487 }
488 (
496 Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))),
497 Some((sdist, SourceDistCompatibility::Compatible(_))),
498 ) if !compatibility.is_excluded() => Some(CompatibleDist::IncompatibleWheel {
499 sdist,
500 wheel,
501 prioritized: self,
502 }),
503 (.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
505 Some(CompatibleDist::SourceDist {
506 sdist,
507 prioritized: self,
508 })
509 }
510 _ => None,
511 }
512 }
513
514 pub(crate) fn incompatible_source(&self) -> Option<&IncompatibleSource> {
516 self.0
517 .source
518 .as_ref()
519 .and_then(|(_, compatibility)| match compatibility {
520 SourceDistCompatibility::Compatible(_) => None,
521 SourceDistCompatibility::Incompatible(incompatibility) => Some(incompatibility),
522 })
523 }
524
525 pub(crate) fn incompatible_wheel(&self) -> Option<&IncompatibleWheel> {
527 self.0
528 .best_wheel_index
529 .map(|i| &self.0.wheels[i])
530 .and_then(|(_, compatibility)| match compatibility {
531 WheelCompatibility::Compatible(_, _, _) => None,
532 WheelCompatibility::Incompatible(incompatibility) => Some(incompatibility),
533 })
534 }
535
536 pub(crate) fn hashes(&self) -> &[HashDigest] {
538 &self.0.hashes
539 }
540
541 pub(crate) fn is_empty(&self) -> bool {
544 self.0.source.is_none() && self.0.wheels.is_empty()
545 }
546
547 pub(crate) fn built_dist(&self) -> Option<RegistryBuiltDist> {
550 let best_wheel_index = self.0.best_wheel_index?;
551
552 let mut adjusted_wheels = Vec::with_capacity(self.0.wheels.len());
555 let mut adjusted_best_index = 0;
556 for (i, (wheel, compatibility)) in self.0.wheels.iter().enumerate() {
557 if compatibility.is_excluded() {
558 continue;
559 }
560 if i == best_wheel_index {
561 adjusted_best_index = adjusted_wheels.len();
562 }
563 adjusted_wheels.push(wheel.clone());
564 }
565
566 let sdist = self
567 .0
568 .source
569 .as_ref()
570 .filter(|(_, compatibility)| !compatibility.is_excluded())
571 .map(|(sdist, _)| sdist.clone());
572 Some(RegistryBuiltDist {
573 wheels: adjusted_wheels,
574 best_wheel_index: adjusted_best_index,
575 sdist,
576 })
577 }
578
579 pub(crate) fn source_dist(&self) -> Option<RegistrySourceDist> {
582 let mut sdist = self
583 .0
584 .source
585 .as_ref()
586 .filter(|(_, compatibility)| !compatibility.is_excluded())
587 .map(|(sdist, _)| sdist.clone())?;
588 assert!(
589 sdist.wheels.is_empty(),
590 "source distribution should not have any wheels yet"
591 );
592 sdist.wheels = self
593 .0
594 .wheels
595 .iter()
596 .filter(|(_, compatibility)| !compatibility.is_excluded())
597 .map(|(wheel, _)| wheel.clone())
598 .collect();
599 Some(sdist)
600 }
601
602 pub(crate) fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
605 self.0.best_wheel_index.map(|i| &self.0.wheels[i])
606 }
607
608 pub(crate) fn files(&self) -> impl Iterator<Item = &File> {
610 self.0
611 .wheels
612 .iter()
613 .map(|(wheel, _)| wheel.file.as_ref())
614 .chain(
615 self.0
616 .source
617 .as_ref()
618 .map(|(source_dist, _)| source_dist.file.as_ref()),
619 )
620 }
621
622 pub(crate) fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
624 self.0
625 .wheels
626 .iter()
627 .flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied())
628 }
629
630 pub(crate) fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
632 self.0
633 .wheels
634 .iter()
635 .flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied())
636 }
637
638 pub(crate) fn platform_tags<'a>(
641 &'a self,
642 tags: &'a Tags,
643 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
644 self.0.wheels.iter().flat_map(move |(wheel, _)| {
645 if wheel.filename.python_tags().iter().any(|wheel_py| {
646 wheel
647 .filename
648 .abi_tags()
649 .iter()
650 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
651 }) {
652 wheel.filename.platform_tags().iter()
653 } else {
654 [].iter()
655 }
656 })
657 }
658}
659
660impl<'a> CompatibleDist<'a> {
661 pub(crate) fn for_resolution(&self) -> ResolvedDistRef<'a> {
663 match self {
664 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
665 Self::SourceDist { sdist, prioritized } => {
666 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
667 }
668 Self::CompatibleWheel {
669 wheel, prioritized, ..
670 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
671 Self::IncompatibleWheel {
672 wheel, prioritized, ..
673 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
674 }
675 }
676
677 pub(crate) fn for_installation(&self) -> ResolvedDistRef<'a> {
679 match self {
680 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
681 Self::SourceDist { sdist, prioritized } => {
682 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
683 }
684 Self::CompatibleWheel {
685 wheel, prioritized, ..
686 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
687 Self::IncompatibleWheel {
688 sdist, prioritized, ..
689 } => ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized },
690 }
691 }
692
693 pub(crate) fn wheel(&self) -> Option<&RegistryBuiltWheel> {
696 match self {
697 Self::InstalledDist(_) => None,
698 Self::SourceDist { .. } => None,
699 Self::CompatibleWheel { wheel, .. } => Some(wheel),
700 Self::IncompatibleWheel { wheel, .. } => Some(wheel),
701 }
702 }
703}
704
705impl WheelCompatibility {
706 fn is_compatible(&self) -> bool {
708 matches!(self, Self::Compatible(_, _, _))
709 }
710
711 fn is_excluded(&self) -> bool {
713 matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
714 }
715
716 fn is_more_compatible(&self, other: &Self) -> bool {
721 match (self, other) {
722 (Self::Compatible(_, _, _), Self::Incompatible(_)) => true,
723 (
724 Self::Compatible(hash, tag_priority, build_tag),
725 Self::Compatible(other_hash, other_tag_priority, other_build_tag),
726 ) => {
727 (hash, tag_priority, build_tag) > (other_hash, other_tag_priority, other_build_tag)
728 }
729 (Self::Incompatible(_), Self::Compatible(_, _, _)) => false,
730 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
731 incompatibility.is_more_compatible(other_incompatibility)
732 }
733 }
734 }
735}
736
737impl SourceDistCompatibility {
738 fn is_compatible(&self) -> bool {
740 matches!(self, Self::Compatible(_))
741 }
742
743 fn is_excluded(&self) -> bool {
745 matches!(
746 self,
747 Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
748 )
749 }
750
751 fn is_more_compatible(&self, other: &Self) -> bool {
757 match (self, other) {
758 (Self::Compatible(_), Self::Incompatible(_)) => true,
759 (Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => {
760 compatibility > other_compatibility
761 }
762 (Self::Incompatible(_), Self::Compatible(_)) => false,
763 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
764 incompatibility.is_more_compatible(other_incompatibility)
765 }
766 }
767 }
768}
769
770impl IncompatibleSource {
771 fn is_more_compatible(&self, other: &Self) -> bool {
772 match self {
773 Self::ExcludeNewer(timestamp_self) => match other {
774 Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
776 Self::NoBuild
777 | Self::RequiresPython(_, _)
778 | Self::Yanked(_)
779 | Self::NotPep625Filename => true,
780 },
781 Self::RequiresPython(_, _) => match other {
782 Self::ExcludeNewer(_) => false,
783 Self::RequiresPython(_, _) => false,
785 Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true,
786 },
787 Self::Yanked(_) => match other {
788 Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => {
789 false
790 }
791 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
793 Self::NoBuild => true,
794 },
795 Self::NoBuild | Self::NotPep625Filename => false,
796 }
797 }
798}
799
800impl IncompatibleWheel {
801 fn is_more_compatible(&self, other: &Self) -> bool {
802 match self {
803 Self::ExcludeNewer(timestamp_self) => match other {
804 Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
806 (None, _) => true,
807 (_, None) => false,
808 (Some(timestamp_self), Some(timestamp_other)) => {
809 timestamp_other < timestamp_self
810 }
811 },
812 Self::MissingPlatform(_)
813 | Self::NoBinary
814 | Self::RequiresPython(_, _)
815 | Self::Tag(_)
816 | Self::Yanked(_) => true,
817 },
818 Self::Tag(tag_self) => match other {
819 Self::ExcludeNewer(_) => false,
820 Self::Tag(tag_other) => tag_self > tag_other,
821 Self::MissingPlatform(_)
822 | Self::NoBinary
823 | Self::RequiresPython(_, _)
824 | Self::Yanked(_) => true,
825 },
826 Self::RequiresPython(_, _) => match other {
827 Self::ExcludeNewer(_) | Self::Tag(_) => false,
828 Self::RequiresPython(_, _) => false,
830 Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true,
831 },
832 Self::Yanked(_) => match other {
833 Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false,
834 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
836 Self::MissingPlatform(_) | Self::NoBinary => true,
837 },
838 Self::NoBinary => match other {
839 Self::ExcludeNewer(_)
840 | Self::Tag(_)
841 | Self::RequiresPython(_, _)
842 | Self::Yanked(_) => false,
843 Self::NoBinary => false,
844 Self::MissingPlatform(_) => true,
845 },
846 Self::MissingPlatform(_) => false,
847 }
848 }
849}
850
851pub(crate) fn implied_markers(
855 filename: &WheelFilename,
856 minimum_libc_version: Option<MinimumLibcVersion>,
857) -> MarkerTree {
858 let python = implied_python_markers(filename);
859 let [glibc, musl] = implied_libc_markers(filename, python, minimum_libc_version);
860 glibc.and(musl)
861}
862
863fn implied_libc_markers(
865 filename: &WheelFilename,
866 python: MarkerTree,
867 minimum_libc_version: Option<MinimumLibcVersion>,
868) -> [MarkerTree; 2] {
869 let Some(minimum_libc_version) = minimum_libc_version else {
870 return [implied_platform_markers(filename.platform_tags()).and(python); 2];
871 };
872 let mut markers = [MarkerTree::FALSE; 2];
873 for tag in filename.platform_tags() {
874 let platform = implied_platform_markers([tag]).and(python);
875 for (markers, supported) in markers
876 .iter_mut()
877 .zip(minimum_libc_version.platform_coverage(tag))
878 {
879 if supported {
880 *markers = markers.or(platform);
881 }
882 }
883 }
884 markers
885}
886
887fn implied_platform_markers<'a>(
889 platform_tags: impl IntoIterator<Item = &'a PlatformTag>,
890) -> MarkerTree {
891 let mut marker = MarkerTree::FALSE;
892 for platform_tag in platform_tags {
893 match platform_tag {
894 PlatformTag::Any => {
895 return MarkerTree::TRUE;
896 }
897
898 PlatformTag::Win32 => {
900 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
901 key: MarkerValueString::SysPlatform,
902 operator: MarkerOperator::Equal,
903 value: arcstr::literal!("win32"),
904 });
905 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
906 key: MarkerValueString::PlatformMachine,
907 operator: MarkerOperator::Equal,
908 value: arcstr::literal!("x86"),
909 }));
910 marker = marker.or(tag_marker);
911 }
912 PlatformTag::WinAmd64 => {
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!("AMD64"),
922 }));
923 marker = marker.or(tag_marker);
924 }
925 PlatformTag::WinArm64 => {
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!("ARM64"),
935 }));
936 marker = marker.or(tag_marker);
937 }
938
939 PlatformTag::Macos {
941 major,
942 minor,
943 binary_format,
944 } => {
945 let Some(release) = macos_darwin_release(*major, *minor) else {
946 continue;
947 };
948 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
949 key: MarkerValueString::SysPlatform,
950 operator: MarkerOperator::Equal,
951 value: arcstr::literal!("darwin"),
952 });
953
954 let mut arch_marker = MarkerTree::FALSE;
956 for arch in binary_format.platform_machine() {
957 let release = if *arch == BinaryFormat::Arm64 {
960 release.clone().max(Version::new([20, 0, 0]))
961 } else {
962 release.clone()
963 };
964 let architecture = MarkerTree::expression(MarkerExpression::String {
965 key: MarkerValueString::PlatformMachine,
966 operator: MarkerOperator::Equal,
967 value: ArcStr::from(arch.name()),
968 });
969 let release = MarkerTree::expression(MarkerExpression::String {
970 key: MarkerValueString::PlatformRelease,
971 operator: MarkerOperator::GreaterEqual,
972 value: ArcStr::from(release.to_string()),
973 });
974 arch_marker = arch_marker.or(architecture.and(release));
975 }
976 tag_marker = tag_marker.and(arch_marker);
977
978 marker = marker.or(tag_marker);
979 }
980
981 PlatformTag::Manylinux { arch, .. }
983 | PlatformTag::Manylinux1 { arch, .. }
984 | PlatformTag::Manylinux2010 { arch, .. }
985 | PlatformTag::Manylinux2014 { arch, .. }
986 | PlatformTag::Musllinux { arch, .. }
987 | PlatformTag::Linux { arch } => {
988 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
989 key: MarkerValueString::SysPlatform,
990 operator: MarkerOperator::Equal,
991 value: arcstr::literal!("linux"),
992 });
993 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
994 key: MarkerValueString::PlatformMachine,
995 operator: MarkerOperator::Equal,
996 value: ArcStr::from(arch.name()),
997 }));
998 marker = marker.or(tag_marker);
999 }
1000
1001 tag => {
1002 debug!("Unknown platform tag in wheel tag: {tag}");
1003 }
1004 }
1005 }
1006
1007 marker
1008}
1009
1010fn macos_darwin_release(major: u16, minor: u16) -> Option<Version> {
1016 let release = match (major, minor) {
1017 (10, 0) => [1, 3, 0],
1018 (10, 1) => [1, 4, 1],
1019 (10, 2..=16) => [u64::from(minor) + 4, 0, 0],
1020 (11..=15, 0) => [u64::from(major) + 9, 0, 0],
1021 (26, 0) => [25, 0, 0],
1022 (27.., 0) => [u64::from(major), 0, 0],
1023 _ => return None,
1024 };
1025 Some(Version::new(release))
1026}
1027
1028fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
1033 let mut marker = MarkerTree::FALSE;
1034
1035 let is_abi3 = filename.abi_tags().iter().any(|tag| tag.is_stable_abi());
1039
1040 for python_tag in filename.python_tags() {
1041 let mut tree = match python_tag {
1043 LanguageTag::None => {
1044 return MarkerTree::TRUE;
1046 }
1047 LanguageTag::Python { major, minor: None } | LanguageTag::CPythonMajor { major } => {
1048 if is_abi3 {
1049 MarkerTree::expression(MarkerExpression::Version {
1050 key: uv_pep508::MarkerValueVersion::PythonVersion,
1051 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1052 u64::from(*major),
1053 ])),
1054 })
1055 } else {
1056 MarkerTree::expression(MarkerExpression::Version {
1057 key: uv_pep508::MarkerValueVersion::PythonVersion,
1058 specifier: VersionSpecifier::equals_star_version(Version::new([
1059 u64::from(*major),
1060 ])),
1061 })
1062 }
1063 }
1064 LanguageTag::Python {
1065 major,
1066 minor: Some(minor),
1067 } => {
1068 MarkerTree::expression(MarkerExpression::Version {
1070 key: uv_pep508::MarkerValueVersion::PythonVersion,
1071 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1072 u64::from(*major),
1073 u64::from(*minor),
1074 ])),
1075 })
1076 .and(MarkerTree::expression(MarkerExpression::Version {
1077 key: uv_pep508::MarkerValueVersion::PythonVersion,
1078 specifier: VersionSpecifier::equals_star_version(Version::new([u64::from(
1079 *major,
1080 )])),
1081 }))
1082 }
1083 LanguageTag::CPython {
1084 python_version: (major, minor),
1085 }
1086 | LanguageTag::PyPy {
1087 python_version: (major, minor),
1088 }
1089 | LanguageTag::GraalPy {
1090 python_version: (major, minor),
1091 }
1092 | LanguageTag::Pyston {
1093 python_version: (major, minor),
1094 } => {
1095 if is_abi3 {
1096 MarkerTree::expression(MarkerExpression::Version {
1097 key: uv_pep508::MarkerValueVersion::PythonVersion,
1098 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1099 u64::from(*major),
1100 u64::from(*minor),
1101 ])),
1102 })
1103 } else {
1104 MarkerTree::expression(MarkerExpression::Version {
1105 key: uv_pep508::MarkerValueVersion::PythonVersion,
1106 specifier: VersionSpecifier::equals_star_version(Version::new([
1107 u64::from(*major),
1108 u64::from(*minor),
1109 ])),
1110 })
1111 }
1112 }
1113 };
1114
1115 match python_tag {
1117 LanguageTag::None | LanguageTag::Python { .. } => {
1118 }
1120 LanguageTag::CPython { .. } | LanguageTag::CPythonMajor { .. } => {
1121 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1122 key: MarkerValueString::PlatformPythonImplementation,
1123 operator: MarkerOperator::Equal,
1124 value: arcstr::literal!("CPython"),
1125 }));
1126 }
1127 LanguageTag::PyPy { .. } => {
1128 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1129 key: MarkerValueString::PlatformPythonImplementation,
1130 operator: MarkerOperator::Equal,
1131 value: arcstr::literal!("PyPy"),
1132 }));
1133 }
1134 LanguageTag::GraalPy { .. } => {
1135 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1136 key: MarkerValueString::PlatformPythonImplementation,
1137 operator: MarkerOperator::Equal,
1138 value: arcstr::literal!("GraalPy"),
1139 }));
1140 }
1141 LanguageTag::Pyston { .. } => {
1142 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1143 key: MarkerValueString::PlatformPythonImplementation,
1144 operator: MarkerOperator::Equal,
1145 value: arcstr::literal!("Pyston"),
1146 }));
1147 }
1148 }
1149
1150 marker = marker.or(tree);
1151 }
1152
1153 marker
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use std::str::FromStr;
1159
1160 use super::*;
1161
1162 #[track_caller]
1163 fn assert_platform_markers(filename: &str, expected: &str) {
1164 let filename = WheelFilename::from_str(filename).unwrap();
1165 assert_eq!(
1166 implied_platform_markers(filename.platform_tags()),
1167 expected.parse::<MarkerTree>().unwrap()
1168 );
1169 }
1170
1171 #[track_caller]
1172 fn assert_python_markers(filename: &str, expected: &str) {
1173 let filename = WheelFilename::from_str(filename).unwrap();
1174 assert_eq!(
1175 implied_python_markers(&filename),
1176 expected.parse::<MarkerTree>().unwrap()
1177 );
1178 }
1179
1180 #[track_caller]
1181 fn assert_implied_markers(filename: &str, expected: &str) {
1182 let filename = WheelFilename::from_str(filename).unwrap();
1183 assert_eq!(
1184 implied_markers(&filename, None),
1185 expected.parse::<MarkerTree>().unwrap()
1186 );
1187 }
1188
1189 #[test]
1190 fn test_implied_platform_markers() {
1191 let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
1192 assert_eq!(
1193 implied_platform_markers(filename.platform_tags()),
1194 MarkerTree::TRUE
1195 );
1196
1197 assert_platform_markers(
1198 "example-1.0-cp310-cp310-win32.whl",
1199 "sys_platform == 'win32' and platform_machine == 'x86'",
1200 );
1201 assert_platform_markers(
1202 "numpy-2.2.1-cp313-cp313t-win_amd64.whl",
1203 "sys_platform == 'win32' and platform_machine == 'AMD64'",
1204 );
1205 assert_platform_markers(
1206 "numpy-2.2.1-cp313-cp313t-win_arm64.whl",
1207 "sys_platform == 'win32' and platform_machine == 'ARM64'",
1208 );
1209 assert_platform_markers(
1210 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1211 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1212 );
1213 assert_platform_markers(
1214 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
1215 "sys_platform == 'linux' and platform_machine == 'x86_64'",
1216 );
1217 assert_platform_markers(
1218 "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",
1219 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1220 );
1221 assert_platform_markers(
1222 "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl",
1223 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '23.0.0'",
1224 );
1225 assert_platform_markers(
1226 "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl",
1227 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '13.0.0'",
1228 );
1229 assert_platform_markers(
1230 "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl",
1231 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '20.0.0'",
1232 );
1233 }
1234
1235 #[test]
1236 fn test_implied_python_markers() {
1237 let filename = WheelFilename::from_str("example-1.0-none-none-any.whl").unwrap();
1238 assert_eq!(implied_python_markers(&filename), MarkerTree::TRUE);
1239
1240 assert_python_markers(
1241 "example-1.0-cp310-cp310-any.whl",
1242 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython'",
1243 );
1244 assert_python_markers(
1245 "example-1.0-cp311-cp311-any.whl",
1246 "python_full_version == '3.11.*' and platform_python_implementation == 'CPython'",
1247 );
1248 assert_python_markers(
1249 "example-1.0-cp312-cp312-any.whl",
1250 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython'",
1251 );
1252 assert_python_markers(
1253 "example-1.0-cp313-cp313-any.whl",
1254 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1255 );
1256 assert_python_markers(
1257 "example-1.0-cp313-cp313t-any.whl",
1258 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1259 );
1260 assert_python_markers(
1261 "example-1.0-pp310-pypy310_pp73-any.whl",
1262 "python_full_version == '3.10.*' and platform_python_implementation == 'PyPy'",
1263 );
1264 assert_python_markers(
1265 "example-1.0-py310-none-any.whl",
1266 "python_full_version >= '3.10' and python_full_version < '4'",
1267 );
1268 assert_python_markers(
1269 "example-1.0-py3-none-any.whl",
1270 "python_full_version >= '3' and python_full_version < '4'",
1271 );
1272 assert_python_markers(
1273 "example-1.0-py311.py312-none-any.whl",
1274 "python_full_version >= '3.11' and python_full_version < '4'",
1275 );
1276
1277 assert_python_markers(
1279 "example-1.0-cp39-abi3-any.whl",
1280 "python_full_version >= '3.9' and platform_python_implementation == 'CPython'",
1281 );
1282 assert_python_markers(
1283 "example-1.0-cp312-abi3-any.whl",
1284 "python_full_version >= '3.12' and platform_python_implementation == 'CPython'",
1285 );
1286 assert_python_markers(
1287 "example-1.0-cp315-abi3t-any.whl",
1288 "python_full_version >= '3.15' and platform_python_implementation == 'CPython'",
1289 );
1290 assert_python_markers(
1291 "example-1.0-cp3-abi3-any.whl",
1292 "python_full_version >= '3' and platform_python_implementation == 'CPython'",
1293 );
1294 }
1295
1296 #[test]
1297 fn test_macos_platform_markers() {
1298 for (tag, expected) in [
1299 (
1300 "macosx_10_5_x86_64",
1301 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '9.0.0'",
1302 ),
1303 (
1304 "macosx_10_9_universal2",
1305 "sys_platform == 'darwin' and ((platform_machine == 'x86_64' and platform_release >= '13.0.0') or (platform_machine == 'arm64' and platform_release >= '20.0.0'))",
1306 ),
1307 (
1308 "macosx_10_16_universal2",
1309 "sys_platform == 'darwin' and (platform_machine == 'arm64' or platform_machine == 'x86_64') and platform_release >= '20.0.0'",
1310 ),
1311 (
1312 "macosx_15_0_arm64",
1313 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '24.0.0'",
1314 ),
1315 (
1316 "macosx_26_0_arm64",
1317 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '25.0.0'",
1318 ),
1319 (
1320 "macosx_27_0_arm64",
1321 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '27.0.0'",
1322 ),
1323 (
1324 "macosx_26_0_arm64.macosx_15_0_x86_64",
1325 "sys_platform == 'darwin' and ((platform_machine == 'arm64' and platform_release >= '25.0.0') or (platform_machine == 'x86_64' and platform_release >= '24.0.0'))",
1326 ),
1327 ] {
1328 let filename =
1329 WheelFilename::from_str(&format!("example-1.0-py3-none-{tag}.whl")).unwrap();
1330 assert_eq!(
1331 implied_platform_markers(filename.platform_tags()),
1332 MarkerTree::from_str(expected).unwrap()
1333 );
1334 }
1335 }
1336
1337 #[test]
1338 fn test_implied_markers() {
1339 assert_implied_markers(
1340 "numpy-1.0-cp310-cp310-win32.whl",
1341 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'",
1342 );
1343 assert_implied_markers(
1344 "pywin32-311-cp314-cp314-win_arm64.whl",
1345 "python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'",
1346 );
1347 assert_implied_markers(
1348 "numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl",
1349 "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'",
1350 );
1351 assert_implied_markers(
1352 "numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1353 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'",
1354 );
1355 assert_implied_markers(
1356 "example-1.0-py3-none-any.whl",
1357 "python_full_version >= '3' and python_full_version < '4'",
1358 );
1359
1360 assert_implied_markers(
1362 "example-1.0-cp39-abi3-manylinux_2_28_x86_64.whl",
1363 "python_full_version >= '3.9' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1364 );
1365 assert_implied_markers(
1366 "example-1.0-cp315-abi3t-manylinux_2_28_x86_64.whl",
1367 "python_full_version >= '3.15' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1368 );
1369 }
1370}