1use std::collections::Bound;
2use std::collections::btree_map::{BTreeMap, Entry};
3use std::ops::RangeBounds;
4use std::sync::OnceLock;
5
6use pubgrub::Ranges;
7use rustc_hash::FxHashMap;
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::{ExcludeNewer, ExcludeNewerValue, 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 exclude_newer: Option<&ExcludeNewer>,
55 flat_index: Option<FlatDistributions>,
56 build_options: &BuildOptions,
57 ) -> Self {
58 let mut stable = false;
59 let mut local = false;
60 let mut map = BTreeMap::new();
61 let mut core_metadata = FxHashMap::default();
62 for (datum_index, datum) in simple_metadata.iter().enumerate() {
66 let version = rkyv::deserialize::<Version, rkyv::rancor::Error>(&datum.version)
68 .expect("archived version always deserializes");
69
70 let core_metadatum =
72 rkyv::deserialize::<Option<ResolutionMetadata>, rkyv::rancor::Error>(
73 &datum.metadata,
74 )
75 .expect("archived metadata always deserializes");
76 if let Some(core_metadatum) = core_metadatum {
77 core_metadata.insert(version.clone(), core_metadatum);
78 }
79
80 stable |= version.is_stable();
81 local |= version.is_local();
82 map.insert(
83 version,
84 LazyPrioritizedDist::OnlySimple(SimplePrioritizedDist {
85 datum_index,
86 dist: OnceLock::new(),
87 }),
88 );
89 }
90 for (version, prioritized_dist) in flat_index.into_iter().flatten() {
93 stable |= version.is_stable();
94 match map.entry(version) {
95 Entry::Vacant(e) => {
96 e.insert(LazyPrioritizedDist::OnlyFlat(prioritized_dist));
97 }
98 Entry::Occupied(e) => match e.remove_entry() {
103 (version, LazyPrioritizedDist::OnlySimple(simple_dist)) => {
104 map.insert(
105 version,
106 LazyPrioritizedDist::Both {
107 flat: prioritized_dist,
108 simple: simple_dist,
109 },
110 );
111 }
112 _ => unreachable!(),
113 },
114 }
115 }
116 Self {
117 inner: VersionMapInner::Lazy(VersionMapLazy {
118 map,
119 stable,
120 local,
121 core_metadata,
122 simple_metadata,
123 no_binary: build_options.no_binary_package(package_name),
124 no_build: build_options.no_build_package(package_name),
125 index: index.clone(),
126 tags: tags.cloned(),
127 allowed_yanks: allowed_yanks.clone(),
128 hasher: hasher.clone(),
129 requires_python: requires_python.clone(),
130 exclude_newer: exclude_newer.and_then(|en| en.exclude_newer_package(package_name)),
131 }),
132 }
133 }
134
135 #[instrument(skip_all, fields(package_name))]
136 pub(crate) fn from_flat_metadata(
137 flat_metadata: Vec<FlatIndexEntry>,
138 tags: Option<&Tags>,
139 hasher: &HashStrategy,
140 build_options: &BuildOptions,
141 ) -> Self {
142 let mut stable = false;
143 let mut local = false;
144 let mut map = BTreeMap::new();
145
146 for (version, prioritized_dist) in
147 FlatDistributions::from_entries(flat_metadata, tags, hasher, build_options)
148 {
149 stable |= version.is_stable();
150 local |= version.is_local();
151 map.insert(version, prioritized_dist);
152 }
153
154 Self {
155 inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
156 }
157 }
158
159 pub fn get_metadata(&self, version: &Version) -> Option<&ResolutionMetadata> {
161 match self.inner {
162 VersionMapInner::Eager(_) => None,
163 VersionMapInner::Lazy(ref lazy) => lazy.core_metadata.get(version),
164 }
165 }
166
167 pub(crate) fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
169 match self.inner {
170 VersionMapInner::Eager(ref eager) => eager.map.get(version),
171 VersionMapInner::Lazy(ref lazy) => lazy.get(version),
172 }
173 }
174
175 pub(crate) fn versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
177 match &self.inner {
178 VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
179 VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.map.keys()),
180 }
181 }
182
183 pub(crate) fn index(&self) -> Option<&IndexUrl> {
185 match &self.inner {
186 VersionMapInner::Eager(_) => None,
187 VersionMapInner::Lazy(lazy) => Some(&lazy.index),
188 }
189 }
190
191 pub(crate) fn iter(
198 &self,
199 range: &Ranges<Version>,
200 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
201 if let Some(version) = range.as_singleton() {
203 either::Either::Left(match self.inner {
204 VersionMapInner::Eager(ref eager) => {
205 either::Either::Left(eager.map.get_key_value(version).into_iter().map(
206 move |(version, dist)| {
207 let version_map_dist = VersionMapDistHandle {
208 inner: VersionMapDistHandleInner::Eager(dist),
209 };
210 (version, version_map_dist)
211 },
212 ))
213 }
214 VersionMapInner::Lazy(ref lazy) => {
215 either::Either::Right(lazy.map.get_key_value(version).into_iter().map(
216 move |(version, dist)| {
217 let version_map_dist = VersionMapDistHandle {
218 inner: VersionMapDistHandleInner::Lazy { lazy, dist },
219 };
220 (version, version_map_dist)
221 },
222 ))
223 }
224 })
225 } else {
226 either::Either::Right(match self.inner {
227 VersionMapInner::Eager(ref eager) => {
228 either::Either::Left(eager.map.range(BoundingRange::from(range)).map(
229 |(version, dist)| {
230 let version_map_dist = VersionMapDistHandle {
231 inner: VersionMapDistHandleInner::Eager(dist),
232 };
233 (version, version_map_dist)
234 },
235 ))
236 }
237 VersionMapInner::Lazy(ref lazy) => {
238 either::Either::Right(lazy.map.range(BoundingRange::from(range)).map(
239 |(version, dist)| {
240 let version_map_dist = VersionMapDistHandle {
241 inner: VersionMapDistHandleInner::Lazy { lazy, dist },
242 };
243 (version, version_map_dist)
244 },
245 ))
246 }
247 })
248 }
249 }
250
251 pub(crate) fn hashes(&self, version: &Version) -> Option<&[HashDigest]> {
253 match self.inner {
254 VersionMapInner::Eager(ref eager) => {
255 eager.map.get(version).map(PrioritizedDist::hashes)
256 }
257 VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(PrioritizedDist::hashes),
258 }
259 }
260
261 pub(crate) fn len(&self) -> usize {
266 match self.inner {
267 VersionMapInner::Eager(VersionMapEager { ref map, .. }) => map.len(),
268 VersionMapInner::Lazy(VersionMapLazy { ref map, .. }) => map.len(),
269 }
270 }
271
272 pub(crate) fn stable(&self) -> bool {
274 match self.inner {
275 VersionMapInner::Eager(ref map) => map.stable,
276 VersionMapInner::Lazy(ref map) => map.stable,
277 }
278 }
279
280 pub(crate) fn local(&self) -> bool {
282 match self.inner {
283 VersionMapInner::Eager(ref map) => map.local,
284 VersionMapInner::Lazy(ref map) => map.local,
285 }
286 }
287}
288
289impl From<FlatDistributions> for VersionMap {
290 fn from(flat_index: FlatDistributions) -> Self {
291 let stable = flat_index.iter().any(|(version, _)| version.is_stable());
292 let local = flat_index.iter().any(|(version, _)| version.is_local());
293 let map = flat_index.into();
294 Self {
295 inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
296 }
297 }
298}
299
300pub(crate) struct VersionMapDistHandle<'a> {
313 inner: VersionMapDistHandleInner<'a>,
314}
315
316enum VersionMapDistHandleInner<'a> {
317 Eager(&'a PrioritizedDist),
318 Lazy {
319 lazy: &'a VersionMapLazy,
320 dist: &'a LazyPrioritizedDist,
321 },
322}
323
324impl<'a> VersionMapDistHandle<'a> {
325 pub(crate) fn prioritized_dist(&self) -> Option<&'a PrioritizedDist> {
327 match self.inner {
328 VersionMapDistHandleInner::Eager(dist) => Some(dist),
329 VersionMapDistHandleInner::Lazy { lazy, dist } => Some(lazy.get_lazy(dist)?),
330 }
331 }
332}
333
334#[derive(Debug)]
336#[expect(clippy::large_enum_variant)]
337enum VersionMapInner {
338 Eager(VersionMapEager),
343 Lazy(VersionMapLazy),
349}
350
351#[derive(Debug)]
353struct VersionMapEager {
354 map: BTreeMap<Version, PrioritizedDist>,
356 stable: bool,
358 local: bool,
360}
361
362#[derive(Debug)]
371struct VersionMapLazy {
372 map: BTreeMap<Version, LazyPrioritizedDist>,
374 stable: bool,
376 local: bool,
378 core_metadata: FxHashMap<Version, ResolutionMetadata>,
380 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
383 no_binary: bool,
385 no_build: bool,
387 index: IndexUrl,
389 tags: Option<Tags>,
392 exclude_newer: Option<ExcludeNewerValue>,
394 allowed_yanks: AllowedYanks,
396 hasher: HashStrategy,
398 requires_python: RequiresPython,
400}
401
402impl VersionMapLazy {
403 fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
405 let lazy_dist = self.map.get(version)?;
406 let priority_dist = self.get_lazy(lazy_dist)?;
407 Some(priority_dist)
408 }
409
410 fn get_lazy<'p>(&'p self, lazy_dist: &'p LazyPrioritizedDist) -> Option<&'p PrioritizedDist> {
416 match *lazy_dist {
417 LazyPrioritizedDist::OnlyFlat(ref dist) => Some(dist),
418 LazyPrioritizedDist::OnlySimple(ref dist) => self.get_simple(None, dist),
419 LazyPrioritizedDist::Both {
420 ref flat,
421 ref simple,
422 } => self.get_simple(Some(flat), simple),
423 }
424 }
425
426 fn get_simple<'p>(
431 &'p self,
432 init: Option<&'p PrioritizedDist>,
433 simple: &'p SimplePrioritizedDist,
434 ) -> Option<&'p PrioritizedDist> {
435 let get_or_init = || {
436 let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(
437 &self
438 .simple_metadata
439 .datum(simple.datum_index)
440 .expect("index to lazy dist is correct")
441 .files,
442 )
443 .expect("archived version files always deserializes");
444 let mut priority_dist = init.cloned().unwrap_or_default();
445 for (filename, file) in files.all() {
446 let (excluded, upload_time) = if let Some(exclude_newer) = &self.exclude_newer {
449 match file.upload_time_utc_ms.as_ref() {
450 Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
451 trace!(
452 "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({exclude_newer})",
453 file.filename
454 );
455 (true, Some(upload_time))
456 }
457 None => {
458 warn_user_once!(
459 "{} is missing an upload date, but user provided: {exclude_newer}",
460 file.filename,
461 );
462 (true, None)
463 }
464 _ => (false, None),
465 }
466 } else {
467 (false, None)
468 };
469
470 let yanked = file.yanked.as_deref();
472 let hashes = file.hashes.clone();
473 match filename {
474 DistFilename::WheelFilename(filename) => {
475 let compatibility = self.wheel_compatibility(
476 &filename,
477 &filename.name,
478 &filename.version,
479 hashes.as_slice(),
480 yanked,
481 excluded,
482 upload_time,
483 );
484 let dist = RegistryBuiltWheel {
485 filename,
486 file: Box::new(file),
487 index: self.index.clone(),
488 };
489 priority_dist.insert_built(dist, hashes, compatibility);
490 }
491 DistFilename::SourceDistFilename(filename) => {
492 let compatibility = self.source_dist_compatibility(
493 &filename.name,
494 &filename.version,
495 hashes.as_slice(),
496 yanked,
497 excluded,
498 upload_time,
499 );
500 let dist = RegistrySourceDist {
501 name: filename.name.clone(),
502 version: filename.version.clone(),
503 ext: filename.extension,
504 file: Box::new(file),
505 index: self.index.clone(),
506 wheels: vec![],
507 };
508 priority_dist.insert_source(dist, hashes, compatibility);
509 }
510 }
511 }
512 if priority_dist.is_empty() {
513 None
514 } else {
515 Some(priority_dist)
516 }
517 };
518 simple.dist.get_or_init(get_or_init).as_ref()
519 }
520
521 fn source_dist_compatibility(
522 &self,
523 name: &PackageName,
524 version: &Version,
525 hashes: &[HashDigest],
526 yanked: Option<&Yanked>,
527 excluded: bool,
528 upload_time: Option<i64>,
529 ) -> SourceDistCompatibility {
530 if self.no_build {
532 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
533 }
534
535 if excluded {
537 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
538 upload_time,
539 ));
540 }
541
542 if let Some(yanked) = yanked {
544 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
545 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
546 yanked.clone(),
547 ));
548 }
549 }
550
551 let hash_policy = self.hasher.get_package(name, version);
553 let required_hashes = hash_policy.digests();
554 let hash = if required_hashes.is_empty() {
555 HashComparison::Matched
556 } else {
557 if hashes.is_empty() {
558 HashComparison::Missing
559 } else if hashes.iter().any(|hash| required_hashes.contains(hash)) {
560 HashComparison::Matched
561 } else {
562 HashComparison::Mismatched
563 }
564 };
565
566 SourceDistCompatibility::Compatible(hash)
567 }
568
569 fn wheel_compatibility(
570 &self,
571 filename: &WheelFilename,
572 name: &PackageName,
573 version: &Version,
574 hashes: &[HashDigest],
575 yanked: Option<&Yanked>,
576 excluded: bool,
577 upload_time: Option<i64>,
578 ) -> WheelCompatibility {
579 if self.no_binary {
581 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
582 }
583
584 if excluded {
586 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
587 }
588
589 if let Some(yanked) = yanked {
591 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
592 return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
593 }
594 }
595
596 let priority = if let Some(tags) = &self.tags {
598 match filename.compatibility(tags) {
599 TagCompatibility::Incompatible(tag) => {
600 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
601 }
602 TagCompatibility::Compatible(priority) => Some(priority),
603 }
604 } else {
605 if !self.requires_python.matches_wheel_tag(filename) {
608 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
609 IncompatibleTag::AbiPythonVersion,
610 ));
611 }
612 None
613 };
614
615 let hash_policy = self.hasher.get_package(name, version);
617 let required_hashes = hash_policy.digests();
618 let hash = if required_hashes.is_empty() {
619 HashComparison::Matched
620 } else {
621 if hashes.is_empty() {
622 HashComparison::Missing
623 } else if hashes.iter().any(|hash| required_hashes.contains(hash)) {
624 HashComparison::Matched
625 } else {
626 HashComparison::Mismatched
627 }
628 };
629
630 let build_tag = filename.build_tag().cloned();
632
633 WheelCompatibility::Compatible(hash, priority, build_tag)
634 }
635}
636
637#[derive(Debug)]
640enum LazyPrioritizedDist {
641 OnlyFlat(PrioritizedDist),
644 OnlySimple(SimplePrioritizedDist),
647 Both {
650 flat: PrioritizedDist,
651 simple: SimplePrioritizedDist,
652 },
653}
654
655#[derive(Debug)]
657struct SimplePrioritizedDist {
658 datum_index: usize,
662 dist: OnceLock<Option<PrioritizedDist>>,
670}
671
672#[derive(Debug)]
674struct BoundingRange<'a> {
675 min: Bound<&'a Version>,
676 max: Bound<&'a Version>,
677}
678
679impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
680 fn from(value: &'a Ranges<Version>) -> Self {
681 let (min, max) = value
682 .bounding_range()
683 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
684 Self { min, max }
685 }
686}
687
688impl<'a> RangeBounds<Version> for BoundingRange<'a> {
689 fn start_bound(&self) -> Bound<&'a Version> {
690 self.min
691 }
692
693 fn end_bound(&self) -> Bound<&'a Version> {
694 self.max
695 }
696}