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 PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility,
15 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(crate) 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 ) -> Self {
89 let mut distributions = Self::default();
90 for entry in entries {
91 let (filename, file, index) = entry.into_parts();
92 distributions.add_file(file, filename, tags, hasher, build_options, index);
93 }
94 distributions
95 }
96
97 pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
99 self.0.iter()
100 }
101
102 fn add_file(
104 &mut self,
105 file: File,
106 filename: DistFilename,
107 tags: Option<&Tags>,
108 hasher: &HashStrategy,
109 build_options: &BuildOptions,
110 index: IndexUrl,
111 ) {
112 match filename {
115 DistFilename::WheelFilename(filename) => {
116 let version = filename.version.clone();
117
118 let compatibility = Self::wheel_compatibility(
119 &filename,
120 file.hashes.as_slice(),
121 tags,
122 hasher,
123 build_options,
124 );
125 let dist = RegistryBuiltWheel {
126 filename,
127 file: Box::new(file),
128 index,
129 size_is_authoritative: false,
130 };
131 self.0
132 .entry(version)
133 .or_default()
134 .insert_built(dist, vec![], compatibility);
135 }
136 DistFilename::SourceDistFilename(filename) => {
137 let compatibility = Self::source_dist_compatibility(
138 &filename,
139 file.hashes.as_slice(),
140 hasher,
141 build_options,
142 );
143 let dist = RegistrySourceDist {
144 name: filename.name.clone(),
145 version: filename.version.clone(),
146 ext: filename.extension,
147 file: Box::new(file),
148 index,
149 wheels: vec![],
150 size_is_authoritative: false,
151 };
152 self.0.entry(filename.version).or_default().insert_source(
153 dist,
154 vec![],
155 compatibility,
156 );
157 }
158 }
159 }
160
161 fn source_dist_compatibility(
162 filename: &SourceDistFilename,
163 hashes: &[HashDigest],
164 hasher: &HashStrategy,
165 build_options: &BuildOptions,
166 ) -> SourceDistCompatibility {
167 if build_options.no_build_package(&filename.name) {
169 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
170 }
171
172 if !filename.extension.is_pep625_compliant() {
177 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
178 }
179
180 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
182 let hash = if hash_policy.requires_validation() {
183 if hashes.is_empty() {
184 HashComparison::Missing
185 } else if hash_policy.matches(hashes) {
186 HashComparison::Matched
187 } else {
188 HashComparison::Mismatched
189 }
190 } else {
191 HashComparison::Matched
192 };
193
194 SourceDistCompatibility::Compatible(hash)
195 }
196
197 fn wheel_compatibility(
198 filename: &WheelFilename,
199 hashes: &[HashDigest],
200 tags: Option<&Tags>,
201 hasher: &HashStrategy,
202 build_options: &BuildOptions,
203 ) -> WheelCompatibility {
204 if build_options.no_binary_package(&filename.name) {
206 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
207 }
208
209 let priority = match tags {
211 Some(tags) => match filename.compatibility(tags) {
212 TagCompatibility::Incompatible(tag) => {
213 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
214 }
215 TagCompatibility::Compatible(priority) => Some(priority),
216 },
217 None => None,
218 };
219
220 let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
222 let hash = if hash_policy.requires_validation() {
223 if hashes.is_empty() {
224 HashComparison::Missing
225 } else if hash_policy.matches(hashes) {
226 HashComparison::Matched
227 } else {
228 HashComparison::Mismatched
229 }
230 } else {
231 HashComparison::Matched
232 };
233
234 let build_tag = filename.build_tag().cloned();
236
237 WheelCompatibility::Compatible(hash, priority, build_tag)
238 }
239}
240
241impl IntoIterator for FlatDistributions {
242 type Item = (Version, PrioritizedDist);
243 type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
244
245 fn into_iter(self) -> Self::IntoIter {
246 self.0.into_iter()
247 }
248}
249
250impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
251 fn from(distributions: FlatDistributions) -> Self {
252 distributions.0
253 }
254}