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_preview::PreviewFeature;
20use uv_pypi_types::HashDigest;
21use uv_types::HashStrategy;
22
23use crate::prioritized_distribution::{
24 HashComparison, IncompatibleSource, IncompatibleWheel, PrioritizedDist, RegistryVariantsJson,
25 SourceDistCompatibility, WheelCompatibility,
26};
27
28#[derive(Debug, Clone, Default)]
30pub struct FlatIndex {
31 index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
33 offline: bool,
36}
37
38impl FlatIndex {
39 pub async fn load(
41 client: &RegistryClient,
42 cache: &Cache,
43 index_locations: &IndexLocations,
44 ) -> Result<Self, FlatIndexError> {
45 let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
46 let entries = client
47 .fetch_all(index_locations.flat_indexes().map(Index::url))
48 .await?;
49 Ok(Self::from_entries(entries))
50 }
51
52 #[instrument(skip_all)]
54 fn from_entries(entries: FlatIndexEntries) -> Self {
55 let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
56 let (entries, offline) = entries.into_parts();
57
58 for entry in entries {
59 index
60 .entry(entry.filename().name().clone())
61 .or_default()
62 .push(entry);
63 }
64
65 Self { index, offline }
66 }
67
68 pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
70 self.index.get(package_name).map(Vec::as_slice)
71 }
72
73 pub(crate) fn offline(&self) -> bool {
76 self.offline
77 }
78}
79
80#[derive(Debug, Clone, Default)]
83pub struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
84
85impl FlatDistributions {
86 #[instrument(skip_all)]
88 pub(crate) fn from_entries(
89 entries: impl IntoIterator<Item = FlatIndexEntry>,
90 tags: Option<&Tags>,
91 hasher: &HashStrategy,
92 build_options: &BuildOptions,
93 minimum_libc_version: Option<MinimumLibcVersion>,
94 ) -> Self {
95 let mut distributions = Self::default();
96 for entry in entries {
97 let (filename, file, index) = entry.into_parts();
98 match filename {
99 IndexEntryFilename::DistFilename(filename) => distributions.add_file(
100 file,
101 filename,
102 tags,
103 hasher,
104 build_options,
105 index,
106 minimum_libc_version,
107 ),
108 IndexEntryFilename::VariantJson(filename) => {
109 distributions
110 .0
111 .entry(filename.version.clone())
112 .or_default()
113 .insert_variants_json(RegistryVariantsJson {
114 filename,
115 file,
116 index,
117 });
118 }
119 }
120 }
121 distributions.0.retain(|_, dist| !dist.is_empty());
123 distributions
124 }
125
126 pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
128 self.0.iter()
129 }
130
131 fn add_file(
133 &mut self,
134 file: File,
135 filename: DistFilename,
136 tags: Option<&Tags>,
137 hasher: &HashStrategy,
138 build_options: &BuildOptions,
139 index: IndexUrl,
140 minimum_libc_version: Option<MinimumLibcVersion>,
141 ) {
142 match filename {
145 DistFilename::WheelFilename(filename) => {
146 let version = filename.version.clone();
147
148 let compatibility = Self::wheel_compatibility(
149 &filename,
150 file.hashes.as_slice(),
151 tags,
152 hasher,
153 build_options,
154 );
155 let dist = RegistryBuiltWheel {
156 filename,
157 file: Box::new(file),
158 index,
159 size_is_authoritative: false,
160 };
161 self.0.entry(version).or_default().insert_built(
162 dist,
163 vec![],
164 compatibility,
165 minimum_libc_version,
166 );
167 }
168 DistFilename::SourceDistFilename(filename) => {
169 let compatibility = Self::source_dist_compatibility(
170 &filename,
171 file.hashes.as_slice(),
172 hasher,
173 build_options,
174 );
175 let dist = RegistrySourceDist {
176 name: filename.name.clone(),
177 version: filename.version.clone(),
178 ext: filename.extension,
179 file: Box::new(file),
180 index,
181 wheels: vec![],
182 size_is_authoritative: false,
183 };
184 self.0.entry(filename.version).or_default().insert_source(
185 dist,
186 vec![],
187 compatibility,
188 );
189 }
190 }
191 }
192
193 fn source_dist_compatibility(
194 filename: &SourceDistFilename,
195 hashes: &[HashDigest],
196 hasher: &HashStrategy,
197 build_options: &BuildOptions,
198 ) -> SourceDistCompatibility {
199 if build_options.no_build_package(&filename.name) {
201 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
202 }
203
204 if !filename.extension.is_pep625_compliant() {
209 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
210 }
211
212 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
214 let hash = if hash_policy.requires_validation() {
215 if hashes.is_empty() {
216 HashComparison::Missing
217 } else if hash_policy.matches(hashes) {
218 HashComparison::Matched
219 } else {
220 HashComparison::Mismatched
221 }
222 } else {
223 HashComparison::Matched
224 };
225
226 SourceDistCompatibility::Compatible(hash)
227 }
228
229 fn wheel_compatibility(
230 filename: &WheelFilename,
231 hashes: &[HashDigest],
232 tags: Option<&Tags>,
233 hasher: &HashStrategy,
234 build_options: &BuildOptions,
235 ) -> WheelCompatibility {
236 if filename.variant().is_some() && !uv_preview::is_enabled(PreviewFeature::WheelVariants) {
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 WheelCompatibility::from_tag_priority(filename, hash, priority)
272 }
273}
274
275impl IntoIterator for FlatDistributions {
276 type Item = (Version, PrioritizedDist);
277 type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
278
279 fn into_iter(self) -> Self::IntoIter {
280 self.0.into_iter()
281 }
282}
283
284impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
285 fn from(distributions: FlatDistributions) -> Self {
286 distributions.0
287 }
288}
289
290impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
291 fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
292 Self(distributions)
293 }
294}