Skip to main content

uv_resolver/
prioritized_distribution.rs

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/// A collection of distributions that have been filtered by relevance.
24#[derive(Debug, Default, Clone)]
25pub struct PrioritizedDist(Box<PrioritizedDistInner>);
26
27/// The contents of a [`PrioritizedDist`], boxed to keep the outer type small.
28#[derive(Debug, Clone)]
29struct PrioritizedDistInner {
30    /// The highest-priority source distribution. Between compatible source distributions this priority is arbitrary.
31    source: Option<(RegistrySourceDist, SourceDistCompatibility)>,
32    /// The highest-priority wheel index. When present, it is
33    /// guaranteed to be a valid index into `wheels`.
34    best_wheel_index: Option<usize>,
35    /// The set of all wheels associated with this distribution.
36    wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
37    /// The hashes for each distribution.
38    hashes: Vec<HashDigest>,
39    /// Python coverage, unioned over compatible wheels independently of their platforms.
40    python_markers: MarkerTree,
41    /// Coverage for the glibc and musl baselines, unioned over compatible wheels separately.
42    /// Unconfigured baselines use ordinary platform coverage. Intersect only after unioning, so
43    /// separate glibc and musl wheels can jointly satisfy both baselines.
44    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/// A distribution that can be used for both resolution and installation.
61#[derive(Debug, Copy, Clone)]
62pub(crate) enum CompatibleDist<'a> {
63    /// The distribution is already installed and can be used.
64    InstalledDist(&'a InstalledDist),
65    /// The distribution should be resolved and installed using a source distribution.
66    SourceDist {
67        /// The source distribution that should be used.
68        sdist: &'a RegistrySourceDist,
69        /// The prioritized distribution that the sdist came from.
70        prioritized: &'a PrioritizedDist,
71    },
72    /// The distribution should be resolved and installed using a wheel distribution.
73    CompatibleWheel {
74        /// The wheel that should be used.
75        wheel: &'a RegistryBuiltWheel,
76        /// The prioritized distribution that the wheel came from.
77        prioritized: &'a PrioritizedDist,
78    },
79    /// The distribution should be resolved using an incompatible wheel distribution, but
80    /// installed using a source distribution.
81    IncompatibleWheel {
82        /// The sdist to be used during installation.
83        sdist: &'a RegistrySourceDist,
84        /// The wheel to be used during resolution.
85        wheel: &'a RegistryBuiltWheel,
86        /// The prioritized distribution that the wheel and sdist came from.
87        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    /// Return whether a usable source distribution or a wheel matching [`RequiresPython`] exists.
114    ///
115    /// Wheel compatibility must be checked against the current Python range when a resolver fork
116    /// narrows the range used to construct the [`PrioritizedDist`].
117    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    /// Return the `requires-python` specifier for the distribution, if any.
127    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    // For installable distributions, return the prioritized distribution it was derived from.
137    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    /// Return the set of supported platforms for the distribution, in terms of their markers.
147    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    /// An incompatible wheel is available.
161    Wheel(IncompatibleWheel),
162    /// An incompatible source distribution is available.
163    Source(IncompatibleSource),
164    /// No distributions are available
165    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    /// The installed version of Python.
341    Installed,
342    /// The target version of Python; that is, the version of Python for which we are resolving
343    /// dependencies. This is typically the same as the installed version, but may be different
344    /// when specifying an alternate Python version for the resolution.
345    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    /// The wheel was published after the exclude newer time.
357    ExcludeNewer(Option<i64>),
358    /// The wheel tags do not match those of the target Python platform.
359    Tag(IncompatibleTag),
360    /// The required Python version is not a superset of the target Python version range.
361    RequiresPython(VersionSpecifiers, PythonRequirementKind),
362    /// The wheel was yanked.
363    Yanked(Yanked),
364    /// The use of binary wheels is disabled.
365    NoBinary,
366    /// Wheels are not available for the current platform.
367    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    /// The source distribution's filename does not confirm to PEP 625.
383    NotPep625Filename,
384}
385
386#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
387pub enum HashComparison {
388    /// The hash is present, but does not match the expected value.
389    Mismatched,
390    /// The hash is missing.
391    Missing,
392    /// The hash matches the expected value.
393    Matched,
394}
395
396impl PrioritizedDist {
397    /// Insert the given built distribution into the [`PrioritizedDist`].
398    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        // Track the hashes.
420        if !compatibility.is_excluded() {
421            self.0.hashes.extend(hashes);
422        }
423        // Track the highest-priority wheel. Variant wheels cannot provide metadata, so prefer
424        // ordinary wheels even when they cannot be installed (e.g., with `--no-binary`).
425        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    /// Insert the given source distribution into the [`PrioritizedDist`].
444    pub fn insert_source(
445        &mut self,
446        dist: RegistrySourceDist,
447        hashes: impl IntoIterator<Item = HashDigest>,
448        compatibility: SourceDistCompatibility,
449    ) {
450        // A usable source distribution provides coverage for all environments.
451        if compatibility.is_compatible() {
452            self.0.python_markers = MarkerTree::TRUE;
453            self.0.markers = [MarkerTree::TRUE; 2];
454        }
455        // Track the hashes.
456        if !compatibility.is_excluded() {
457            self.0.hashes.extend(hashes);
458        }
459        // Track the highest-priority source.
460        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    /// Return the highest-priority distribution for the package version, if any.
470    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            // If both are compatible, break ties based on the hash outcome. For example, prefer a
474            // source distribution with a matching hash over a wheel with a mismatched hash. When
475            // the outcomes are equivalent (e.g., both have a matching hash), prefer the wheel.
476            (
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            // Prefer the highest-priority, platform-compatible wheel.
493            (Some((wheel, WheelCompatibility::Compatible(..))), _) => {
494                Some(CompatibleDist::CompatibleWheel {
495                    wheel,
496                    prioritized: self,
497                })
498            }
499            // If we have a compatible source distribution and an incompatible wheel, return the
500            // wheel. We assume that all distributions have the same metadata for a given package
501            // version. If a compatible source distribution exists, we assume we can build it, but
502            // using the wheel is faster.
503            //
504            // (If the incompatible wheel should actually be ignored entirely, fall through to
505            // using the source distribution. Variant wheels are not supported yet.)
506            (
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            // Otherwise, if we have a source distribution, return it.
517            (.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => {
518                Some(CompatibleDist::SourceDist {
519                    sdist,
520                    prioritized: self,
521                })
522            }
523            _ => None,
524        }
525    }
526
527    /// Return the incompatibility for the best source distribution, if any.
528    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    /// Return the incompatibility for the best wheel, if any.
539    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    /// Return the hashes for each distribution.
550    pub(crate) fn hashes(&self) -> &[HashDigest] {
551        &self.0.hashes
552    }
553
554    /// Returns true if and only if this distribution does not contain any
555    /// source distributions or wheels.
556    pub(crate) fn is_empty(&self) -> bool {
557        self.0.source.is_none() && self.0.wheels.is_empty()
558    }
559
560    /// If this prioritized dist has at least one wheel, then this creates
561    /// a built distribution with the best wheel in this prioritized dist.
562    pub(crate) fn built_dist(&self) -> Option<RegistryBuiltDist> {
563        let best_wheel_index = self.0.best_wheel_index?;
564
565        // Remove any excluded wheels from the list of wheels, and adjust the wheel index to be
566        // relative to the filtered list.
567        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    /// If this prioritized dist has an sdist, then this creates a source
593    /// distribution.
594    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    /// Returns the "best" wheel in this prioritized distribution, if one
616    /// exists.
617    pub(crate) fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
618        self.0.best_wheel_index.map(|i| &self.0.wheels[i])
619    }
620
621    /// Returns an iterator of all wheels and the source distribution, if any.
622    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    /// Returns an iterator over all Python tags for the distribution.
636    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    /// Returns an iterator over all ABI tags for the distribution.
644    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    /// Returns the set of platform tags for the distribution that are ABI-compatible with the given
652    /// tags.
653    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    /// Return the [`ResolvedDistRef`] to use during resolution.
675    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    /// Return the [`ResolvedDistRef`] to use during installation.
691    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    /// Returns a [`RegistryBuiltWheel`] if the distribution includes a compatible or incompatible
707    /// wheel.
708    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    /// Return `true` if the distribution is compatible.
720    fn is_compatible(&self) -> bool {
721        matches!(self, Self::Compatible(_, _, _))
722    }
723
724    /// Return `true` if the distribution is excluded.
725    fn is_excluded(&self) -> bool {
726        matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_)))
727    }
728
729    /// Return `true` if the current compatibility is more compatible than another.
730    ///
731    /// Compatible wheels are always higher more compatible than incompatible wheels.
732    /// Compatible wheel ordering is determined by tag priority.
733    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    /// Return `true` if the distribution is compatible.
752    fn is_compatible(&self) -> bool {
753        matches!(self, Self::Compatible(_))
754    }
755
756    /// Return `true` if the distribution is excluded.
757    fn is_excluded(&self) -> bool {
758        matches!(
759            self,
760            Self::Incompatible(IncompatibleSource::ExcludeNewer(_))
761        )
762    }
763
764    /// Return the higher priority compatibility.
765    ///
766    /// Compatible source distributions are always higher priority than incompatible source distributions.
767    /// Compatible source distribution priority is arbitrary.
768    /// Incompatible source distribution priority selects a source distribution that was "closest" to being usable.
769    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                // Smaller timestamps are closer to the cut-off time
788                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                // Version specifiers cannot be reasonably compared
797                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                // Yanks with a reason are more helpful for errors
805                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                // Smaller timestamps are closer to the cut-off time
818                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                // Version specifiers cannot be reasonably compared
842                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                // Yanks with a reason are more helpful for errors
848                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
864/// Given a wheel filename, determine the markers covered by every configured libc baseline.
865///
866/// A wheel with multiple platform tags can remain eligible without covering every tagged platform.
867pub(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
876/// Infer coverage for each libc independently so separate wheels can satisfy each baseline.
877fn 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
900/// Infer the environments described by a set of platform tags.
901fn 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            // Windows
912            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            // macOS
953            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                // Extract the architecture from the end of the tag.
968                let mut arch_marker = MarkerTree::FALSE;
969                for arch in binary_format.platform_machine() {
970                    // A universal2 wheel can target an older macOS on Intel, but its ARM
971                    // slice requires macOS 11 (Darwin 20) or later.
972                    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            // Linux
995            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
1023/// Translate a macOS deployment target into the corresponding
1024/// [Darwin kernel release](<https://en.wikipedia.org/wiki/Darwin_(operating_system)#Darwin_20_onwards>).
1025///
1026/// macOS 10.16 is the compatibility spelling of macOS 11. macOS 26 uses Darwin 25;
1027/// starting with macOS 27, the major versions match.
1028fn 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
1041/// Given a wheel filename, determine the set of supported Python versions, in terms of their markers.
1042///
1043/// This is roughly the inverse of Python tag generation: given a tag, we want to infer the
1044/// supported Python version (rather than generating the supported tags from a given Python version).
1045fn implied_python_markers(filename: &WheelFilename) -> MarkerTree {
1046    let mut marker = MarkerTree::FALSE;
1047
1048    // If any ABI tag is a stable ABI (`abi3` or `abi3t`), the python tag represents a minimum
1049    // version rather than an exact version. For example, `cp39-abi3` means "compatible with
1050    // CPython 3.9+".
1051    let is_abi3 = filename.abi_tags().iter().any(|tag| tag.is_stable_abi());
1052
1053    for python_tag in filename.python_tags() {
1054        // First, construct the version marker based on the tag
1055        let mut tree = match python_tag {
1056            LanguageTag::None => {
1057                // No Python tag means no Python version requirement.
1058                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                // Generic Python tags support later minor versions within the same major version.
1082                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        // Then, add implementation markers for implementation-specific tags
1129        match python_tag {
1130            LanguageTag::None | LanguageTag::Python { .. } => {
1131                // No implementation marker needed
1132            }
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        // abi3 wheels: the python tag represents a minimum version, not an exact version.
1291        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        // abi3 wheel: cp39-abi3 means CPython >= 3.9, combined with platform markers.
1374        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}