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, 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 CompatibleDist<'_> {
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<&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 Compatible(HashComparison, Option<TagPriority>, Option<BuildTag>),
363}
364
365#[derive(Debug, PartialEq, Eq, Clone)]
366pub enum IncompatibleWheel {
367 ExcludeNewer(Option<i64>),
369 Tag(IncompatibleTag),
371 RequiresPython(VersionSpecifiers, PythonRequirementKind),
373 Yanked(Yanked),
375 NoBinary,
377 MissingPlatform(MarkerTree),
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum SourceDistCompatibility {
383 Incompatible(IncompatibleSource),
384 Compatible(HashComparison),
385}
386
387#[derive(Debug, PartialEq, Eq, Clone)]
388pub enum IncompatibleSource {
389 ExcludeNewer(Option<i64>),
390 RequiresPython(VersionSpecifiers, PythonRequirementKind),
391 Yanked(Yanked),
392 NoBuild,
393 NotPep625Filename,
395}
396
397#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
398pub enum HashComparison {
399 Mismatched,
401 Missing,
403 Matched,
405}
406
407impl PrioritizedDist {
408 pub(crate) fn insert_variants_json(&mut self, variants_json: RegistryVariantsJson) {
409 if !self.0.variants_json.contains(&variants_json) {
411 self.0.variants_json.push(variants_json);
412 }
413 }
414
415 pub(crate) fn variants_json(&self) -> &[RegistryVariantsJson] {
416 &self.0.variants_json
417 }
418
419 pub(crate) fn insert_built(
421 &mut self,
422 dist: RegistryBuiltWheel,
423 hashes: impl IntoIterator<Item = HashDigest>,
424 compatibility: WheelCompatibility,
425 minimum_libc_version: Option<MinimumLibcVersion>,
426 ) {
427 if compatibility.is_compatible()
428 && (!self.0.python_markers.is_true()
429 || !self.0.markers.iter().all(|markers| markers.is_true()))
430 {
431 let python = implied_python_markers(&dist.filename);
432 self.0.python_markers = self.0.python_markers.or(python);
433 for (coverage, markers) in self.0.markers.iter_mut().zip(implied_libc_markers(
434 &dist.filename,
435 python,
436 minimum_libc_version,
437 )) {
438 *coverage = coverage.or(markers);
439 }
440 }
441 if !compatibility.is_excluded() {
443 self.0.hashes.extend(hashes);
444 }
445 if let Some((existing_wheel, existing_compatibility)) = self.best_wheel() {
448 let is_more_compatible =
449 match (dist.filename.variant(), existing_wheel.filename.variant()) {
450 (None, Some(_)) => true,
451 (Some(_), None) => false,
452 (None, None) | (Some(_), Some(_)) => {
453 compatibility.is_more_compatible(existing_compatibility)
454 }
455 };
456 if is_more_compatible {
457 self.0.best_wheel_index = Some(self.0.wheels.len());
458 }
459 } else {
460 self.0.best_wheel_index = Some(self.0.wheels.len());
461 }
462 self.0.wheels.push((dist, compatibility));
463 }
464
465 pub fn insert_source(
467 &mut self,
468 dist: RegistrySourceDist,
469 hashes: impl IntoIterator<Item = HashDigest>,
470 compatibility: SourceDistCompatibility,
471 ) {
472 if compatibility.is_compatible() {
474 self.0.python_markers = MarkerTree::TRUE;
475 self.0.markers = [MarkerTree::TRUE; 2];
476 }
477 if !compatibility.is_excluded() {
479 self.0.hashes.extend(hashes);
480 }
481 if let Some((.., existing_compatibility)) = &self.0.source {
483 if compatibility.is_more_compatible(existing_compatibility) {
484 self.0.source = Some((dist, compatibility));
485 }
486 } else {
487 self.0.source = Some((dist, compatibility));
488 }
489 }
490
491 pub(crate) fn get(&self) -> Option<CompatibleDist<'_>> {
493 let best_wheel = self.0.best_wheel_index.map(|i| &self.0.wheels[i]);
494 match (&best_wheel, &self.0.source) {
495 (
499 Some((wheel, WheelCompatibility::Compatible(wheel_hash, ..))),
500 Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))),
501 ) => {
502 if sdist_hash > wheel_hash {
503 Some(CompatibleDist::SourceDist {
504 sdist,
505 prioritized: self,
506 })
507 } else {
508 Some(CompatibleDist::CompatibleWheel {
509 wheel,
510 prioritized: self,
511 })
512 }
513 }
514 (Some((wheel, WheelCompatibility::Compatible(..))), _) => {
516 Some(CompatibleDist::CompatibleWheel {
517 wheel,
518 prioritized: self,
519 })
520 }
521 (
529 Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))),
530 Some((sdist, SourceDistCompatibility::Compatible(_))),
531 ) if !compatibility.is_excluded() && wheel.filename.variant().is_none() => {
532 Some(CompatibleDist::IncompatibleWheel {
533 sdist,
534 wheel,
535 prioritized: self,
536 })
537 }
538 (.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
540 Some(CompatibleDist::SourceDist {
541 sdist,
542 prioritized: self,
543 })
544 }
545 _ => None,
546 }
547 }
548
549 pub(crate) fn incompatible_source(&self) -> Option<&IncompatibleSource> {
551 self.0
552 .source
553 .as_ref()
554 .and_then(|(_, compatibility)| match compatibility {
555 SourceDistCompatibility::Compatible(_) => None,
556 SourceDistCompatibility::Incompatible(incompatibility) => Some(incompatibility),
557 })
558 }
559
560 pub(crate) fn incompatible_wheel(&self) -> Option<&IncompatibleWheel> {
562 self.0
563 .best_wheel_index
564 .map(|i| &self.0.wheels[i])
565 .and_then(|(_, compatibility)| match compatibility {
566 WheelCompatibility::Compatible(_, _, _) => None,
567 WheelCompatibility::Incompatible(incompatibility) => Some(incompatibility),
568 })
569 }
570
571 pub(crate) fn hashes(&self) -> &[HashDigest] {
573 &self.0.hashes
574 }
575
576 pub(crate) fn is_empty(&self) -> bool {
579 self.0.source.is_none() && self.0.wheels.is_empty()
580 }
581
582 pub(crate) fn built_dist(&self) -> Option<RegistryBuiltDist> {
585 let best_wheel_index = self.0.best_wheel_index?;
586
587 let mut adjusted_wheels = Vec::with_capacity(self.0.wheels.len());
590 let mut adjusted_best_index = 0;
591 for (i, (wheel, compatibility)) in self.0.wheels.iter().enumerate() {
592 if compatibility.is_excluded() {
593 continue;
594 }
595 if i == best_wheel_index {
596 adjusted_best_index = adjusted_wheels.len();
597 }
598 adjusted_wheels.push(wheel.clone());
599 }
600
601 let sdist = self
602 .0
603 .source
604 .as_ref()
605 .filter(|(_, compatibility)| !compatibility.is_excluded())
606 .map(|(sdist, _)| sdist.clone());
607 Some(RegistryBuiltDist {
608 wheels: adjusted_wheels,
609 best_wheel_index: adjusted_best_index,
610 sdist,
611 })
612 }
613
614 pub(crate) fn source_dist(&self) -> Option<RegistrySourceDist> {
617 let mut sdist = self
618 .0
619 .source
620 .as_ref()
621 .filter(|(_, compatibility)| !compatibility.is_excluded())
622 .map(|(sdist, _)| sdist.clone())?;
623 assert!(
624 sdist.wheels.is_empty(),
625 "source distribution should not have any wheels yet"
626 );
627 sdist.wheels = self
628 .0
629 .wheels
630 .iter()
631 .filter(|(_, compatibility)| !compatibility.is_excluded())
632 .map(|(wheel, _)| wheel.clone())
633 .collect();
634 Some(sdist)
635 }
636
637 pub(crate) fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
640 self.0.best_wheel_index.map(|i| &self.0.wheels[i])
641 }
642
643 pub(crate) fn files(&self) -> impl Iterator<Item = &File> {
645 self.0
646 .wheels
647 .iter()
648 .map(|(wheel, _)| wheel.file.as_ref())
649 .chain(
650 self.0
651 .source
652 .as_ref()
653 .map(|(source_dist, _)| source_dist.file.as_ref()),
654 )
655 }
656
657 pub(crate) fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
659 self.0
660 .wheels
661 .iter()
662 .flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied())
663 }
664
665 pub(crate) fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
667 self.0
668 .wheels
669 .iter()
670 .flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied())
671 }
672
673 pub(crate) fn platform_tags<'a>(
676 &'a self,
677 tags: &'a Tags,
678 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
679 self.0.wheels.iter().flat_map(move |(wheel, _)| {
680 if wheel.filename.python_tags().iter().any(|wheel_py| {
681 wheel
682 .filename
683 .abi_tags()
684 .iter()
685 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
686 }) {
687 wheel.filename.platform_tags().iter()
688 } else {
689 [].iter()
690 }
691 })
692 }
693}
694
695impl<'a> CompatibleDist<'a> {
696 pub(crate) fn for_resolution(&self) -> ResolvedDistRef<'a> {
698 match self {
699 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
700 Self::SourceDist { sdist, prioritized } => {
701 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
702 }
703 Self::CompatibleWheel {
704 wheel, prioritized, ..
705 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
706 Self::IncompatibleWheel {
707 wheel, prioritized, ..
708 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
709 }
710 }
711
712 pub(crate) fn for_installation(&self) -> ResolvedDistRef<'a> {
714 match self {
715 Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist },
716 Self::SourceDist { sdist, prioritized } => {
717 ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }
718 }
719 Self::CompatibleWheel {
720 wheel, prioritized, ..
721 } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized },
722 Self::IncompatibleWheel {
723 sdist, prioritized, ..
724 } => ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized },
725 }
726 }
727
728 pub(crate) fn wheel(&self) -> Option<&RegistryBuiltWheel> {
731 match self {
732 Self::InstalledDist(_) => None,
733 Self::SourceDist { .. } => None,
734 Self::CompatibleWheel { wheel, .. } => Some(wheel),
735 Self::IncompatibleWheel { wheel, .. } => Some(wheel),
736 }
737 }
738}
739
740impl WheelCompatibility {
741 fn is_compatible(&self) -> bool {
743 matches!(self, Self::Compatible(_, _, _))
744 }
745
746 fn is_excluded(&self) -> bool {
748 matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
749 }
750
751 fn is_more_compatible(&self, other: &Self) -> bool {
756 match (self, other) {
757 (Self::Compatible(_, _, _), Self::Incompatible(_)) => true,
758 (
759 Self::Compatible(hash, tag_priority, build_tag),
760 Self::Compatible(other_hash, other_tag_priority, other_build_tag),
761 ) => {
762 (hash, tag_priority, build_tag) > (other_hash, other_tag_priority, other_build_tag)
763 }
764 (Self::Incompatible(_), Self::Compatible(_, _, _)) => false,
765 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
766 incompatibility.is_more_compatible(other_incompatibility)
767 }
768 }
769 }
770}
771
772impl SourceDistCompatibility {
773 fn is_compatible(&self) -> bool {
775 matches!(self, Self::Compatible(_))
776 }
777
778 fn is_excluded(&self) -> bool {
780 matches!(
781 self,
782 Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
783 )
784 }
785
786 fn is_more_compatible(&self, other: &Self) -> bool {
792 match (self, other) {
793 (Self::Compatible(_), Self::Incompatible(_)) => true,
794 (Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => {
795 compatibility > other_compatibility
796 }
797 (Self::Incompatible(_), Self::Compatible(_)) => false,
798 (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
799 incompatibility.is_more_compatible(other_incompatibility)
800 }
801 }
802 }
803}
804
805impl IncompatibleSource {
806 fn is_more_compatible(&self, other: &Self) -> bool {
807 match self {
808 Self::ExcludeNewer(timestamp_self) => match other {
809 Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
811 Self::NoBuild
812 | Self::RequiresPython(_, _)
813 | Self::Yanked(_)
814 | Self::NotPep625Filename => true,
815 },
816 Self::RequiresPython(_, _) => match other {
817 Self::ExcludeNewer(_) => false,
818 Self::RequiresPython(_, _) => false,
820 Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true,
821 },
822 Self::Yanked(_) => match other {
823 Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => {
824 false
825 }
826 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
828 Self::NoBuild => true,
829 },
830 Self::NoBuild | Self::NotPep625Filename => false,
831 }
832 }
833}
834
835impl IncompatibleWheel {
836 fn is_more_compatible(&self, other: &Self) -> bool {
837 match self {
838 Self::ExcludeNewer(timestamp_self) => match other {
839 Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
841 (None, _) => true,
842 (_, None) => false,
843 (Some(timestamp_self), Some(timestamp_other)) => {
844 timestamp_other < timestamp_self
845 }
846 },
847 Self::MissingPlatform(_)
848 | Self::NoBinary
849 | Self::RequiresPython(_, _)
850 | Self::Tag(_)
851 | Self::Yanked(_) => true,
852 },
853 Self::Tag(tag_self) => match other {
854 Self::ExcludeNewer(_) => false,
855 Self::Tag(tag_other) => tag_self > tag_other,
856 Self::MissingPlatform(_)
857 | Self::NoBinary
858 | Self::RequiresPython(_, _)
859 | Self::Yanked(_) => true,
860 },
861 Self::RequiresPython(_, _) => match other {
862 Self::ExcludeNewer(_) | Self::Tag(_) => false,
863 Self::RequiresPython(_, _) => false,
865 Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true,
866 },
867 Self::Yanked(_) => match other {
868 Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false,
869 Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
871 Self::MissingPlatform(_) | Self::NoBinary => true,
872 },
873 Self::NoBinary => match other {
874 Self::ExcludeNewer(_)
875 | Self::Tag(_)
876 | Self::RequiresPython(_, _)
877 | Self::Yanked(_) => false,
878 Self::NoBinary => false,
879 Self::MissingPlatform(_) => true,
880 },
881 Self::MissingPlatform(_) => false,
882 }
883 }
884}
885
886pub(crate) fn implied_markers(
890 filename: &WheelFilename,
891 minimum_libc_version: Option<MinimumLibcVersion>,
892) -> MarkerTree {
893 let python = implied_python_markers(filename);
894 let [glibc, musl] = implied_libc_markers(filename, python, minimum_libc_version);
895 glibc.and(musl)
896}
897
898fn implied_libc_markers(
900 filename: &WheelFilename,
901 python: MarkerTree,
902 minimum_libc_version: Option<MinimumLibcVersion>,
903) -> [MarkerTree; 2] {
904 let Some(minimum_libc_version) = minimum_libc_version else {
905 return [implied_platform_markers(filename.platform_tags()).and(python); 2];
906 };
907 let mut markers = [MarkerTree::FALSE; 2];
908 for tag in filename.platform_tags() {
909 let platform = implied_platform_markers([tag]).and(python);
910 for (markers, supported) in markers
911 .iter_mut()
912 .zip(minimum_libc_version.platform_coverage(tag))
913 {
914 if supported {
915 *markers = markers.or(platform);
916 }
917 }
918 }
919 markers
920}
921
922fn implied_platform_markers<'a>(
924 platform_tags: impl IntoIterator<Item = &'a PlatformTag>,
925) -> MarkerTree {
926 let mut marker = MarkerTree::FALSE;
927 for platform_tag in platform_tags {
928 match platform_tag {
929 PlatformTag::Any => {
930 return MarkerTree::TRUE;
931 }
932
933 PlatformTag::Win32 => {
935 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
936 key: MarkerValueString::SysPlatform,
937 operator: MarkerOperator::Equal,
938 value: arcstr::literal!("win32"),
939 });
940 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
941 key: MarkerValueString::PlatformMachine,
942 operator: MarkerOperator::Equal,
943 value: arcstr::literal!("x86"),
944 }));
945 marker = marker.or(tag_marker);
946 }
947 PlatformTag::WinAmd64 => {
948 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
949 key: MarkerValueString::SysPlatform,
950 operator: MarkerOperator::Equal,
951 value: arcstr::literal!("win32"),
952 });
953 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
954 key: MarkerValueString::PlatformMachine,
955 operator: MarkerOperator::Equal,
956 value: arcstr::literal!("AMD64"),
957 }));
958 marker = marker.or(tag_marker);
959 }
960 PlatformTag::WinArm64 => {
961 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
962 key: MarkerValueString::SysPlatform,
963 operator: MarkerOperator::Equal,
964 value: arcstr::literal!("win32"),
965 });
966 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
967 key: MarkerValueString::PlatformMachine,
968 operator: MarkerOperator::Equal,
969 value: arcstr::literal!("ARM64"),
970 }));
971 marker = marker.or(tag_marker);
972 }
973
974 PlatformTag::Macos {
976 major,
977 minor,
978 binary_format,
979 } => {
980 let Some(release) = macos_darwin_release(*major, *minor) else {
981 continue;
982 };
983 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
984 key: MarkerValueString::SysPlatform,
985 operator: MarkerOperator::Equal,
986 value: arcstr::literal!("darwin"),
987 });
988
989 let mut arch_marker = MarkerTree::FALSE;
991 for arch in binary_format.platform_machine() {
992 let release = if *arch == BinaryFormat::Arm64 {
995 release.clone().max(Version::new([20, 0, 0]))
996 } else {
997 release.clone()
998 };
999 let architecture = MarkerTree::expression(MarkerExpression::String {
1000 key: MarkerValueString::PlatformMachine,
1001 operator: MarkerOperator::Equal,
1002 value: ArcStr::from(arch.name()),
1003 });
1004 let release = MarkerTree::expression(MarkerExpression::String {
1005 key: MarkerValueString::PlatformRelease,
1006 operator: MarkerOperator::GreaterEqual,
1007 value: ArcStr::from(release.to_string()),
1008 });
1009 arch_marker = arch_marker.or(architecture.and(release));
1010 }
1011 tag_marker = tag_marker.and(arch_marker);
1012
1013 marker = marker.or(tag_marker);
1014 }
1015
1016 PlatformTag::Manylinux { arch, .. }
1018 | PlatformTag::Manylinux1 { arch, .. }
1019 | PlatformTag::Manylinux2010 { arch, .. }
1020 | PlatformTag::Manylinux2014 { arch, .. }
1021 | PlatformTag::Musllinux { arch, .. }
1022 | PlatformTag::Linux { arch } => {
1023 let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
1024 key: MarkerValueString::SysPlatform,
1025 operator: MarkerOperator::Equal,
1026 value: arcstr::literal!("linux"),
1027 });
1028 tag_marker = tag_marker.and(MarkerTree::expression(MarkerExpression::String {
1029 key: MarkerValueString::PlatformMachine,
1030 operator: MarkerOperator::Equal,
1031 value: ArcStr::from(arch.name()),
1032 }));
1033 marker = marker.or(tag_marker);
1034 }
1035
1036 tag => {
1037 debug!("Unknown platform tag in wheel tag: {tag}");
1038 }
1039 }
1040 }
1041
1042 marker
1043}
1044
1045fn macos_darwin_release(major: u16, minor: u16) -> Option<Version> {
1051 let release = match (major, minor) {
1052 (10, 0) => [1, 3, 0],
1053 (10, 1) => [1, 4, 1],
1054 (10, 2..=16) => [u64::from(minor) + 4, 0, 0],
1055 (11..=15, 0) => [u64::from(major) + 9, 0, 0],
1056 (26, 0) => [25, 0, 0],
1057 (27.., 0) => [u64::from(major), 0, 0],
1058 _ => return None,
1059 };
1060 Some(Version::new(release))
1061}
1062
1063fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
1068 let mut marker = MarkerTree::FALSE;
1069
1070 let is_abi3 = filename.abi_tags().iter().any(|tag| tag.is_stable_abi());
1074
1075 for python_tag in filename.python_tags() {
1076 let mut tree = match python_tag {
1078 LanguageTag::None => {
1079 return MarkerTree::TRUE;
1081 }
1082 LanguageTag::Python { major, minor: None } | LanguageTag::CPythonMajor { major } => {
1083 if is_abi3 {
1084 MarkerTree::expression(MarkerExpression::Version {
1085 key: uv_pep508::MarkerValueVersion::PythonVersion,
1086 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1087 u64::from(*major),
1088 ])),
1089 })
1090 } else {
1091 MarkerTree::expression(MarkerExpression::Version {
1092 key: uv_pep508::MarkerValueVersion::PythonVersion,
1093 specifier: VersionSpecifier::equals_star_version(Version::new([
1094 u64::from(*major),
1095 ])),
1096 })
1097 }
1098 }
1099 LanguageTag::Python {
1100 major,
1101 minor: Some(minor),
1102 } => {
1103 MarkerTree::expression(MarkerExpression::Version {
1105 key: uv_pep508::MarkerValueVersion::PythonVersion,
1106 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1107 u64::from(*major),
1108 u64::from(*minor),
1109 ])),
1110 })
1111 .and(MarkerTree::expression(MarkerExpression::Version {
1112 key: uv_pep508::MarkerValueVersion::PythonVersion,
1113 specifier: VersionSpecifier::equals_star_version(Version::new([u64::from(
1114 *major,
1115 )])),
1116 }))
1117 }
1118 LanguageTag::CPython {
1119 python_version: (major, minor),
1120 }
1121 | LanguageTag::PyPy {
1122 python_version: (major, minor),
1123 }
1124 | LanguageTag::GraalPy {
1125 python_version: (major, minor),
1126 }
1127 | LanguageTag::Pyston {
1128 python_version: (major, minor),
1129 } => {
1130 if is_abi3 {
1131 MarkerTree::expression(MarkerExpression::Version {
1132 key: uv_pep508::MarkerValueVersion::PythonVersion,
1133 specifier: VersionSpecifier::greater_than_equal_version(Version::new([
1134 u64::from(*major),
1135 u64::from(*minor),
1136 ])),
1137 })
1138 } else {
1139 MarkerTree::expression(MarkerExpression::Version {
1140 key: uv_pep508::MarkerValueVersion::PythonVersion,
1141 specifier: VersionSpecifier::equals_star_version(Version::new([
1142 u64::from(*major),
1143 u64::from(*minor),
1144 ])),
1145 })
1146 }
1147 }
1148 };
1149
1150 match python_tag {
1152 LanguageTag::None | LanguageTag::Python { .. } => {
1153 }
1155 LanguageTag::CPython { .. } | LanguageTag::CPythonMajor { .. } => {
1156 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1157 key: MarkerValueString::PlatformPythonImplementation,
1158 operator: MarkerOperator::Equal,
1159 value: arcstr::literal!("CPython"),
1160 }));
1161 }
1162 LanguageTag::PyPy { .. } => {
1163 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1164 key: MarkerValueString::PlatformPythonImplementation,
1165 operator: MarkerOperator::Equal,
1166 value: arcstr::literal!("PyPy"),
1167 }));
1168 }
1169 LanguageTag::GraalPy { .. } => {
1170 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1171 key: MarkerValueString::PlatformPythonImplementation,
1172 operator: MarkerOperator::Equal,
1173 value: arcstr::literal!("GraalPy"),
1174 }));
1175 }
1176 LanguageTag::Pyston { .. } => {
1177 tree = tree.and(MarkerTree::expression(MarkerExpression::String {
1178 key: MarkerValueString::PlatformPythonImplementation,
1179 operator: MarkerOperator::Equal,
1180 value: arcstr::literal!("Pyston"),
1181 }));
1182 }
1183 }
1184
1185 marker = marker.or(tree);
1186 }
1187
1188 marker
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193 use std::str::FromStr;
1194
1195 use super::*;
1196
1197 #[track_caller]
1198 fn assert_platform_markers(filename: &str, expected: &str) {
1199 let filename = WheelFilename::from_str(filename).unwrap();
1200 assert_eq!(
1201 implied_platform_markers(filename.platform_tags()),
1202 expected.parse::<MarkerTree>().unwrap()
1203 );
1204 }
1205
1206 #[track_caller]
1207 fn assert_python_markers(filename: &str, expected: &str) {
1208 let filename = WheelFilename::from_str(filename).unwrap();
1209 assert_eq!(
1210 implied_python_markers(&filename),
1211 expected.parse::<MarkerTree>().unwrap()
1212 );
1213 }
1214
1215 #[track_caller]
1216 fn assert_implied_markers(filename: &str, expected: &str) {
1217 let filename = WheelFilename::from_str(filename).unwrap();
1218 assert_eq!(
1219 implied_markers(&filename, None),
1220 expected.parse::<MarkerTree>().unwrap()
1221 );
1222 }
1223
1224 #[test]
1225 fn test_implied_platform_markers() {
1226 let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap();
1227 assert_eq!(
1228 implied_platform_markers(filename.platform_tags()),
1229 MarkerTree::TRUE
1230 );
1231
1232 assert_platform_markers(
1233 "example-1.0-cp310-cp310-win32.whl",
1234 "sys_platform == 'win32' and platform_machine == 'x86'",
1235 );
1236 assert_platform_markers(
1237 "numpy-2.2.1-cp313-cp313t-win_amd64.whl",
1238 "sys_platform == 'win32' and platform_machine == 'AMD64'",
1239 );
1240 assert_platform_markers(
1241 "numpy-2.2.1-cp313-cp313t-win_arm64.whl",
1242 "sys_platform == 'win32' and platform_machine == 'ARM64'",
1243 );
1244 assert_platform_markers(
1245 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1246 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1247 );
1248 assert_platform_markers(
1249 "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
1250 "sys_platform == 'linux' and platform_machine == 'x86_64'",
1251 );
1252 assert_platform_markers(
1253 "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",
1254 "sys_platform == 'linux' and platform_machine == 'aarch64'",
1255 );
1256 assert_platform_markers(
1257 "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl",
1258 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '23.0.0'",
1259 );
1260 assert_platform_markers(
1261 "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl",
1262 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '13.0.0'",
1263 );
1264 assert_platform_markers(
1265 "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl",
1266 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '20.0.0'",
1267 );
1268 }
1269
1270 #[test]
1271 fn test_implied_python_markers() {
1272 let filename = WheelFilename::from_str("example-1.0-none-none-any.whl").unwrap();
1273 assert_eq!(implied_python_markers(&filename), MarkerTree::TRUE);
1274
1275 assert_python_markers(
1276 "example-1.0-cp310-cp310-any.whl",
1277 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython'",
1278 );
1279 assert_python_markers(
1280 "example-1.0-cp311-cp311-any.whl",
1281 "python_full_version == '3.11.*' and platform_python_implementation == 'CPython'",
1282 );
1283 assert_python_markers(
1284 "example-1.0-cp312-cp312-any.whl",
1285 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython'",
1286 );
1287 assert_python_markers(
1288 "example-1.0-cp313-cp313-any.whl",
1289 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1290 );
1291 assert_python_markers(
1292 "example-1.0-cp313-cp313t-any.whl",
1293 "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'",
1294 );
1295 assert_python_markers(
1296 "example-1.0-pp310-pypy310_pp73-any.whl",
1297 "python_full_version == '3.10.*' and platform_python_implementation == 'PyPy'",
1298 );
1299 assert_python_markers(
1300 "example-1.0-py310-none-any.whl",
1301 "python_full_version >= '3.10' and python_full_version < '4'",
1302 );
1303 assert_python_markers(
1304 "example-1.0-py3-none-any.whl",
1305 "python_full_version >= '3' and python_full_version < '4'",
1306 );
1307 assert_python_markers(
1308 "example-1.0-py311.py312-none-any.whl",
1309 "python_full_version >= '3.11' and python_full_version < '4'",
1310 );
1311
1312 assert_python_markers(
1314 "example-1.0-cp39-abi3-any.whl",
1315 "python_full_version >= '3.9' and platform_python_implementation == 'CPython'",
1316 );
1317 assert_python_markers(
1318 "example-1.0-cp312-abi3-any.whl",
1319 "python_full_version >= '3.12' and platform_python_implementation == 'CPython'",
1320 );
1321 assert_python_markers(
1322 "example-1.0-cp315-abi3t-any.whl",
1323 "python_full_version >= '3.15' and platform_python_implementation == 'CPython'",
1324 );
1325 assert_python_markers(
1326 "example-1.0-cp3-abi3-any.whl",
1327 "python_full_version >= '3' and platform_python_implementation == 'CPython'",
1328 );
1329 }
1330
1331 #[test]
1332 fn test_macos_platform_markers() {
1333 for (tag, expected) in [
1334 (
1335 "macosx_10_5_x86_64",
1336 "sys_platform == 'darwin' and platform_machine == 'x86_64' and platform_release >= '9.0.0'",
1337 ),
1338 (
1339 "macosx_10_9_universal2",
1340 "sys_platform == 'darwin' and ((platform_machine == 'x86_64' and platform_release >= '13.0.0') or (platform_machine == 'arm64' and platform_release >= '20.0.0'))",
1341 ),
1342 (
1343 "macosx_10_16_universal2",
1344 "sys_platform == 'darwin' and (platform_machine == 'arm64' or platform_machine == 'x86_64') and platform_release >= '20.0.0'",
1345 ),
1346 (
1347 "macosx_15_0_arm64",
1348 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '24.0.0'",
1349 ),
1350 (
1351 "macosx_26_0_arm64",
1352 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '25.0.0'",
1353 ),
1354 (
1355 "macosx_27_0_arm64",
1356 "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release >= '27.0.0'",
1357 ),
1358 (
1359 "macosx_26_0_arm64.macosx_15_0_x86_64",
1360 "sys_platform == 'darwin' and ((platform_machine == 'arm64' and platform_release >= '25.0.0') or (platform_machine == 'x86_64' and platform_release >= '24.0.0'))",
1361 ),
1362 ] {
1363 let filename =
1364 WheelFilename::from_str(&format!("example-1.0-py3-none-{tag}.whl")).unwrap();
1365 assert_eq!(
1366 implied_platform_markers(filename.platform_tags()),
1367 MarkerTree::from_str(expected).unwrap()
1368 );
1369 }
1370 }
1371
1372 #[test]
1373 fn test_implied_markers() {
1374 assert_implied_markers(
1375 "numpy-1.0-cp310-cp310-win32.whl",
1376 "python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'",
1377 );
1378 assert_implied_markers(
1379 "pywin32-311-cp314-cp314-win_arm64.whl",
1380 "python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'",
1381 );
1382 assert_implied_markers(
1383 "numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl",
1384 "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'",
1385 );
1386 assert_implied_markers(
1387 "numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
1388 "python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'",
1389 );
1390 assert_implied_markers(
1391 "example-1.0-py3-none-any.whl",
1392 "python_full_version >= '3' and python_full_version < '4'",
1393 );
1394
1395 assert_implied_markers(
1397 "example-1.0-cp39-abi3-manylinux_2_28_x86_64.whl",
1398 "python_full_version >= '3.9' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1399 );
1400 assert_implied_markers(
1401 "example-1.0-cp315-abi3t-manylinux_2_28_x86_64.whl",
1402 "python_full_version >= '3.15' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'x86_64'",
1403 );
1404 }
1405}