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