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, WheelFilename};
13use uv_distribution_types::{
14 HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
15 RegistryBuiltWheel, RegistrySourceDist, RequiresPython, SourceDistCompatibility,
16 WheelCompatibility,
17};
18use uv_normalize::PackageName;
19use uv_pep440::Version;
20use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
21use uv_pypi_types::{HashDigest, ResolutionMetadata, Yanked};
22use uv_types::HashStrategy;
23use uv_warnings::warn_user_once;
24
25use crate::flat_index::FlatDistributions;
26use crate::yanks::AllowedYanks;
27
28#[derive(Debug)]
30pub struct VersionMap {
31 inner: VersionMapInner,
33}
34
35impl VersionMap {
36 #[instrument(skip_all, fields(package_name))]
46 pub(crate) fn from_simple_metadata(
47 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
48 package_name: &PackageName,
49 index: IndexUrl,
50 tags: Option<Tags>,
51 requires_python: RequiresPython,
52 allowed_yanks: AllowedYanks,
53 hasher: HashStrategy,
54 included_version_cutoff: Option<Timestamp>,
55 available_version_cutoff: Option<Timestamp>,
56 flat_index: Option<FlatDistributions>,
57 build_options: &BuildOptions,
58 ) -> Self {
59 let mut stable = false;
60 let mut local = false;
61 let mut entries = Vec::with_capacity(simple_metadata.iter().size_hint().0);
62 for (datum_index, datum) in simple_metadata.iter().enumerate() {
66 let version = rkyv::deserialize::<Version, rkyv::rancor::Error>(&datum.version)
67 .expect("archived version always deserializes");
68
69 stable |= version.is_stable();
70 local |= version.is_local();
71 debug_assert!(
72 entries
73 .last()
74 .is_none_or(|entry: &VersionMapLazyEntry| entry.version < version),
75 "simple metadata versions must be sorted and unique"
76 );
77 entries.push(VersionMapLazyEntry {
78 version,
79 dist: LazyPrioritizedDist {
80 flat: None,
81 simple: Some(SimplePrioritizedDist {
82 datum_index,
83 dist: OnceLock::new(),
84 }),
85 },
86 });
87 }
88 let mut map = VersionMapLazyIndex { entries };
89 if let Some(flat_index) = flat_index {
92 stable |= flat_index.iter().any(|(version, _)| version.is_stable());
93 map = map.merge_flat(flat_index);
94 }
95 Self {
96 inner: VersionMapInner::Lazy(VersionMapLazy {
97 map,
98 stable,
99 local,
100 simple_metadata,
101 no_binary: build_options.no_binary_package(package_name),
102 no_build: build_options.no_build_package(package_name),
103 index,
104 tags,
105 allowed_yanks,
106 hasher,
107 requires_python,
108 included_version_cutoff,
109 available_version_cutoff,
110 }),
111 }
112 }
113
114 #[instrument(skip_all, fields(package_name))]
115 pub(crate) fn from_flat_metadata(
116 flat_metadata: Vec<FlatIndexEntry>,
117 tags: Option<&Tags>,
118 hasher: &HashStrategy,
119 build_options: &BuildOptions,
120 ) -> Self {
121 let mut stable = false;
122 let mut local = false;
123 let mut map = BTreeMap::new();
124
125 for (version, prioritized_dist) in
126 FlatDistributions::from_entries(flat_metadata, tags, hasher, build_options)
127 {
128 stable |= version.is_stable();
129 local |= version.is_local();
130 map.insert(version, prioritized_dist);
131 }
132
133 Self {
134 inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
135 }
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 hashes(&self, version: &Version) -> Option<&[HashDigest]> {
258 match self.inner {
259 VersionMapInner::Eager(ref eager) => {
260 eager.map.get(version).map(PrioritizedDist::hashes)
261 }
262 VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(PrioritizedDist::hashes),
263 }
264 }
265
266 pub(crate) fn len(&self) -> usize {
271 match self.inner {
272 VersionMapInner::Eager(VersionMapEager { ref map, .. }) => map.len(),
273 VersionMapInner::Lazy(VersionMapLazy { ref map, .. }) => map.len(),
274 }
275 }
276
277 pub(crate) fn stable(&self) -> bool {
279 match self.inner {
280 VersionMapInner::Eager(ref map) => map.stable,
281 VersionMapInner::Lazy(ref map) => map.stable,
282 }
283 }
284
285 pub(crate) fn local(&self) -> bool {
287 match self.inner {
288 VersionMapInner::Eager(ref map) => map.local,
289 VersionMapInner::Lazy(ref map) => map.local,
290 }
291 }
292}
293
294impl From<FlatDistributions> for VersionMap {
295 fn from(flat_index: FlatDistributions) -> Self {
296 let stable = flat_index.iter().any(|(version, _)| version.is_stable());
297 let local = flat_index.iter().any(|(version, _)| version.is_local());
298 let map = flat_index.into();
299 Self {
300 inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
301 }
302 }
303}
304
305pub(crate) struct VersionMapDistHandle<'a> {
318 inner: VersionMapDistHandleInner<'a>,
319}
320
321enum VersionMapDistHandleInner<'a> {
322 Eager(&'a PrioritizedDist),
323 Lazy {
324 lazy: &'a VersionMapLazy,
325 dist: &'a LazyPrioritizedDist,
326 },
327}
328
329impl<'a> VersionMapDistHandle<'a> {
330 pub(crate) fn prioritized_dist(&self) -> Option<&'a PrioritizedDist> {
332 match self.inner {
333 VersionMapDistHandleInner::Eager(dist) => Some(dist),
334 VersionMapDistHandleInner::Lazy { lazy, dist } => Some(lazy.get_lazy(dist)?),
335 }
336 }
337}
338
339#[derive(Debug)]
341#[expect(clippy::large_enum_variant)]
342enum VersionMapInner {
343 Eager(VersionMapEager),
348 Lazy(VersionMapLazy),
354}
355
356#[derive(Debug)]
358struct VersionMapEager {
359 map: BTreeMap<Version, PrioritizedDist>,
361 stable: bool,
363 local: bool,
365}
366
367#[derive(Debug)]
369struct VersionMapLazyEntry {
370 version: Version,
371 dist: LazyPrioritizedDist,
372}
373
374#[derive(Debug)]
376struct VersionMapLazyIndex {
377 entries: Vec<VersionMapLazyEntry>,
378}
379
380impl VersionMapLazyIndex {
381 fn merge_flat(self, flat_index: FlatDistributions) -> Self {
383 let flat_count = flat_index.iter().size_hint().0;
384 let mut merged = Vec::with_capacity(self.entries.len() + flat_count);
385 let mut simple = self.entries.into_iter().peekable();
386 let mut flat = flat_index.into_iter().peekable();
387 let flat_entry = |(version, dist)| VersionMapLazyEntry {
388 version,
389 dist: LazyPrioritizedDist {
390 flat: Some(dist),
391 simple: None,
392 },
393 };
394 while let (Some(simple_entry), Some((flat_version, _))) = (simple.peek(), flat.peek()) {
395 match simple_entry.version.cmp(flat_version) {
396 std::cmp::Ordering::Less => {
397 if let Some(entry) = simple.next() {
398 merged.push(entry);
399 }
400 }
401 std::cmp::Ordering::Greater => {
402 if let Some(entry) = flat.next() {
403 merged.push(flat_entry(entry));
404 }
405 }
406 std::cmp::Ordering::Equal => {
407 if let (Some(mut entry), Some((_, dist))) = (simple.next(), flat.next()) {
408 entry.dist.flat = Some(dist);
409 merged.push(entry);
410 }
411 }
412 }
413 }
414 merged.extend(simple);
415 merged.extend(flat.map(flat_entry));
416
417 Self { entries: merged }
418 }
419
420 fn get(&self, version: &Version) -> Option<&VersionMapLazyEntry> {
421 let index = self
422 .entries
423 .binary_search_by(|entry| entry.version.cmp(version))
424 .ok()?;
425 self.entries.get(index)
426 }
427
428 fn keys(&self) -> impl DoubleEndedIterator<Item = &Version> {
429 self.entries.iter().map(|entry| &entry.version)
430 }
431
432 fn range(&self, range: BoundingRange<'_>) -> &[VersionMapLazyEntry] {
433 let start = match range.min {
434 Bound::Included(version) => self
435 .entries
436 .partition_point(|entry| entry.version < *version),
437 Bound::Excluded(version) => self
438 .entries
439 .partition_point(|entry| entry.version <= *version),
440 Bound::Unbounded => 0,
441 };
442 let end = match range.max {
443 Bound::Included(version) => self
444 .entries
445 .partition_point(|entry| entry.version <= *version),
446 Bound::Excluded(version) => self
447 .entries
448 .partition_point(|entry| entry.version < *version),
449 Bound::Unbounded => self.entries.len(),
450 };
451 self.entries.get(start..end).unwrap_or_default()
452 }
453
454 fn len(&self) -> usize {
455 self.entries.len()
456 }
457}
458
459#[derive(Debug)]
468struct VersionMapLazy {
469 map: VersionMapLazyIndex,
471 stable: bool,
473 local: bool,
475 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
478 no_binary: bool,
480 no_build: bool,
482 index: IndexUrl,
484 tags: Option<Tags>,
487 included_version_cutoff: Option<Timestamp>,
490 available_version_cutoff: Option<Timestamp>,
492 allowed_yanks: AllowedYanks,
494 hasher: HashStrategy,
496 requires_python: RequiresPython,
498}
499
500impl VersionMapLazy {
501 fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
503 let archived = self
504 .map
505 .get(version)
506 .and_then(|entry| entry.dist.simple.as_ref())
507 .and_then(|simple| self.simple_metadata.datum(simple.datum_index))
508 .and_then(|datum| datum.metadata.as_ref())?;
509 Some(
510 rkyv::deserialize::<ResolutionMetadata, rkyv::rancor::Error>(archived)
511 .expect("archived metadata always deserializes"),
512 )
513 }
514
515 fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
517 self.get_lazy(&self.map.get(version)?.dist)
518 }
519
520 fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
523 self.map.entries.iter().filter_map(move |entry| {
524 let included = match (&entry.dist.flat, &entry.dist.simple) {
525 (Some(_), _) => true,
527 (None, Some(simple)) => self.any_file_included(simple),
528 (None, None) => false,
529 };
530 included.then_some(&entry.version)
531 })
532 }
533
534 fn any_file_included(&self, simple: &SimplePrioritizedDist) -> bool {
539 if self.included_version_cutoff.is_none() && self.available_version_cutoff.is_none() {
540 return true;
541 }
542 let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
543 return false;
544 };
545 let files = &datum.files;
546 files
547 .wheels
548 .iter()
549 .map(|wheel| &wheel.file)
550 .chain(files.source_dists.iter().map(|sdist| &sdist.file))
551 .any(|file| {
552 let upload_time = file.upload_time_utc_ms.as_ref().map(|t| t.to_native());
553 let excluded = if let Some(cutoff) = &self.included_version_cutoff {
554 upload_time.is_none_or(|t| t >= cutoff.as_millisecond())
555 } else if let Some(cutoff) = &self.available_version_cutoff {
556 upload_time.is_some_and(|t| t >= cutoff.as_millisecond())
557 } else {
558 false
559 };
560 !excluded
561 })
562 }
563
564 fn get_lazy<'p>(&'p self, lazy_dist: &'p LazyPrioritizedDist) -> Option<&'p PrioritizedDist> {
570 match (&lazy_dist.flat, &lazy_dist.simple) {
571 (Some(flat), Some(simple)) => self.get_simple(Some(flat), simple),
572 (Some(flat), None) => Some(flat),
573 (None, Some(simple)) => self.get_simple(None, simple),
574 (None, None) => None,
575 }
576 }
577
578 fn get_simple<'p>(
583 &'p self,
584 init: Option<&'p PrioritizedDist>,
585 simple: &'p SimplePrioritizedDist,
586 ) -> Option<&'p PrioritizedDist> {
587 let get_or_init = || {
588 let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(
589 &self
590 .simple_metadata
591 .datum(simple.datum_index)
592 .expect("index to lazy dist is correct")
593 .files,
594 )
595 .expect("archived version files always deserializes");
596 let mut priority_dist = init.cloned().unwrap_or_default();
597 for (filename, file) in files.all() {
598 let (excluded, upload_time) = if let Some(included_version_cutoff) =
601 &self.included_version_cutoff
602 {
603 match file.upload_time_utc_ms.as_ref() {
604 Some(&upload_time)
605 if upload_time >= included_version_cutoff.as_millisecond() =>
606 {
607 trace!(
608 "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({included_version_cutoff})",
609 file.filename
610 );
611 (true, Some(upload_time))
612 }
613 None => {
614 warn_user_once!(
615 "{} is missing an upload date, but user provided: {included_version_cutoff}",
616 file.filename,
617 );
618 (true, None)
619 }
620 _ => (false, None),
621 }
622 } else if let Some(available_version_cutoff) = &self.available_version_cutoff {
623 match file.upload_time_utc_ms.as_ref() {
624 Some(&upload_time)
625 if upload_time >= available_version_cutoff.as_millisecond() =>
626 {
627 trace!(
628 "Excluding `{}` (uploaded {upload_time}) due to available version cutoff ({available_version_cutoff})",
629 file.filename
630 );
631 (true, Some(upload_time))
632 }
633 _ => (false, None),
634 }
635 } else {
636 (false, None)
637 };
638
639 let yanked = file.yanked.as_deref();
641 let hashes = file.hashes.clone();
642 match filename {
643 DistFilename::WheelFilename(filename) => {
644 let compatibility = self.wheel_compatibility(
645 &filename,
646 &filename.name,
647 &filename.version,
648 hashes.as_slice(),
649 yanked,
650 excluded,
651 upload_time,
652 );
653 let dist = RegistryBuiltWheel {
654 filename,
655 file: Box::new(file),
656 index: self.index.clone(),
657 };
658 priority_dist.insert_built(dist, hashes, compatibility);
659 }
660 DistFilename::SourceDistFilename(filename) => {
661 let compatibility = self.source_dist_compatibility(
662 &filename.name,
663 &filename.version,
664 hashes.as_slice(),
665 yanked,
666 excluded,
667 upload_time,
668 );
669 let dist = RegistrySourceDist {
670 name: filename.name.clone(),
671 version: filename.version.clone(),
672 ext: filename.extension,
673 file: Box::new(file),
674 index: self.index.clone(),
675 wheels: vec![],
676 };
677 priority_dist.insert_source(dist, hashes, compatibility);
678 }
679 }
680 }
681 if priority_dist.is_empty() {
682 None
683 } else {
684 Some(priority_dist)
685 }
686 };
687 simple.dist.get_or_init(get_or_init).as_ref()
688 }
689
690 fn source_dist_compatibility(
691 &self,
692 name: &PackageName,
693 version: &Version,
694 hashes: &[HashDigest],
695 yanked: Option<&Yanked>,
696 excluded: bool,
697 upload_time: Option<i64>,
698 ) -> SourceDistCompatibility {
699 if self.no_build {
701 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
702 }
703
704 if excluded {
706 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
707 upload_time,
708 ));
709 }
710
711 if let Some(yanked) = yanked {
713 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
714 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
715 yanked.clone(),
716 ));
717 }
718 }
719
720 let hash_policy = self.hasher.get_package(name, version);
722 let required_hashes = hash_policy.digests();
723 let hash = if required_hashes.is_empty() {
724 HashComparison::Matched
725 } else {
726 if hashes.is_empty() {
727 HashComparison::Missing
728 } else if hash_policy.matches(hashes) {
729 HashComparison::Matched
730 } else {
731 HashComparison::Mismatched
732 }
733 };
734
735 SourceDistCompatibility::Compatible(hash)
736 }
737
738 fn wheel_compatibility(
739 &self,
740 filename: &WheelFilename,
741 name: &PackageName,
742 version: &Version,
743 hashes: &[HashDigest],
744 yanked: Option<&Yanked>,
745 excluded: bool,
746 upload_time: Option<i64>,
747 ) -> WheelCompatibility {
748 if self.no_binary {
750 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
751 }
752
753 if excluded {
755 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
756 }
757
758 if let Some(yanked) = yanked {
760 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
761 return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
762 }
763 }
764
765 let priority = if let Some(tags) = &self.tags {
767 match filename.compatibility(tags) {
768 TagCompatibility::Incompatible(tag) => {
769 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
770 }
771 TagCompatibility::Compatible(priority) => Some(priority),
772 }
773 } else {
774 if !self.requires_python.matches_wheel_tag(filename) {
777 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
778 IncompatibleTag::AbiPythonVersion,
779 ));
780 }
781 None
782 };
783
784 let hash_policy = self.hasher.get_package(name, version);
786 let required_hashes = hash_policy.digests();
787 let hash = if required_hashes.is_empty() {
788 HashComparison::Matched
789 } else {
790 if hashes.is_empty() {
791 HashComparison::Missing
792 } else if hash_policy.matches(hashes) {
793 HashComparison::Matched
794 } else {
795 HashComparison::Mismatched
796 }
797 };
798
799 let build_tag = filename.build_tag().cloned();
801
802 WheelCompatibility::Compatible(hash, priority, build_tag)
803 }
804}
805
806#[derive(Debug)]
808struct LazyPrioritizedDist {
809 flat: Option<PrioritizedDist>,
811 simple: Option<SimplePrioritizedDist>,
813}
814
815#[derive(Debug)]
817struct SimplePrioritizedDist {
818 datum_index: usize,
822 dist: OnceLock<Option<PrioritizedDist>>,
830}
831
832#[derive(Debug)]
834struct BoundingRange<'a> {
835 min: Bound<&'a Version>,
836 max: Bound<&'a Version>,
837}
838
839impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
840 fn from(value: &'a Ranges<Version>) -> Self {
841 let (min, max) = value
842 .bounding_range()
843 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
844 Self { min, max }
845 }
846}
847
848impl<'a> RangeBounds<Version> for BoundingRange<'a> {
849 fn start_bound(&self) -> Bound<&'a Version> {
850 self.min
851 }
852
853 fn end_bound(&self) -> Bound<&'a Version> {
854 self.max
855 }
856}