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#[derive(Debug)]
33pub struct VersionMap {
34 inner: VersionMapInner,
36}
37
38impl VersionMap {
39 #[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 for (datum_index, datum) in simple_metadata.iter().enumerate() {
69 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 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 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 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 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 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 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 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 pub(crate) fn iter(
199 &self,
200 range: &Ranges<Version>,
201 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
202 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 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 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 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 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
308pub(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 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 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#[derive(Debug)]
356#[expect(clippy::large_enum_variant)]
357enum VersionMapInner {
358 Eager(VersionMapEager),
363 Lazy(VersionMapLazy),
369}
370
371#[derive(Debug)]
373struct VersionMapEager {
374 map: BTreeMap<Version, PrioritizedDist>,
376 local: bool,
378}
379
380#[derive(Debug)]
382struct VersionMapLazyEntry {
383 version: Version,
384 dist: LazyPrioritizedDist,
385}
386
387#[derive(Debug)]
389struct VersionMapLazyIndex {
390 entries: Vec<VersionMapLazyEntry>,
391}
392
393impl VersionMapLazyIndex {
394 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#[derive(Debug)]
481struct VersionMapLazy {
482 package_name: PackageName,
484 map: VersionMapLazyIndex,
486 local: bool,
488 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
491 no_binary: bool,
493 no_build: bool,
495 index: IndexUrl,
497 tags: Option<Tags>,
500 included_version_cutoff: Option<Timestamp>,
503 available_version_cutoff: Option<Timestamp>,
505 allowed_yanks: AllowedYanks,
507 hasher: HashStrategy,
509 requires_python: RequiresPython,
511 minimum_libc_version: Option<MinimumLibcVersion>,
513}
514
515impl VersionMapLazy {
516 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 fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
532 self.get_lazy(&self.map.get(version)?.dist)
533 }
534
535 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 (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 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 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 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 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 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 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 if excluded {
763 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
764 upload_time,
765 ));
766 }
767
768 if self.no_build {
770 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
771 }
772
773 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 if !filename.extension.is_pep625_compliant() {
791 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
792 }
793
794 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 if excluded {
826 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
827 }
828
829 if self.no_binary {
831 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
832 }
833
834 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 if filename.variant().is_some() {
844 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
845 IncompatibleTag::Variant,
846 ));
847 }
848
849 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 if !self.requires_python.matches_wheel_tag(filename) {
861 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
862 IncompatibleTag::AbiPythonVersion,
863 ));
864 }
865 None
866 };
867
868 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 let build_tag = filename.build_tag().cloned();
885
886 WheelCompatibility::Compatible(hash, priority, build_tag)
887 }
888}
889
890#[derive(Debug)]
892struct LazyPrioritizedDist {
893 flat: Option<PrioritizedDist>,
895 simple: Option<SimplePrioritizedDist>,
897}
898
899#[derive(Debug)]
901struct SimplePrioritizedDist {
902 datum_index: usize,
906 dist: OnceLock<Option<PrioritizedDist>>,
914}
915
916#[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}