1use std::collections::BTreeMap;
2use std::collections::btree_map::Entry;
3
4use rustc_hash::FxHashMap;
5use tracing::instrument;
6
7use uv_client::{FlatIndexEntries, FlatIndexEntry};
8use uv_configuration::BuildOptions;
9use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
10use uv_distribution_types::{
11 File, HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
12 RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility, WheelCompatibility,
13};
14use uv_normalize::PackageName;
15use uv_pep440::Version;
16use uv_platform_tags::{TagCompatibility, Tags};
17use uv_pypi_types::HashDigest;
18use uv_types::HashStrategy;
19
20#[derive(Debug, Clone, Default)]
22pub struct FlatIndex {
23 index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
25 offline: bool,
28}
29
30impl FlatIndex {
31 #[instrument(skip_all)]
33 pub fn from_entries(entries: FlatIndexEntries) -> Self {
34 let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
35 let (entries, offline) = entries.into_parts();
36
37 for entry in entries {
38 index
39 .entry(entry.filename().name().clone())
40 .or_default()
41 .push(entry);
42 }
43
44 Self { index, offline }
45 }
46
47 pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
49 self.index.get(package_name).map(Vec::as_slice)
50 }
51
52 pub(crate) fn offline(&self) -> bool {
55 self.offline
56 }
57}
58
59#[derive(Debug, Clone, Default)]
62pub(crate) struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
63
64impl FlatDistributions {
65 #[instrument(skip_all)]
67 pub(crate) fn from_entries(
68 entries: impl IntoIterator<Item = FlatIndexEntry>,
69 tags: Option<&Tags>,
70 hasher: &HashStrategy,
71 build_options: &BuildOptions,
72 ) -> Self {
73 let mut distributions = Self::default();
74 for entry in entries {
75 let (filename, file, index) = entry.into_parts();
76 distributions.add_file(file, filename, tags, hasher, build_options, index);
77 }
78 distributions
79 }
80
81 pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
83 self.0.iter()
84 }
85
86 fn add_file(
88 &mut self,
89 file: File,
90 filename: DistFilename,
91 tags: Option<&Tags>,
92 hasher: &HashStrategy,
93 build_options: &BuildOptions,
94 index: IndexUrl,
95 ) {
96 match filename {
99 DistFilename::WheelFilename(filename) => {
100 let version = filename.version.clone();
101
102 let compatibility = Self::wheel_compatibility(
103 &filename,
104 file.hashes.as_slice(),
105 tags,
106 hasher,
107 build_options,
108 );
109 let dist = RegistryBuiltWheel {
110 filename,
111 file: Box::new(file),
112 index,
113 size_is_authoritative: false,
114 };
115 match self.0.entry(version) {
116 Entry::Occupied(mut entry) => {
117 entry.get_mut().insert_built(dist, vec![], compatibility);
118 }
119 Entry::Vacant(entry) => {
120 entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility));
121 }
122 }
123 }
124 DistFilename::SourceDistFilename(filename) => {
125 let compatibility = Self::source_dist_compatibility(
126 &filename,
127 file.hashes.as_slice(),
128 hasher,
129 build_options,
130 );
131 let dist = RegistrySourceDist {
132 name: filename.name.clone(),
133 version: filename.version.clone(),
134 ext: filename.extension,
135 file: Box::new(file),
136 index,
137 wheels: vec![],
138 size_is_authoritative: false,
139 };
140 match self.0.entry(filename.version) {
141 Entry::Occupied(mut entry) => {
142 entry.get_mut().insert_source(dist, vec![], compatibility);
143 }
144 Entry::Vacant(entry) => {
145 entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility));
146 }
147 }
148 }
149 }
150 }
151
152 fn source_dist_compatibility(
153 filename: &SourceDistFilename,
154 hashes: &[HashDigest],
155 hasher: &HashStrategy,
156 build_options: &BuildOptions,
157 ) -> SourceDistCompatibility {
158 if build_options.no_build_package(&filename.name) {
160 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
161 }
162
163 if !filename.extension.is_pep625_compliant() {
168 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
169 }
170
171 let hash_policy = hasher.get_package(&filename.name, &filename.version);
173 let hash = if hash_policy.requires_validation() {
174 if hashes.is_empty() {
175 HashComparison::Missing
176 } else if hash_policy.matches(hashes) {
177 HashComparison::Matched
178 } else {
179 HashComparison::Mismatched
180 }
181 } else {
182 HashComparison::Matched
183 };
184
185 SourceDistCompatibility::Compatible(hash)
186 }
187
188 fn wheel_compatibility(
189 filename: &WheelFilename,
190 hashes: &[HashDigest],
191 tags: Option<&Tags>,
192 hasher: &HashStrategy,
193 build_options: &BuildOptions,
194 ) -> WheelCompatibility {
195 if build_options.no_binary_package(&filename.name) {
197 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
198 }
199
200 let priority = match tags {
202 Some(tags) => match filename.compatibility(tags) {
203 TagCompatibility::Incompatible(tag) => {
204 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
205 }
206 TagCompatibility::Compatible(priority) => Some(priority),
207 },
208 None => None,
209 };
210
211 let hash_policy = hasher.get_package(&filename.name, &filename.version);
213 let hash = if hash_policy.requires_validation() {
214 if hashes.is_empty() {
215 HashComparison::Missing
216 } else if hash_policy.matches(hashes) {
217 HashComparison::Matched
218 } else {
219 HashComparison::Mismatched
220 }
221 } else {
222 HashComparison::Matched
223 };
224
225 let build_tag = filename.build_tag().cloned();
227
228 WheelCompatibility::Compatible(hash, priority, build_tag)
229 }
230}
231
232impl IntoIterator for FlatDistributions {
233 type Item = (Version, PrioritizedDist);
234 type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
235
236 fn into_iter(self) -> Self::IntoIter {
237 self.0.into_iter()
238 }
239}
240
241impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
242 fn from(distributions: FlatDistributions) -> Self {
243 distributions.0
244 }
245}