1use std::collections::BTreeMap;
2
3use rustc_hash::FxHashMap;
4use tracing::instrument;
5
6use uv_cache::Cache;
7use uv_client::{
8 FlatIndexClient, FlatIndexEntries, FlatIndexEntry, FlatIndexError, RegistryClient,
9};
10use uv_configuration::BuildOptions;
11use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
12use uv_distribution_types::{
13 File, Index, IndexEntryFilename, IndexLocations, IndexUrl, MinimumLibcVersion,
14 RegistryBuiltWheel, RegistrySourceDist,
15};
16use uv_normalize::PackageName;
17use uv_pep440::Version;
18use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
19use uv_pypi_types::HashDigest;
20use uv_types::HashStrategy;
21
22use crate::prioritized_distribution::{
23 HashComparison, IncompatibleSource, IncompatibleWheel, PrioritizedDist, RegistryVariantsJson,
24 SourceDistCompatibility, WheelCompatibility,
25};
26
27#[derive(Debug, Clone, Default)]
29pub struct FlatIndex {
30 index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
32 offline: bool,
35}
36
37impl FlatIndex {
38 pub async fn load(
40 client: &RegistryClient,
41 cache: &Cache,
42 index_locations: &IndexLocations,
43 ) -> Result<Self, FlatIndexError> {
44 let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
45 let entries = client
46 .fetch_all(index_locations.flat_indexes().map(Index::url))
47 .await?;
48 Ok(Self::from_entries(entries))
49 }
50
51 #[instrument(skip_all)]
53 fn from_entries(entries: FlatIndexEntries) -> Self {
54 let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
55 let (entries, offline) = entries.into_parts();
56
57 for entry in entries {
58 index
59 .entry(entry.filename().name().clone())
60 .or_default()
61 .push(entry);
62 }
63
64 Self { index, offline }
65 }
66
67 pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
69 self.index.get(package_name).map(Vec::as_slice)
70 }
71
72 pub(crate) fn offline(&self) -> bool {
75 self.offline
76 }
77}
78
79#[derive(Debug, Clone, Default)]
82pub struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
83
84impl FlatDistributions {
85 #[instrument(skip_all)]
87 pub(crate) fn from_entries(
88 entries: impl IntoIterator<Item = FlatIndexEntry>,
89 tags: Option<&Tags>,
90 hasher: &HashStrategy,
91 build_options: &BuildOptions,
92 minimum_libc_version: Option<MinimumLibcVersion>,
93 ) -> Self {
94 let mut distributions = Self::default();
95 for entry in entries {
96 let (filename, file, index) = entry.into_parts();
97 match filename {
98 IndexEntryFilename::DistFilename(filename) => distributions.add_file(
99 file,
100 filename,
101 tags,
102 hasher,
103 build_options,
104 index,
105 minimum_libc_version,
106 ),
107 IndexEntryFilename::VariantJson(filename) => {
108 distributions
109 .0
110 .entry(filename.version.clone())
111 .or_default()
112 .insert_variants_json(RegistryVariantsJson {
113 filename,
114 file,
115 index,
116 });
117 }
118 }
119 }
120 distributions.0.retain(|_, dist| !dist.is_empty());
122 distributions
123 }
124
125 pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
127 self.0.iter()
128 }
129
130 fn add_file(
132 &mut self,
133 file: File,
134 filename: DistFilename,
135 tags: Option<&Tags>,
136 hasher: &HashStrategy,
137 build_options: &BuildOptions,
138 index: IndexUrl,
139 minimum_libc_version: Option<MinimumLibcVersion>,
140 ) {
141 match filename {
144 DistFilename::WheelFilename(filename) => {
145 let version = filename.version.clone();
146
147 let compatibility = Self::wheel_compatibility(
148 &filename,
149 file.hashes.as_slice(),
150 tags,
151 hasher,
152 build_options,
153 );
154 let dist = RegistryBuiltWheel {
155 filename,
156 file: Box::new(file),
157 index,
158 size_is_authoritative: false,
159 };
160 self.0.entry(version).or_default().insert_built(
161 dist,
162 vec![],
163 compatibility,
164 minimum_libc_version,
165 );
166 }
167 DistFilename::SourceDistFilename(filename) => {
168 let compatibility = Self::source_dist_compatibility(
169 &filename,
170 file.hashes.as_slice(),
171 hasher,
172 build_options,
173 );
174 let dist = RegistrySourceDist {
175 name: filename.name.clone(),
176 version: filename.version.clone(),
177 ext: filename.extension,
178 file: Box::new(file),
179 index,
180 wheels: vec![],
181 size_is_authoritative: false,
182 };
183 self.0.entry(filename.version).or_default().insert_source(
184 dist,
185 vec![],
186 compatibility,
187 );
188 }
189 }
190 }
191
192 fn source_dist_compatibility(
193 filename: &SourceDistFilename,
194 hashes: &[HashDigest],
195 hasher: &HashStrategy,
196 build_options: &BuildOptions,
197 ) -> SourceDistCompatibility {
198 if build_options.no_build_package(&filename.name) {
200 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
201 }
202
203 if !filename.extension.is_pep625_compliant() {
208 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
209 }
210
211 let hash_policy = hasher.archive_policy_for_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 SourceDistCompatibility::Compatible(hash)
226 }
227
228 fn wheel_compatibility(
229 filename: &WheelFilename,
230 hashes: &[HashDigest],
231 tags: Option<&Tags>,
232 hasher: &HashStrategy,
233 build_options: &BuildOptions,
234 ) -> WheelCompatibility {
235 if filename.variant().is_some() {
237 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
238 IncompatibleTag::Variant,
239 ));
240 }
241 if build_options.no_binary_package(&filename.name) {
243 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
244 }
245
246 let priority = match tags {
248 Some(tags) => match filename.tag_compatibility(tags) {
249 TagCompatibility::Incompatible(tag) => {
250 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
251 }
252 TagCompatibility::Compatible(priority) => Some(priority),
253 },
254 None => None,
255 };
256
257 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
259 let hash = if hash_policy.requires_validation() {
260 if hashes.is_empty() {
261 HashComparison::Missing
262 } else if hash_policy.matches(hashes) {
263 HashComparison::Matched
264 } else {
265 HashComparison::Mismatched
266 }
267 } else {
268 HashComparison::Matched
269 };
270
271 let build_tag = filename.build_tag().cloned();
273
274 WheelCompatibility::Compatible(hash, priority, build_tag)
275 }
276}
277
278impl IntoIterator for FlatDistributions {
279 type Item = (Version, PrioritizedDist);
280 type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
281
282 fn into_iter(self) -> Self::IntoIter {
283 self.0.into_iter()
284 }
285}
286
287impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
288 fn from(distributions: FlatDistributions) -> Self {
289 distributions.0
290 }
291}
292
293impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
294 fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
295 Self(distributions)
296 }
297}