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::{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<ExcludeNewerValue>,
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,
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 exclude_newer(&self) -> Option<&ExcludeNewerValue> {
193 match &self.inner {
194 VersionMapInner::Eager(_) => None,
195 VersionMapInner::Lazy(lazy) => lazy.exclude_newer.as_ref(),
196 }
197 }
198
199 pub(crate) fn iter(
206 &self,
207 range: &Ranges<Version>,
208 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
209 if let Some(version) = range.as_singleton() {
211 either::Either::Left(match self.inner {
212 VersionMapInner::Eager(ref eager) => {
213 either::Either::Left(eager.map.get_key_value(version).into_iter().map(
214 move |(version, dist)| {
215 let version_map_dist = VersionMapDistHandle {
216 inner: VersionMapDistHandleInner::Eager(dist),
217 };
218 (version, version_map_dist)
219 },
220 ))
221 }
222 VersionMapInner::Lazy(ref lazy) => {
223 either::Either::Right(lazy.map.get_key_value(version).into_iter().map(
224 move |(version, dist)| {
225 let version_map_dist = VersionMapDistHandle {
226 inner: VersionMapDistHandleInner::Lazy { lazy, dist },
227 };
228 (version, version_map_dist)
229 },
230 ))
231 }
232 })
233 } else {
234 either::Either::Right(match self.inner {
235 VersionMapInner::Eager(ref eager) => {
236 either::Either::Left(eager.map.range(BoundingRange::from(range)).map(
237 |(version, dist)| {
238 let version_map_dist = VersionMapDistHandle {
239 inner: VersionMapDistHandleInner::Eager(dist),
240 };
241 (version, version_map_dist)
242 },
243 ))
244 }
245 VersionMapInner::Lazy(ref lazy) => {
246 either::Either::Right(lazy.map.range(BoundingRange::from(range)).map(
247 |(version, dist)| {
248 let version_map_dist = VersionMapDistHandle {
249 inner: VersionMapDistHandleInner::Lazy { lazy, dist },
250 };
251 (version, version_map_dist)
252 },
253 ))
254 }
255 })
256 }
257 }
258
259 pub(crate) fn hashes(&self, version: &Version) -> Option<&[HashDigest]> {
261 match self.inner {
262 VersionMapInner::Eager(ref eager) => {
263 eager.map.get(version).map(PrioritizedDist::hashes)
264 }
265 VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(PrioritizedDist::hashes),
266 }
267 }
268
269 pub(crate) fn len(&self) -> usize {
274 match self.inner {
275 VersionMapInner::Eager(VersionMapEager { ref map, .. }) => map.len(),
276 VersionMapInner::Lazy(VersionMapLazy { ref map, .. }) => map.len(),
277 }
278 }
279
280 pub(crate) fn stable(&self) -> bool {
282 match self.inner {
283 VersionMapInner::Eager(ref map) => map.stable,
284 VersionMapInner::Lazy(ref map) => map.stable,
285 }
286 }
287
288 pub(crate) fn local(&self) -> bool {
290 match self.inner {
291 VersionMapInner::Eager(ref map) => map.local,
292 VersionMapInner::Lazy(ref map) => map.local,
293 }
294 }
295}
296
297impl From<FlatDistributions> for VersionMap {
298 fn from(flat_index: FlatDistributions) -> Self {
299 let stable = flat_index.iter().any(|(version, _)| version.is_stable());
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, stable, 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 pub(crate) fn prioritized_dist(&self) -> Option<&'a PrioritizedDist> {
335 match self.inner {
336 VersionMapDistHandleInner::Eager(dist) => Some(dist),
337 VersionMapDistHandleInner::Lazy { lazy, dist } => Some(lazy.get_lazy(dist)?),
338 }
339 }
340}
341
342#[derive(Debug)]
344#[expect(clippy::large_enum_variant)]
345enum VersionMapInner {
346 Eager(VersionMapEager),
351 Lazy(VersionMapLazy),
357}
358
359#[derive(Debug)]
361struct VersionMapEager {
362 map: BTreeMap<Version, PrioritizedDist>,
364 stable: bool,
366 local: bool,
368}
369
370#[derive(Debug)]
379struct VersionMapLazy {
380 map: BTreeMap<Version, LazyPrioritizedDist>,
382 stable: bool,
384 local: bool,
386 core_metadata: FxHashMap<Version, ResolutionMetadata>,
388 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
391 no_binary: bool,
393 no_build: bool,
395 index: IndexUrl,
397 tags: Option<Tags>,
400 exclude_newer: Option<ExcludeNewerValue>,
402 allowed_yanks: AllowedYanks,
404 hasher: HashStrategy,
406 requires_python: RequiresPython,
408}
409
410impl VersionMapLazy {
411 fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
413 let lazy_dist = self.map.get(version)?;
414 let priority_dist = self.get_lazy(lazy_dist)?;
415 Some(priority_dist)
416 }
417
418 fn get_lazy<'p>(&'p self, lazy_dist: &'p LazyPrioritizedDist) -> Option<&'p PrioritizedDist> {
424 match *lazy_dist {
425 LazyPrioritizedDist::OnlyFlat(ref dist) => Some(dist),
426 LazyPrioritizedDist::OnlySimple(ref dist) => self.get_simple(None, dist),
427 LazyPrioritizedDist::Both {
428 ref flat,
429 ref simple,
430 } => self.get_simple(Some(flat), simple),
431 }
432 }
433
434 fn get_simple<'p>(
439 &'p self,
440 init: Option<&'p PrioritizedDist>,
441 simple: &'p SimplePrioritizedDist,
442 ) -> Option<&'p PrioritizedDist> {
443 let get_or_init = || {
444 let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(
445 &self
446 .simple_metadata
447 .datum(simple.datum_index)
448 .expect("index to lazy dist is correct")
449 .files,
450 )
451 .expect("archived version files always deserializes");
452 let mut priority_dist = init.cloned().unwrap_or_default();
453 for (filename, file) in files.all() {
454 let (excluded, upload_time) = if let Some(exclude_newer) = &self.exclude_newer {
457 match file.upload_time_utc_ms.as_ref() {
458 Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
459 trace!(
460 "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({exclude_newer})",
461 file.filename
462 );
463 (true, Some(upload_time))
464 }
465 None => {
466 warn_user_once!(
467 "{} is missing an upload date, but user provided: {exclude_newer}",
468 file.filename,
469 );
470 (true, None)
471 }
472 _ => (false, None),
473 }
474 } else {
475 (false, None)
476 };
477
478 let yanked = file.yanked.as_deref();
480 let hashes = file.hashes.clone();
481 match filename {
482 DistFilename::WheelFilename(filename) => {
483 let compatibility = self.wheel_compatibility(
484 &filename,
485 &filename.name,
486 &filename.version,
487 hashes.as_slice(),
488 yanked,
489 excluded,
490 upload_time,
491 );
492 let dist = RegistryBuiltWheel {
493 filename,
494 file: Box::new(file),
495 index: self.index.clone(),
496 };
497 priority_dist.insert_built(dist, hashes, compatibility);
498 }
499 DistFilename::SourceDistFilename(filename) => {
500 let compatibility = self.source_dist_compatibility(
501 &filename.name,
502 &filename.version,
503 hashes.as_slice(),
504 yanked,
505 excluded,
506 upload_time,
507 );
508 let dist = RegistrySourceDist {
509 name: filename.name.clone(),
510 version: filename.version.clone(),
511 ext: filename.extension,
512 file: Box::new(file),
513 index: self.index.clone(),
514 wheels: vec![],
515 };
516 priority_dist.insert_source(dist, hashes, compatibility);
517 }
518 }
519 }
520 if priority_dist.is_empty() {
521 None
522 } else {
523 Some(priority_dist)
524 }
525 };
526 simple.dist.get_or_init(get_or_init).as_ref()
527 }
528
529 fn source_dist_compatibility(
530 &self,
531 name: &PackageName,
532 version: &Version,
533 hashes: &[HashDigest],
534 yanked: Option<&Yanked>,
535 excluded: bool,
536 upload_time: Option<i64>,
537 ) -> SourceDistCompatibility {
538 if self.no_build {
540 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
541 }
542
543 if excluded {
545 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
546 upload_time,
547 ));
548 }
549
550 if let Some(yanked) = yanked {
552 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
553 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
554 yanked.clone(),
555 ));
556 }
557 }
558
559 let hash_policy = self.hasher.get_package(name, version);
561 let required_hashes = hash_policy.digests();
562 let hash = if required_hashes.is_empty() {
563 HashComparison::Matched
564 } else {
565 if hashes.is_empty() {
566 HashComparison::Missing
567 } else if hash_policy.matches(hashes) {
568 HashComparison::Matched
569 } else {
570 HashComparison::Mismatched
571 }
572 };
573
574 SourceDistCompatibility::Compatible(hash)
575 }
576
577 fn wheel_compatibility(
578 &self,
579 filename: &WheelFilename,
580 name: &PackageName,
581 version: &Version,
582 hashes: &[HashDigest],
583 yanked: Option<&Yanked>,
584 excluded: bool,
585 upload_time: Option<i64>,
586 ) -> WheelCompatibility {
587 if self.no_binary {
589 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
590 }
591
592 if excluded {
594 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
595 }
596
597 if let Some(yanked) = yanked {
599 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
600 return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
601 }
602 }
603
604 let priority = if let Some(tags) = &self.tags {
606 match filename.compatibility(tags) {
607 TagCompatibility::Incompatible(tag) => {
608 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
609 }
610 TagCompatibility::Compatible(priority) => Some(priority),
611 }
612 } else {
613 if !self.requires_python.matches_wheel_tag(filename) {
616 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
617 IncompatibleTag::AbiPythonVersion,
618 ));
619 }
620 None
621 };
622
623 let hash_policy = self.hasher.get_package(name, version);
625 let required_hashes = hash_policy.digests();
626 let hash = if required_hashes.is_empty() {
627 HashComparison::Matched
628 } else {
629 if hashes.is_empty() {
630 HashComparison::Missing
631 } else if hash_policy.matches(hashes) {
632 HashComparison::Matched
633 } else {
634 HashComparison::Mismatched
635 }
636 };
637
638 let build_tag = filename.build_tag().cloned();
640
641 WheelCompatibility::Compatible(hash, priority, build_tag)
642 }
643}
644
645#[derive(Debug)]
648enum LazyPrioritizedDist {
649 OnlyFlat(PrioritizedDist),
652 OnlySimple(SimplePrioritizedDist),
655 Both {
658 flat: PrioritizedDist,
659 simple: SimplePrioritizedDist,
660 },
661}
662
663#[derive(Debug)]
665struct SimplePrioritizedDist {
666 datum_index: usize,
670 dist: OnceLock<Option<PrioritizedDist>>,
678}
679
680#[derive(Debug)]
682struct BoundingRange<'a> {
683 min: Bound<&'a Version>,
684 max: Bound<&'a Version>,
685}
686
687impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
688 fn from(value: &'a Ranges<Version>) -> Self {
689 let (min, max) = value
690 .bounding_range()
691 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
692 Self { min, max }
693 }
694}
695
696impl<'a> RangeBounds<Version> for BoundingRange<'a> {
697 fn start_bound(&self) -> Bound<&'a Version> {
698 self.min
699 }
700
701 fn end_bound(&self) -> Bound<&'a Version> {
702 self.max
703 }
704}