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, HashComparison, IncompatibleSource, IncompatibleWheel, Index, IndexLocations, IndexUrl,
14 MinimumLibcVersion, PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist,
15 SourceDistCompatibility, WheelCompatibility,
16};
17use uv_normalize::PackageName;
18use uv_pep440::Version;
19use uv_platform_tags::{TagCompatibility, Tags};
20use uv_pypi_types::HashDigest;
21use uv_types::HashStrategy;
22
23#[derive(Debug, Clone, Default)]
25pub struct FlatIndex {
26 index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
28 offline: bool,
31}
32
33impl FlatIndex {
34 pub async fn load(
36 client: &RegistryClient,
37 cache: &Cache,
38 index_locations: &IndexLocations,
39 ) -> Result<Self, FlatIndexError> {
40 let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
41 let entries = client
42 .fetch_all(index_locations.flat_indexes().map(Index::url))
43 .await?;
44 Ok(Self::from_entries(entries))
45 }
46
47 #[instrument(skip_all)]
49 fn from_entries(entries: FlatIndexEntries) -> Self {
50 let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
51 let (entries, offline) = entries.into_parts();
52
53 for entry in entries {
54 index
55 .entry(entry.filename().name().clone())
56 .or_default()
57 .push(entry);
58 }
59
60 Self { index, offline }
61 }
62
63 pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
65 self.index.get(package_name).map(Vec::as_slice)
66 }
67
68 pub(crate) fn offline(&self) -> bool {
71 self.offline
72 }
73}
74
75#[derive(Debug, Clone, Default)]
78pub struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
79
80impl FlatDistributions {
81 #[instrument(skip_all)]
83 pub(crate) fn from_entries(
84 entries: impl IntoIterator<Item = FlatIndexEntry>,
85 tags: Option<&Tags>,
86 hasher: &HashStrategy,
87 build_options: &BuildOptions,
88 minimum_libc_version: Option<MinimumLibcVersion>,
89 ) -> Self {
90 let mut distributions = Self::default();
91 for entry in entries {
92 let (filename, file, index) = entry.into_parts();
93 distributions.add_file(
94 file,
95 filename,
96 tags,
97 hasher,
98 build_options,
99 index,
100 minimum_libc_version,
101 );
102 }
103 distributions
104 }
105
106 pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
108 self.0.iter()
109 }
110
111 fn add_file(
113 &mut self,
114 file: File,
115 filename: DistFilename,
116 tags: Option<&Tags>,
117 hasher: &HashStrategy,
118 build_options: &BuildOptions,
119 index: IndexUrl,
120 minimum_libc_version: Option<MinimumLibcVersion>,
121 ) {
122 match filename {
125 DistFilename::WheelFilename(filename) => {
126 let version = filename.version.clone();
127
128 let compatibility = Self::wheel_compatibility(
129 &filename,
130 file.hashes.as_slice(),
131 tags,
132 hasher,
133 build_options,
134 );
135 let dist = RegistryBuiltWheel {
136 filename,
137 file: Box::new(file),
138 index,
139 size_is_authoritative: false,
140 };
141 self.0.entry(version).or_default().insert_built(
142 dist,
143 vec![],
144 compatibility,
145 minimum_libc_version,
146 );
147 }
148 DistFilename::SourceDistFilename(filename) => {
149 let compatibility = Self::source_dist_compatibility(
150 &filename,
151 file.hashes.as_slice(),
152 hasher,
153 build_options,
154 );
155 let dist = RegistrySourceDist {
156 name: filename.name.clone(),
157 version: filename.version.clone(),
158 ext: filename.extension,
159 file: Box::new(file),
160 index,
161 wheels: vec![],
162 size_is_authoritative: false,
163 };
164 self.0.entry(filename.version).or_default().insert_source(
165 dist,
166 vec![],
167 compatibility,
168 );
169 }
170 }
171 }
172
173 fn source_dist_compatibility(
174 filename: &SourceDistFilename,
175 hashes: &[HashDigest],
176 hasher: &HashStrategy,
177 build_options: &BuildOptions,
178 ) -> SourceDistCompatibility {
179 if build_options.no_build_package(&filename.name) {
181 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
182 }
183
184 if !filename.extension.is_pep625_compliant() {
189 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
190 }
191
192 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
194 let hash = if hash_policy.requires_validation() {
195 if hashes.is_empty() {
196 HashComparison::Missing
197 } else if hash_policy.matches(hashes) {
198 HashComparison::Matched
199 } else {
200 HashComparison::Mismatched
201 }
202 } else {
203 HashComparison::Matched
204 };
205
206 SourceDistCompatibility::Compatible(hash)
207 }
208
209 fn wheel_compatibility(
210 filename: &WheelFilename,
211 hashes: &[HashDigest],
212 tags: Option<&Tags>,
213 hasher: &HashStrategy,
214 build_options: &BuildOptions,
215 ) -> WheelCompatibility {
216 if build_options.no_binary_package(&filename.name) {
218 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
219 }
220
221 let priority = match tags {
223 Some(tags) => match filename.compatibility(tags) {
224 TagCompatibility::Incompatible(tag) => {
225 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
226 }
227 TagCompatibility::Compatible(priority) => Some(priority),
228 },
229 None => None,
230 };
231
232 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
234 let hash = if hash_policy.requires_validation() {
235 if hashes.is_empty() {
236 HashComparison::Missing
237 } else if hash_policy.matches(hashes) {
238 HashComparison::Matched
239 } else {
240 HashComparison::Mismatched
241 }
242 } else {
243 HashComparison::Matched
244 };
245
246 let build_tag = filename.build_tag().cloned();
248
249 WheelCompatibility::Compatible(hash, priority, build_tag)
250 }
251}
252
253impl IntoIterator for FlatDistributions {
254 type Item = (Version, PrioritizedDist);
255 type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
256
257 fn into_iter(self) -> Self::IntoIter {
258 self.0.into_iter()
259 }
260}
261
262impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
263 fn from(distributions: FlatDistributions) -> Self {
264 distributions.0
265 }
266}
267
268impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
269 fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
270 Self(distributions)
271 }
272}