Skip to main content

uv_resolver/
version_map.rs

1use std::collections::BTreeMap;
2use std::collections::Bound;
3use std::ops::RangeBounds;
4use std::sync::OnceLock;
5
6use jiff::Timestamp;
7use pubgrub::Ranges;
8use tracing::{instrument, trace};
9
10use uv_client::{FlatIndexEntry, OwnedArchive, SimpleDetailMetadata, VersionFiles};
11use uv_configuration::BuildOptions;
12use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
13use uv_distribution_types::{
14    IndexEntryFilename, IndexUrl, MinimumLibcVersion, RegistryBuiltWheel, RegistrySourceDist,
15    RequiresPython,
16};
17use uv_normalize::PackageName;
18use uv_pep440::Version;
19use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
20use uv_pypi_types::{HashDigest, ResolutionMetadata, Yanked};
21use uv_types::HashStrategy;
22use uv_warnings::warn_user_once;
23
24use crate::flat_index::FlatDistributions;
25use crate::prioritized_distribution::{
26    HashComparison, IncompatibleSource, IncompatibleWheel, PrioritizedDist, RegistryVariantsJson,
27    SourceDistCompatibility, WheelCompatibility,
28};
29use crate::yanks::AllowedYanks;
30
31/// A map from versions to distributions.
32#[derive(Debug)]
33pub struct VersionMap {
34    /// The inner representation of the version map.
35    inner: VersionMapInner,
36}
37
38impl VersionMap {
39    /// Initialize a [`VersionMap`] from the given metadata.
40    ///
41    /// Note it is possible for files to have a different yank status per PEP 592 but in the official
42    /// PyPI warehouse this cannot happen.
43    ///
44    /// Here, we track if each file is yanked separately. If a release is partially yanked, the
45    /// unyanked distributions _can_ be used.
46    ///
47    /// PEP 592: <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
48    #[instrument(skip_all, fields(package_name))]
49    pub(crate) fn from_simple_metadata(
50        simple_metadata: OwnedArchive<SimpleDetailMetadata>,
51        package_name: &PackageName,
52        index: IndexUrl,
53        tags: Option<Tags>,
54        requires_python: RequiresPython,
55        allowed_yanks: AllowedYanks,
56        hasher: HashStrategy,
57        included_version_cutoff: Option<Timestamp>,
58        available_version_cutoff: Option<Timestamp>,
59        flat_index: Option<FlatDistributions>,
60        build_options: &BuildOptions,
61        minimum_libc_version: Option<MinimumLibcVersion>,
62    ) -> Self {
63        let mut local = false;
64        let mut entries = Vec::with_capacity(simple_metadata.iter().size_hint().0);
65        // Create stubs for each entry in simple metadata. The full conversion
66        // from a `VersionFiles` to a PrioritizedDist for each version
67        // isn't done until that specific version is requested.
68        for (datum_index, datum) in simple_metadata.iter().enumerate() {
69            // Index metadata alone does not provide an installable distribution.
70            if datum.files.wheels.is_empty() && datum.files.source_dists.is_empty() {
71                continue;
72            }
73            let version = rkyv::deserialize::<Version, rkyv::rancor::Error>(&datum.version)
74                .expect("archived version always deserializes");
75
76            local |= version.is_local();
77            debug_assert!(
78                entries
79                    .last()
80                    .is_none_or(|entry: &VersionMapLazyEntry| entry.version < version),
81                "simple metadata versions must be sorted and unique"
82            );
83            entries.push(VersionMapLazyEntry {
84                version,
85                dist: LazyPrioritizedDist {
86                    flat: None,
87                    simple: Some(SimplePrioritizedDist {
88                        datum_index,
89                        dist: OnceLock::new(),
90                    }),
91                },
92            });
93        }
94        let mut map = VersionMapLazyIndex { entries };
95        // If a set of flat distributions have been given, linearly merge the
96        // already sorted flat entries with the archive-ordered simple vector.
97        if let Some(flat_index) = flat_index {
98            map = map.merge_flat(flat_index);
99        }
100        Self {
101            inner: VersionMapInner::Lazy(VersionMapLazy {
102                package_name: package_name.clone(),
103                map,
104                local,
105                simple_metadata,
106                no_binary: build_options.no_binary_package(package_name),
107                no_build: build_options.no_build_package(package_name),
108                index,
109                tags,
110                allowed_yanks,
111                hasher,
112                requires_python,
113                included_version_cutoff,
114                available_version_cutoff,
115                minimum_libc_version,
116            }),
117        }
118    }
119
120    #[instrument(skip_all, fields(package_name))]
121    pub(crate) fn from_flat_metadata(
122        flat_metadata: Vec<FlatIndexEntry>,
123        tags: Option<&Tags>,
124        hasher: &HashStrategy,
125        build_options: &BuildOptions,
126        minimum_libc_version: Option<MinimumLibcVersion>,
127    ) -> Self {
128        FlatDistributions::from_entries(
129            flat_metadata,
130            tags,
131            hasher,
132            build_options,
133            minimum_libc_version,
134        )
135        .into()
136    }
137
138    /// Return the [`ResolutionMetadata`] for the given version, if any.
139    pub(crate) fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
140        match self.inner {
141            VersionMapInner::Eager(_) => None,
142            VersionMapInner::Lazy(ref lazy) => lazy.get_metadata(version),
143        }
144    }
145
146    /// Return the [`DistFile`] for the given version, if any.
147    pub(crate) fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
148        match self.inner {
149            VersionMapInner::Eager(ref eager) => eager.map.get(version),
150            VersionMapInner::Lazy(ref lazy) => lazy.get(version),
151        }
152    }
153
154    /// Return an iterator over the versions in this map.
155    pub(crate) fn versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
156        match &self.inner {
157            VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
158            VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.map.keys()),
159        }
160    }
161
162    /// Returns versions with at least one file not excluded by an upload-time cutoff, in ascending
163    /// order.
164    ///
165    /// Versions unavailable for other reasons remain present so callers receive a conservative
166    /// superset of selectable versions. These reasons include yanks, hashes, `requires-python`,
167    /// and `--no-binary` or `--no-build` policies. Flat-index versions bypass upload-time cutoffs
168    /// because their files do not carry upload times.
169    pub(crate) fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
170        match &self.inner {
171            VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
172            VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.included_versions()),
173        }
174    }
175
176    /// Return the index URL where this package came from.
177    pub(crate) fn index(&self) -> Option<&IndexUrl> {
178        match &self.inner {
179            VersionMapInner::Eager(_) => None,
180            VersionMapInner::Lazy(lazy) => Some(&lazy.index),
181        }
182    }
183
184    /// Return the included-version cutoff for this version map, if any.
185    pub(crate) fn included_version_cutoff(&self) -> Option<&Timestamp> {
186        match &self.inner {
187            VersionMapInner::Eager(_) => None,
188            VersionMapInner::Lazy(lazy) => lazy.included_version_cutoff.as_ref(),
189        }
190    }
191
192    /// Return an iterator over the versions and distributions.
193    ///
194    /// Note that the value returned in this iterator is a [`VersionMapDist`],
195    /// which can be used to lazily request a [`CompatibleDist`]. This is
196    /// useful in cases where one can skip materializing a full distribution
197    /// for each version.
198    pub(crate) fn iter(
199        &self,
200        range: &Ranges<Version>,
201    ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
202        // Performance optimization: If we only have a single version, return that version directly.
203        if let Some(version) = range.as_singleton() {
204            either::Either::Left(match self.inner {
205                VersionMapInner::Eager(ref eager) => {
206                    either::Either::Left(eager.map.get_key_value(version).into_iter().map(
207                        move |(version, dist)| {
208                            let version_map_dist = VersionMapDistHandle {
209                                inner: VersionMapDistHandleInner::Eager(dist),
210                            };
211                            (version, version_map_dist)
212                        },
213                    ))
214                }
215                VersionMapInner::Lazy(ref lazy) => {
216                    either::Either::Right(lazy.map.get(version).into_iter().map(move |entry| {
217                        let version_map_dist = VersionMapDistHandle {
218                            inner: VersionMapDistHandleInner::Lazy {
219                                lazy,
220                                dist: &entry.dist,
221                            },
222                        };
223                        (&entry.version, version_map_dist)
224                    }))
225                }
226            })
227        } else {
228            either::Either::Right(match self.inner {
229                VersionMapInner::Eager(ref eager) => {
230                    either::Either::Left(eager.map.range(BoundingRange::from(range)).map(
231                        |(version, dist)| {
232                            let version_map_dist = VersionMapDistHandle {
233                                inner: VersionMapDistHandleInner::Eager(dist),
234                            };
235                            (version, version_map_dist)
236                        },
237                    ))
238                }
239                VersionMapInner::Lazy(ref lazy) => {
240                    either::Either::Right(lazy.map.range(BoundingRange::from(range)).iter().map(
241                        |entry| {
242                            let version_map_dist = VersionMapDistHandle {
243                                inner: VersionMapDistHandleInner::Lazy {
244                                    lazy,
245                                    dist: &entry.dist,
246                                },
247                            };
248                            (&entry.version, version_map_dist)
249                        },
250                    ))
251                }
252            })
253        }
254    }
255
256    /// Return an iterator over the versions that can be considered for selection.
257    ///
258    /// Unlike [`Self::iter`], this skips lazy registry versions whose files are all excluded by
259    /// an upload-time cutoff without materializing their distributions. Files without an upload
260    /// time remain included so that materialization can emit the appropriate warning.
261    pub(crate) fn iter_included(
262        &self,
263        range: &Ranges<Version>,
264    ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
265        self.iter(range).filter(|(_, dist)| dist.is_included())
266    }
267
268    /// Return the [`Hashes`] for the given version, if any.
269    pub(crate) fn hashes(&self, version: &Version) -> Option<&[HashDigest]> {
270        match self.inner {
271            VersionMapInner::Eager(ref eager) => {
272                eager.map.get(version).map(PrioritizedDist::hashes)
273            }
274            VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(PrioritizedDist::hashes),
275        }
276    }
277
278    /// Returns the total number of distinct versions in this map.
279    ///
280    /// Note that this may include versions of distributions that are not
281    /// usable in the current environment.
282    pub(crate) fn len(&self) -> usize {
283        match self.inner {
284            VersionMapInner::Eager(VersionMapEager { ref map, .. }) => map.len(),
285            VersionMapInner::Lazy(VersionMapLazy { ref map, .. }) => map.len(),
286        }
287    }
288
289    /// Returns `true` if the map contains at least one local version (e.g., `2.6.0+cpu`).
290    pub(crate) fn local(&self) -> bool {
291        match self.inner {
292            VersionMapInner::Eager(ref map) => map.local,
293            VersionMapInner::Lazy(ref map) => map.local,
294        }
295    }
296}
297
298impl From<FlatDistributions> for VersionMap {
299    fn from(flat_index: FlatDistributions) -> Self {
300        let local = flat_index.iter().any(|(version, _)| version.is_local());
301        let map = flat_index.into();
302        Self {
303            inner: VersionMapInner::Eager(VersionMapEager { map, local }),
304        }
305    }
306}
307
308/// A lazily initialized distribution.
309///
310/// This permits access to a handle that can be turned into a resolvable
311/// distribution when desired. This is coupled with a `Version` in
312/// [`VersionMap::iter`] to permit iteration over all items in a map without
313/// necessarily constructing a distribution for every version if it isn't
314/// needed.
315///
316/// Note that because of laziness, not all such items can be turned into
317/// a valid distribution. For example, if in the process of building a
318/// distribution no compatible wheel or source distribution could be found,
319/// then building a `CompatibleDist` will fail.
320pub(crate) struct VersionMapDistHandle<'a> {
321    inner: VersionMapDistHandleInner<'a>,
322}
323
324enum VersionMapDistHandleInner<'a> {
325    Eager(&'a PrioritizedDist),
326    Lazy {
327        lazy: &'a VersionMapLazy,
328        dist: &'a LazyPrioritizedDist,
329    },
330}
331
332impl<'a> VersionMapDistHandle<'a> {
333    /// Returns whether this distribution can be considered for selection.
334    fn is_included(&self) -> bool {
335        match self.inner {
336            VersionMapDistHandleInner::Eager(_) => true,
337            VersionMapDistHandleInner::Lazy { lazy, dist } => match (&dist.flat, &dist.simple) {
338                (Some(_), _) => true,
339                (None, Some(simple)) => lazy.any_file_materializable(simple),
340                (None, None) => false,
341            },
342        }
343    }
344
345    /// Returns a prioritized distribution from this handle.
346    pub(crate) fn prioritized_dist(&self) -> Option<&'a PrioritizedDist> {
347        match self.inner {
348            VersionMapDistHandleInner::Eager(dist) => Some(dist),
349            VersionMapDistHandleInner::Lazy { lazy, dist } => Some(lazy.get_lazy(dist)?),
350        }
351    }
352}
353
354/// The kind of internal version map we have.
355#[derive(Debug)]
356#[expect(clippy::large_enum_variant)]
357enum VersionMapInner {
358    /// All distributions are fully materialized in memory.
359    ///
360    /// This usually happens when one needs a `VersionMap` from a
361    /// `FlatDistributions`.
362    Eager(VersionMapEager),
363    /// Some distributions might be fully materialized (i.e., by initializing
364    /// a `VersionMap` with a `FlatDistributions`), but some distributions
365    /// might still be in their "raw" `SimpleDetailMetadata` format. In this case, a
366    /// `PrioritizedDist` isn't actually created in memory until the
367    /// specific version has been requested.
368    Lazy(VersionMapLazy),
369}
370
371/// A map from versions to distributions that are fully materialized in memory.
372#[derive(Debug)]
373struct VersionMapEager {
374    /// A map from version to distribution.
375    map: BTreeMap<Version, PrioritizedDist>,
376    /// Whether the version map contains at least one local version.
377    local: bool,
378}
379
380/// An entry in the immutable lazy version index.
381#[derive(Debug)]
382struct VersionMapLazyEntry {
383    version: Version,
384    dist: LazyPrioritizedDist,
385}
386
387/// A compact immutable version index, ordered by native PEP 440 versions.
388#[derive(Debug)]
389struct VersionMapLazyIndex {
390    entries: Vec<VersionMapLazyEntry>,
391}
392
393impl VersionMapLazyIndex {
394    /// Merge the sorted flat index into the sorted simple entries in one pass.
395    fn merge_flat(self, flat_index: FlatDistributions) -> Self {
396        let flat_count = flat_index.iter().size_hint().0;
397        let mut merged = Vec::with_capacity(self.entries.len() + flat_count);
398        let mut simple = self.entries.into_iter().peekable();
399        let mut flat = flat_index.into_iter().peekable();
400        let flat_entry = |(version, dist)| VersionMapLazyEntry {
401            version,
402            dist: LazyPrioritizedDist {
403                flat: Some(dist),
404                simple: None,
405            },
406        };
407        while let (Some(simple_entry), Some((flat_version, _))) = (simple.peek(), flat.peek()) {
408            match simple_entry.version.cmp(flat_version) {
409                std::cmp::Ordering::Less => {
410                    if let Some(entry) = simple.next() {
411                        merged.push(entry);
412                    }
413                }
414                std::cmp::Ordering::Greater => {
415                    if let Some(entry) = flat.next() {
416                        merged.push(flat_entry(entry));
417                    }
418                }
419                std::cmp::Ordering::Equal => {
420                    if let (Some(mut entry), Some((_, dist))) = (simple.next(), flat.next()) {
421                        entry.dist.flat = Some(dist);
422                        merged.push(entry);
423                    }
424                }
425            }
426        }
427        merged.extend(simple);
428        merged.extend(flat.map(flat_entry));
429
430        Self { entries: merged }
431    }
432
433    fn get(&self, version: &Version) -> Option<&VersionMapLazyEntry> {
434        let index = self
435            .entries
436            .binary_search_by(|entry| entry.version.cmp(version))
437            .ok()?;
438        self.entries.get(index)
439    }
440
441    fn keys(&self) -> impl DoubleEndedIterator<Item = &Version> {
442        self.entries.iter().map(|entry| &entry.version)
443    }
444
445    fn range(&self, range: BoundingRange<'_>) -> &[VersionMapLazyEntry] {
446        let start = match range.min {
447            Bound::Included(version) => self
448                .entries
449                .partition_point(|entry| entry.version < *version),
450            Bound::Excluded(version) => self
451                .entries
452                .partition_point(|entry| entry.version <= *version),
453            Bound::Unbounded => 0,
454        };
455        let end = match range.max {
456            Bound::Included(version) => self
457                .entries
458                .partition_point(|entry| entry.version <= *version),
459            Bound::Excluded(version) => self
460                .entries
461                .partition_point(|entry| entry.version < *version),
462            Bound::Unbounded => self.entries.len(),
463        };
464        self.entries.get(start..end).unwrap_or_default()
465    }
466
467    fn len(&self) -> usize {
468        self.entries.len()
469    }
470}
471
472/// A map that lazily materializes some prioritized distributions upon access.
473///
474/// The idea here is that some packages have a lot of versions published, and
475/// needing to materialize a full `VersionMap` with all corresponding metadata
476/// for every version in memory is expensive. Since a `SimpleDetailMetadata` can be
477/// materialized with very little cost (via `rkyv` in the warm cached case),
478/// avoiding another conversion step into a fully filled out `VersionMap` can
479/// provide substantial savings in some cases.
480#[derive(Debug)]
481struct VersionMapLazy {
482    /// The normalized package name used to reconstruct cached wheel filenames.
483    package_name: PackageName,
484    /// An immutable archive-order index from version to possibly-initialized distribution.
485    map: VersionMapLazyIndex,
486    /// Whether the version map contains at least one local version.
487    local: bool,
488    /// The raw simple metadata from which `PrioritizedDist`s should
489    /// be constructed.
490    simple_metadata: OwnedArchive<SimpleDetailMetadata>,
491    /// When true, wheels aren't allowed.
492    no_binary: bool,
493    /// When true, source dists aren't allowed.
494    no_build: bool,
495    /// The URL of the index where this package came from.
496    index: IndexUrl,
497    /// The set of compatibility tags that determines whether a wheel is usable
498    /// in the current environment.
499    tags: Option<Tags>,
500    /// Files newer than this timestamp are considered excluded, i.e., that they cannot be selected by the
501    /// resolver.
502    included_version_cutoff: Option<Timestamp>,
503    /// Files newer than this timestamp are considered unavailable, i.e., that they do not exist.
504    available_version_cutoff: Option<Timestamp>,
505    /// Which yanked versions are allowed
506    allowed_yanks: AllowedYanks,
507    /// The hashes of allowed distributions.
508    hasher: HashStrategy,
509    /// The `requires-python` constraint for the resolution.
510    requires_python: RequiresPython,
511    /// The libc baselines required during universal resolution.
512    minimum_libc_version: Option<MinimumLibcVersion>,
513}
514
515impl VersionMapLazy {
516    /// Returns the registry-provided metadata for the given version, if it exists.
517    fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
518        let archived = self
519            .map
520            .get(version)
521            .and_then(|entry| entry.dist.simple.as_ref())
522            .and_then(|simple| self.simple_metadata.datum(simple.datum_index))
523            .and_then(|datum| datum.metadata.as_deref())?;
524        Some(
525            rkyv::deserialize::<ResolutionMetadata, rkyv::rancor::Error>(archived)
526                .expect("archived metadata always deserializes"),
527        )
528    }
529
530    /// Returns the distribution for the given version, if it exists.
531    fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
532        self.get_lazy(&self.map.get(version)?.dist)
533    }
534
535    /// Returns an iterator over the versions with at least one file within the exclude-newer
536    /// cutoffs, without materializing the distributions.
537    fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
538        self.map.entries.iter().filter_map(move |entry| {
539            let included = match (&entry.dist.flat, &entry.dist.simple) {
540                // Flat index files have no upload times and bypass the cutoffs.
541                (Some(_), _) => true,
542                (None, Some(simple)) => self.any_file_included(simple),
543                (None, None) => false,
544            };
545            included.then_some(&entry.version)
546        })
547    }
548
549    /// Returns whether at least one file keeps this version inside the candidate universe.
550    ///
551    /// This mirrors the per-file cutoff handling in [`Self::get_simple`] without materializing the
552    /// distribution, including the two cutoff modes' distinct handling of missing upload times.
553    fn any_file_included(&self, simple: &SimplePrioritizedDist) -> bool {
554        if self.included_version_cutoff.is_none() && self.available_version_cutoff.is_none() {
555            return true;
556        }
557        let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
558            return false;
559        };
560        let files = &datum.files;
561        files
562            .wheels
563            .iter()
564            .chain(files.source_dists.iter())
565            .any(|file| {
566                let upload_time = file.upload_time_utc_ms();
567                let excluded = if let Some(cutoff) = &self.included_version_cutoff {
568                    upload_time.is_none_or(|t| t >= cutoff.as_millisecond())
569                } else if let Some(cutoff) = &self.available_version_cutoff {
570                    upload_time.is_some_and(|t| t >= cutoff.as_millisecond())
571                } else {
572                    false
573                };
574                !excluded
575            })
576    }
577
578    /// Returns whether a version should be materialized during candidate selection.
579    ///
580    /// Missing upload times are retained here, even for `included_version_cutoff`, since
581    /// materializing them is what emits the corresponding `exclude-newer` warning.
582    fn any_file_materializable(&self, simple: &SimplePrioritizedDist) -> bool {
583        let Some(cutoff) = self
584            .included_version_cutoff
585            .as_ref()
586            .or(self.available_version_cutoff.as_ref())
587        else {
588            return true;
589        };
590        let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
591            return false;
592        };
593        datum
594            .files
595            .wheels
596            .iter()
597            .chain(datum.files.source_dists.iter())
598            .any(|file| {
599                file.upload_time_utc_ms()
600                    .is_none_or(|upload_time| upload_time < cutoff.as_millisecond())
601            })
602    }
603
604    /// Given a reference to a possibly-initialized distribution that is in
605    /// this lazy map, return the corresponding distribution.
606    ///
607    /// When both a flat and simple distribution are present internally, they
608    /// are merged automatically.
609    fn get_lazy<'p>(&'p self, lazy_dist: &'p LazyPrioritizedDist) -> Option<&'p PrioritizedDist> {
610        match (&lazy_dist.flat, &lazy_dist.simple) {
611            (Some(flat), Some(simple)) => self.get_simple(Some(flat), simple),
612            (Some(flat), None) => Some(flat),
613            (None, Some(simple)) => self.get_simple(None, simple),
614            (None, None) => None,
615        }
616    }
617
618    /// Given an optional starting point, return the final form of the
619    /// given simple distribution. If it wasn't initialized yet, then this
620    /// initializes it. If the distribution would otherwise be empty, this
621    /// returns `None`.
622    fn get_simple<'p>(
623        &'p self,
624        init: Option<&'p PrioritizedDist>,
625        simple: &'p SimplePrioritizedDist,
626    ) -> Option<&'p PrioritizedDist> {
627        let get_or_init = || {
628            let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(
629                &self
630                    .simple_metadata
631                    .datum(simple.datum_index)
632                    .expect("index to lazy dist is correct")
633                    .files,
634            )
635            .expect("archived version files always deserializes");
636            let mut priority_dist = init.cloned().unwrap_or_default();
637            for (filename, file) in files.all_entries(&self.package_name) {
638                let filename = match filename {
639                    IndexEntryFilename::DistFilename(filename) => filename,
640                    IndexEntryFilename::VariantJson(filename) => {
641                        if !priority_dist
642                            .variants_json()
643                            .iter()
644                            .any(|variants_json| variants_json.index == self.index)
645                        {
646                            priority_dist.insert_variants_json(RegistryVariantsJson {
647                                filename,
648                                file,
649                                index: self.index.clone(),
650                            });
651                        }
652                        continue;
653                    }
654                };
655                // Support resolving as if it were an earlier timestamp, at least as long files have
656                // upload time information.
657                let (excluded, upload_time) = if let Some(included_version_cutoff) =
658                    &self.included_version_cutoff
659                {
660                    match file.upload_time_utc_ms.as_ref() {
661                        Some(&upload_time)
662                            if upload_time >= included_version_cutoff.as_millisecond() =>
663                        {
664                            trace!(
665                                "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({included_version_cutoff})",
666                                file.filename
667                            );
668                            (true, Some(upload_time))
669                        }
670                        None => {
671                            warn_user_once!(
672                                "`{}` is missing an upload date, but user provided: {included_version_cutoff}",
673                                file.filename,
674                            );
675                            (true, None)
676                        }
677                        _ => (false, None),
678                    }
679                } else if let Some(available_version_cutoff) = &self.available_version_cutoff {
680                    match file.upload_time_utc_ms.as_ref() {
681                        Some(&upload_time)
682                            if upload_time >= available_version_cutoff.as_millisecond() =>
683                        {
684                            trace!(
685                                "Excluding `{}` (uploaded {upload_time}) due to available version cutoff ({available_version_cutoff})",
686                                file.filename
687                            );
688                            (true, Some(upload_time))
689                        }
690                        _ => (false, None),
691                    }
692                } else {
693                    (false, None)
694                };
695
696                // Prioritize amongst all available files.
697                let yanked = file.yanked.as_deref();
698                let hashes = file.hashes.clone();
699                match filename {
700                    DistFilename::WheelFilename(filename) => {
701                        let compatibility = self.wheel_compatibility(
702                            &filename,
703                            &filename.name,
704                            &filename.version,
705                            hashes.as_slice(),
706                            yanked,
707                            excluded,
708                            upload_time,
709                        );
710                        let dist = RegistryBuiltWheel {
711                            filename,
712                            file: Box::new(file),
713                            index: self.index.clone(),
714                            size_is_authoritative: false,
715                        };
716                        priority_dist.insert_built(
717                            dist,
718                            hashes,
719                            compatibility,
720                            self.minimum_libc_version,
721                        );
722                    }
723                    DistFilename::SourceDistFilename(filename) => {
724                        let compatibility = self.source_dist_compatibility(
725                            &filename,
726                            hashes.as_slice(),
727                            yanked,
728                            excluded,
729                            upload_time,
730                        );
731                        let dist = RegistrySourceDist {
732                            name: filename.name.clone(),
733                            version: filename.version.clone(),
734                            ext: filename.extension,
735                            file: Box::new(file),
736                            index: self.index.clone(),
737                            wheels: vec![],
738                            size_is_authoritative: false,
739                        };
740                        priority_dist.insert_source(dist, hashes, compatibility);
741                    }
742                }
743            }
744            if priority_dist.is_empty() {
745                None
746            } else {
747                Some(priority_dist)
748            }
749        };
750        simple.dist.get_or_init(get_or_init).as_ref()
751    }
752
753    fn source_dist_compatibility(
754        &self,
755        filename: &SourceDistFilename,
756        hashes: &[HashDigest],
757        yanked: Option<&Yanked>,
758        excluded: bool,
759        upload_time: Option<i64>,
760    ) -> SourceDistCompatibility {
761        // Check if after upload time cutoff
762        if excluded {
763            return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
764                upload_time,
765            ));
766        }
767
768        // Check if builds are disabled
769        if self.no_build {
770            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
771        }
772
773        // Check if yanked
774        if let Some(yanked) = yanked {
775            if yanked.is_yanked()
776                && !self
777                    .allowed_yanks
778                    .contains(&filename.name, &filename.version)
779            {
780                return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
781                    yanked.clone(),
782                ));
783            }
784        }
785
786        // Check if the filename is PEP 625-compliant.
787        // TODO: Strengthen this check more; right now we allow `.zip`
788        // (which is not compliant) and we don't strictly
789        // enforce the formatting rules for the name or version.
790        if !filename.extension.is_pep625_compliant() {
791            return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
792        }
793
794        // Check if hashes line up. If hashes aren't required, they're considered matching.
795        let hash_policy = self
796            .hasher
797            .archive_policy_for_package(&filename.name, &filename.version);
798        let required_hashes = hash_policy.digests();
799        let hash = if required_hashes.is_empty() {
800            HashComparison::Matched
801        } else {
802            if hashes.is_empty() {
803                HashComparison::Missing
804            } else if hash_policy.matches(hashes) {
805                HashComparison::Matched
806            } else {
807                HashComparison::Mismatched
808            }
809        };
810
811        SourceDistCompatibility::Compatible(hash)
812    }
813
814    fn wheel_compatibility(
815        &self,
816        filename: &WheelFilename,
817        name: &PackageName,
818        version: &Version,
819        hashes: &[HashDigest],
820        yanked: Option<&Yanked>,
821        excluded: bool,
822        upload_time: Option<i64>,
823    ) -> WheelCompatibility {
824        // Check if after upload time cutoff
825        if excluded {
826            return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
827        }
828
829        // Check if binaries are disabled
830        if self.no_binary {
831            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
832        }
833
834        // Check if yanked
835        if let Some(yanked) = yanked {
836            if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
837                return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
838            }
839        }
840
841        // Variant wheels cannot be selected, including during universal resolution.
842        // TODO(konsti): Support wheel variants during resolution.
843        if filename.variant().is_some() {
844            return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
845                IncompatibleTag::Variant,
846            ));
847        }
848
849        // Determine a compatibility for the wheel based on tags.
850        let priority = if let Some(tags) = &self.tags {
851            match filename.tag_compatibility(tags) {
852                TagCompatibility::Incompatible(tag) => {
853                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
854                }
855                TagCompatibility::Compatible(priority) => Some(priority),
856            }
857        } else {
858            // Check if the wheel is compatible with the `requires-python` (i.e., the Python
859            // ABI tag is not less than the `requires-python` minimum version).
860            if !self.requires_python.matches_wheel_tag(filename) {
861                return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
862                    IncompatibleTag::AbiPythonVersion,
863                ));
864            }
865            None
866        };
867
868        // Check if hashes line up. If hashes aren't required, they're considered matching.
869        let hash_policy = self.hasher.archive_policy_for_package(name, version);
870        let required_hashes = hash_policy.digests();
871        let hash = if required_hashes.is_empty() {
872            HashComparison::Matched
873        } else {
874            if hashes.is_empty() {
875                HashComparison::Missing
876            } else if hash_policy.matches(hashes) {
877                HashComparison::Matched
878            } else {
879                HashComparison::Mismatched
880            }
881        };
882
883        // Break ties with the build tag.
884        let build_tag = filename.build_tag().cloned();
885
886        WheelCompatibility::Compatible(hash, priority, build_tag)
887    }
888}
889
890/// Represents a possibly initialized [`PrioritizedDist`] for a package version.
891#[derive(Debug)]
892struct LazyPrioritizedDist {
893    /// An eagerly constructed distribution from [`FlatDistributions`], if present.
894    flat: Option<PrioritizedDist>,
895    /// A lazy index into [`SimpleDetailMetadata`], if present.
896    simple: Option<SimplePrioritizedDist>,
897}
898
899/// Represents a lazily initialized `PrioritizedDist`.
900#[derive(Debug)]
901struct SimplePrioritizedDist {
902    /// An offset into `SimpleDetailMetadata` corresponding to a `SimpleMetadatum`.
903    /// This provides access to a `VersionFiles` that is used to construct a
904    /// `PrioritizedDist`.
905    datum_index: usize,
906    /// A lazily initialized distribution.
907    ///
908    /// Note that the `Option` does not represent the initialization state.
909    /// The `Option` can be `None` even after initialization, for example,
910    /// if initialization could not find any usable files from which to
911    /// construct a distribution. (One easy way to effect this, at the time
912    /// of writing, is to use `--exclude-newer 1900-01-01`.)
913    dist: OnceLock<Option<PrioritizedDist>>,
914}
915
916/// A range that can be used to iterate over a subset of a [`BTreeMap`].
917#[derive(Debug)]
918struct BoundingRange<'a> {
919    min: Bound<&'a Version>,
920    max: Bound<&'a Version>,
921}
922
923impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
924    fn from(value: &'a Ranges<Version>) -> Self {
925        let (min, max) = value
926            .bounding_range()
927            .unwrap_or((Bound::Unbounded, Bound::Unbounded));
928        Self { min, max }
929    }
930}
931
932impl<'a> RangeBounds<Version> for BoundingRange<'a> {
933    fn start_bound(&self) -> Bound<&'a Version> {
934        self.min
935    }
936
937    fn end_bound(&self) -> Bound<&'a Version> {
938        self.max
939    }
940}