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