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;
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, ExcludeNewerTimestamp, 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#[allow(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<ExcludeNewerTimestamp>,
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 (true, Some(upload_time))
452 }
453 None => {
454 warn_user_once!(
455 "{} is missing an upload date, but user provided: {exclude_newer}",
456 file.filename,
457 );
458 (true, None)
459 }
460 _ => (false, None),
461 }
462 } else {
463 (false, None)
464 };
465
466 let yanked = file.yanked.as_deref();
468 let hashes = file.hashes.clone();
469 match filename {
470 DistFilename::WheelFilename(filename) => {
471 let compatibility = self.wheel_compatibility(
472 &filename,
473 &filename.name,
474 &filename.version,
475 hashes.as_slice(),
476 yanked,
477 excluded,
478 upload_time,
479 );
480 let dist = RegistryBuiltWheel {
481 filename,
482 file: Box::new(file),
483 index: self.index.clone(),
484 };
485 priority_dist.insert_built(dist, hashes, compatibility);
486 }
487 DistFilename::SourceDistFilename(filename) => {
488 let compatibility = self.source_dist_compatibility(
489 &filename.name,
490 &filename.version,
491 hashes.as_slice(),
492 yanked,
493 excluded,
494 upload_time,
495 );
496 let dist = RegistrySourceDist {
497 name: filename.name.clone(),
498 version: filename.version.clone(),
499 ext: filename.extension,
500 file: Box::new(file),
501 index: self.index.clone(),
502 wheels: vec![],
503 };
504 priority_dist.insert_source(dist, hashes, compatibility);
505 }
506 }
507 }
508 if priority_dist.is_empty() {
509 None
510 } else {
511 Some(priority_dist)
512 }
513 };
514 simple.dist.get_or_init(get_or_init).as_ref()
515 }
516
517 fn source_dist_compatibility(
518 &self,
519 name: &PackageName,
520 version: &Version,
521 hashes: &[HashDigest],
522 yanked: Option<&Yanked>,
523 excluded: bool,
524 upload_time: Option<i64>,
525 ) -> SourceDistCompatibility {
526 if self.no_build {
528 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
529 }
530
531 if excluded {
533 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
534 upload_time,
535 ));
536 }
537
538 if let Some(yanked) = yanked {
540 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
541 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
542 yanked.clone(),
543 ));
544 }
545 }
546
547 let hash_policy = self.hasher.get_package(name, version);
549 let required_hashes = hash_policy.digests();
550 let hash = if required_hashes.is_empty() {
551 HashComparison::Matched
552 } else {
553 if hashes.is_empty() {
554 HashComparison::Missing
555 } else if hashes.iter().any(|hash| required_hashes.contains(hash)) {
556 HashComparison::Matched
557 } else {
558 HashComparison::Mismatched
559 }
560 };
561
562 SourceDistCompatibility::Compatible(hash)
563 }
564
565 fn wheel_compatibility(
566 &self,
567 filename: &WheelFilename,
568 name: &PackageName,
569 version: &Version,
570 hashes: &[HashDigest],
571 yanked: Option<&Yanked>,
572 excluded: bool,
573 upload_time: Option<i64>,
574 ) -> WheelCompatibility {
575 if self.no_binary {
577 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
578 }
579
580 if excluded {
582 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
583 }
584
585 if let Some(yanked) = yanked {
587 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
588 return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
589 }
590 }
591
592 let priority = if let Some(tags) = &self.tags {
594 match filename.compatibility(tags) {
595 TagCompatibility::Incompatible(tag) => {
596 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
597 }
598 TagCompatibility::Compatible(priority) => Some(priority),
599 }
600 } else {
601 if !self.requires_python.matches_wheel_tag(filename) {
604 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
605 IncompatibleTag::AbiPythonVersion,
606 ));
607 }
608 None
609 };
610
611 let hash_policy = self.hasher.get_package(name, version);
613 let required_hashes = hash_policy.digests();
614 let hash = if required_hashes.is_empty() {
615 HashComparison::Matched
616 } else {
617 if hashes.is_empty() {
618 HashComparison::Missing
619 } else if hashes.iter().any(|hash| required_hashes.contains(hash)) {
620 HashComparison::Matched
621 } else {
622 HashComparison::Mismatched
623 }
624 };
625
626 let build_tag = filename.build_tag().cloned();
628
629 WheelCompatibility::Compatible(hash, priority, build_tag)
630 }
631}
632
633#[derive(Debug)]
636enum LazyPrioritizedDist {
637 OnlyFlat(PrioritizedDist),
640 OnlySimple(SimplePrioritizedDist),
643 Both {
646 flat: PrioritizedDist,
647 simple: SimplePrioritizedDist,
648 },
649}
650
651#[derive(Debug)]
653struct SimplePrioritizedDist {
654 datum_index: usize,
658 dist: OnceLock<Option<PrioritizedDist>>,
666}
667
668#[derive(Debug)]
670struct BoundingRange<'a> {
671 min: Bound<&'a Version>,
672 max: Bound<&'a Version>,
673}
674
675impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
676 fn from(value: &'a Ranges<Version>) -> Self {
677 let (min, max) = value
678 .bounding_range()
679 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
680 Self { min, max }
681 }
682}
683
684impl<'a> RangeBounds<Version> for BoundingRange<'a> {
685 fn start_bound(&self) -> Bound<&'a Version> {
686 self.min
687 }
688
689 fn end_bound(&self) -> Bound<&'a Version> {
690 self.max
691 }
692}