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, IndexLocations, IndexUrl, MinimumLibcVersion, RegistryBuiltWheel,
14 RegistrySourceDist,
15};
16use uv_normalize::PackageName;
17use uv_pep440::Version;
18use uv_platform_tags::{TagCompatibility, Tags};
19use uv_pypi_types::HashDigest;
20use uv_types::HashStrategy;
21
22use crate::prioritized_distribution::{
23 HashComparison, IncompatibleSource, IncompatibleWheel, PrioritizedDist,
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 distributions.add_file(
98 file,
99 filename,
100 tags,
101 hasher,
102 build_options,
103 index,
104 minimum_libc_version,
105 );
106 }
107 distributions
108 }
109
110 pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
112 self.0.iter()
113 }
114
115 fn add_file(
117 &mut self,
118 file: File,
119 filename: DistFilename,
120 tags: Option<&Tags>,
121 hasher: &HashStrategy,
122 build_options: &BuildOptions,
123 index: IndexUrl,
124 minimum_libc_version: Option<MinimumLibcVersion>,
125 ) {
126 match filename {
129 DistFilename::WheelFilename(filename) => {
130 let version = filename.version.clone();
131
132 let compatibility = Self::wheel_compatibility(
133 &filename,
134 file.hashes.as_slice(),
135 tags,
136 hasher,
137 build_options,
138 );
139 let dist = RegistryBuiltWheel {
140 filename,
141 file: Box::new(file),
142 index,
143 size_is_authoritative: false,
144 };
145 self.0.entry(version).or_default().insert_built(
146 dist,
147 vec![],
148 compatibility,
149 minimum_libc_version,
150 );
151 }
152 DistFilename::SourceDistFilename(filename) => {
153 let compatibility = Self::source_dist_compatibility(
154 &filename,
155 file.hashes.as_slice(),
156 hasher,
157 build_options,
158 );
159 let dist = RegistrySourceDist {
160 name: filename.name.clone(),
161 version: filename.version.clone(),
162 ext: filename.extension,
163 file: Box::new(file),
164 index,
165 wheels: vec![],
166 size_is_authoritative: false,
167 };
168 self.0.entry(filename.version).or_default().insert_source(
169 dist,
170 vec![],
171 compatibility,
172 );
173 }
174 }
175 }
176
177 fn source_dist_compatibility(
178 filename: &SourceDistFilename,
179 hashes: &[HashDigest],
180 hasher: &HashStrategy,
181 build_options: &BuildOptions,
182 ) -> SourceDistCompatibility {
183 if build_options.no_build_package(&filename.name) {
185 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
186 }
187
188 if !filename.extension.is_pep625_compliant() {
193 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
194 }
195
196 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
198 let hash = if hash_policy.requires_validation() {
199 if hashes.is_empty() {
200 HashComparison::Missing
201 } else if hash_policy.matches(hashes) {
202 HashComparison::Matched
203 } else {
204 HashComparison::Mismatched
205 }
206 } else {
207 HashComparison::Matched
208 };
209
210 SourceDistCompatibility::Compatible(hash)
211 }
212
213 fn wheel_compatibility(
214 filename: &WheelFilename,
215 hashes: &[HashDigest],
216 tags: Option<&Tags>,
217 hasher: &HashStrategy,
218 build_options: &BuildOptions,
219 ) -> WheelCompatibility {
220 if build_options.no_binary_package(&filename.name) {
222 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
223 }
224
225 let priority = match tags {
227 Some(tags) => match filename.compatibility(tags) {
228 TagCompatibility::Incompatible(tag) => {
229 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
230 }
231 TagCompatibility::Compatible(priority) => Some(priority),
232 },
233 None => None,
234 };
235
236 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
238 let hash = if hash_policy.requires_validation() {
239 if hashes.is_empty() {
240 HashComparison::Missing
241 } else if hash_policy.matches(hashes) {
242 HashComparison::Matched
243 } else {
244 HashComparison::Mismatched
245 }
246 } else {
247 HashComparison::Matched
248 };
249
250 let build_tag = filename.build_tag().cloned();
252
253 WheelCompatibility::Compatible(hash, priority, build_tag)
254 }
255}
256
257impl IntoIterator for FlatDistributions {
258 type Item = (Version, PrioritizedDist);
259 type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
260
261 fn into_iter(self) -> Self::IntoIter {
262 self.0.into_iter()
263 }
264}
265
266impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
267 fn from(distributions: FlatDistributions) -> Self {
268 distributions.0
269 }
270}
271
272impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
273 fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
274 Self(distributions)
275 }
276}