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