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