1use std::borrow::Cow;
2use std::collections::hash_map::Entry;
3
4use rustc_hash::{FxHashMap, FxHashSet};
5
6use uv_cache::{Cache, CacheBucket, WheelCache};
7use uv_cache_info::CacheInfo;
8use uv_distribution_filename::WheelFilename;
9use uv_distribution_types::{
10 BuildInfo, BuildVariables, CachedRegistryDist, ConfigSettings, ExtraBuildRequirement,
11 ExtraBuildRequires, ExtraBuildVariables, Hashed, Index, IndexLocations, IndexUrl,
12 PackageConfigSettings, RegistryBuiltDist, RegistrySourceDist,
13};
14use uv_fs::{directories, files};
15use uv_normalize::PackageName;
16use uv_pep440::Version;
17use uv_platform_tags::Tags;
18use uv_types::HashStrategy;
19
20use crate::index::cached_wheel::{CachedWheel, ResolvedWheel};
21use crate::source::{HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer};
22
23#[derive(Debug, Clone, Hash, PartialEq, Eq)]
25pub struct IndexEntry<'index> {
26 dist: CachedRegistryDist,
28 built: bool,
30 index: &'index Index,
32}
33
34impl IndexEntry<'_> {
35 pub fn index(&self) -> &Index {
37 self.index
38 }
39
40 pub fn is_built(&self) -> bool {
42 self.built
43 }
44
45 pub fn dist(&self) -> &CachedRegistryDist {
47 &self.dist
48 }
49
50 fn matches_wheel(
51 &self,
52 index: &IndexUrl,
53 filename: &WheelFilename,
54 no_build: bool,
55 no_binary: bool,
56 ) -> bool {
57 self.matches_index_and_build_policy(index, no_build, no_binary)
58 && self.dist.filename == *filename
59 }
60
61 fn matches_source(
62 &self,
63 index: &IndexUrl,
64 name: &PackageName,
65 version: &Version,
66 no_build: bool,
67 no_binary: bool,
68 ) -> bool {
69 self.matches_index_and_build_policy(index, no_build, no_binary)
70 && self.dist.filename.name == *name
71 && self.dist.filename.version == *version
72 }
73
74 fn matches_index_and_build_policy(
75 &self,
76 index: &IndexUrl,
77 no_build: bool,
78 no_binary: bool,
79 ) -> bool {
80 if *self.index.url() != *index {
81 return false;
82 }
83 if self.built { !no_build } else { !no_binary }
84 }
85}
86
87#[derive(Debug)]
89pub struct RegistryWheelIndex<'a> {
90 cache: &'a Cache,
91 tags: &'a Tags,
92 index_locations: &'a IndexLocations,
93 hasher: &'a HashStrategy,
94 index: FxHashMap<&'a PackageName, Vec<IndexEntry<'a>>>,
95 config_settings: &'a ConfigSettings,
96 config_settings_package: &'a PackageConfigSettings,
97 extra_build_requires: &'a ExtraBuildRequires,
98 extra_build_variables: &'a ExtraBuildVariables,
99}
100
101impl<'a> RegistryWheelIndex<'a> {
102 pub fn new(
104 cache: &'a Cache,
105 tags: &'a Tags,
106 index_locations: &'a IndexLocations,
107 hasher: &'a HashStrategy,
108 config_settings: &'a ConfigSettings,
109 config_settings_package: &'a PackageConfigSettings,
110 extra_build_requires: &'a ExtraBuildRequires,
111 extra_build_variables: &'a ExtraBuildVariables,
112 ) -> Self {
113 Self {
114 cache,
115 tags,
116 index_locations,
117 hasher,
118 config_settings,
119 config_settings_package,
120 extra_build_requires,
121 extra_build_variables,
122 index: FxHashMap::default(),
123 }
124 }
125
126 pub fn wheel(
128 &mut self,
129 wheel: &'a RegistryBuiltDist,
130 no_build: bool,
131 no_binary: bool,
132 ) -> Option<&CachedRegistryDist> {
133 let wheel = wheel.best_wheel();
134 self.get(&wheel.filename.name).find_map(|entry| {
135 entry
136 .matches_wheel(&wheel.index, &wheel.filename, no_build, no_binary)
137 .then_some(&entry.dist)
138 })
139 }
140
141 pub fn source(
143 &mut self,
144 source: &'a RegistrySourceDist,
145 no_build: bool,
146 no_binary: bool,
147 ) -> Option<&CachedRegistryDist> {
148 self.get(&source.name).find_map(|entry| {
149 entry
150 .matches_source(
151 &source.index,
152 &source.name,
153 &source.version,
154 no_build,
155 no_binary,
156 )
157 .then_some(&entry.dist)
158 })
159 }
160
161 pub fn get(&mut self, name: &'a PackageName) -> impl Iterator<Item = &IndexEntry<'_>> {
165 self.get_impl(name).iter().rev()
166 }
167
168 fn get_impl(&mut self, name: &'a PackageName) -> &[IndexEntry<'_>] {
170 (match self.index.entry(name) {
171 Entry::Occupied(entry) => entry.into_mut(),
172 Entry::Vacant(entry) => entry.insert(Self::index(
173 name,
174 self.cache,
175 self.tags,
176 self.index_locations,
177 self.hasher,
178 self.config_settings,
179 self.config_settings_package,
180 self.extra_build_requires,
181 self.extra_build_variables,
182 )),
183 }) as _
184 }
185
186 fn index<'index>(
188 package: &PackageName,
189 cache: &Cache,
190 tags: &Tags,
191 index_locations: &'index IndexLocations,
192 hasher: &HashStrategy,
193 config_settings: &ConfigSettings,
194 config_settings_package: &PackageConfigSettings,
195 extra_build_requires: &ExtraBuildRequires,
196 extra_build_variables: &ExtraBuildVariables,
197 ) -> Vec<IndexEntry<'index>> {
198 let mut entries = vec![];
199
200 let mut seen = FxHashSet::default();
201 for index in index_locations.allowed_indexes() {
202 if !seen.insert(index.url()) {
203 continue;
204 }
205
206 let wheel_dir = cache.shard(
208 CacheBucket::Wheels,
209 WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
210 );
211
212 for file in files(&wheel_dir).ok().into_iter().flatten() {
215 match index.url() {
216 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
218 if file
219 .extension()
220 .is_some_and(|ext| ext.eq_ignore_ascii_case("http"))
221 {
222 if let Some(wheel) =
223 CachedWheel::from_http_pointer(wheel_dir.join(file), cache)
224 {
225 if wheel.filename.compatibility(tags).is_compatible() {
226 if wheel.satisfies(
228 hasher.get_package(
229 &wheel.filename.name,
230 &wheel.filename.version,
231 ),
232 ) {
233 entries.push(IndexEntry {
234 dist: wheel.into_registry_dist(),
235 index,
236 built: false,
237 });
238 }
239 }
240 }
241 }
242 }
243 IndexUrl::Path(_) => {
245 if file
246 .extension()
247 .is_some_and(|ext| ext.eq_ignore_ascii_case("rev"))
248 {
249 if let Some(wheel) =
250 CachedWheel::from_local_pointer(wheel_dir.join(file), cache)
251 {
252 if wheel.filename.compatibility(tags).is_compatible() {
253 if wheel.satisfies(
255 hasher.get_package(
256 &wheel.filename.name,
257 &wheel.filename.version,
258 ),
259 ) {
260 entries.push(IndexEntry {
261 dist: wheel.into_registry_dist(),
262 index,
263 built: false,
264 });
265 }
266 }
267 }
268 }
269 }
270 }
271 }
272
273 let cache_shard = cache.shard(
276 CacheBucket::SourceDistributions,
277 WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
278 );
279
280 for shard in directories(&cache_shard).ok().into_iter().flatten() {
282 let cache_shard = cache_shard.shard(shard);
283
284 let revision = match index.url() {
286 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
288 let revision_entry = cache_shard.entry(HTTP_REVISION);
289 if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision_entry) {
290 Some(pointer.into_revision())
291 } else {
292 None
293 }
294 }
295 IndexUrl::Path(_) => {
297 let revision_entry = cache_shard.entry(LOCAL_REVISION);
298 if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision_entry) {
299 Some(pointer.into_revision())
300 } else {
301 None
302 }
303 }
304 };
305
306 if let Some(revision) = revision {
307 let cache_shard = cache_shard.shard(revision.id());
308
309 let extra_build_deps =
311 Self::extra_build_requires_for(package, extra_build_requires);
312 let extra_build_vars =
313 Self::extra_build_variables_for(package, extra_build_variables);
314 let config_settings = Self::config_settings_for(
315 package,
316 config_settings,
317 config_settings_package,
318 );
319 let build_info = BuildInfo::from_settings(
320 config_settings.into_owned(),
321 extra_build_deps.to_vec(),
322 extra_build_vars.cloned(),
323 );
324 let cache_shard = build_info
325 .cache_shard()
326 .map(|digest| cache_shard.shard(digest))
327 .unwrap_or(cache_shard);
328
329 for wheel_dir in uv_fs::entries(cache_shard).ok().into_iter().flatten() {
330 if wheel_dir
332 .extension()
333 .is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
334 {
335 continue;
336 }
337
338 if let Some(wheel) = ResolvedWheel::from_built_source(wheel_dir, cache) {
339 if wheel.filename.compatibility(tags).is_compatible() {
340 if revision.satisfies(
342 hasher
343 .get_package(&wheel.filename.name, &wheel.filename.version),
344 ) {
345 let wheel = CachedWheel::from_entry(
346 wheel,
347 revision.hashes().into(),
348 CacheInfo::default(),
349 build_info.clone(),
350 );
351 entries.push(IndexEntry {
352 dist: wheel.into_registry_dist(),
353 index,
354 built: true,
355 });
356 }
357 }
358 }
359 }
360 }
361 }
362 }
363
364 entries.sort_unstable_by(|a, b| {
368 a.dist
369 .filename
370 .version
371 .cmp(&b.dist.filename.version)
372 .then_with(|| {
373 a.dist
374 .filename
375 .compatibility(tags)
376 .cmp(&b.dist.filename.compatibility(tags))
377 .then_with(|| a.built.cmp(&b.built))
378 })
379 });
380
381 entries
382 }
383
384 fn config_settings_for<'settings>(
386 name: &PackageName,
387 config_settings: &'settings ConfigSettings,
388 config_settings_package: &PackageConfigSettings,
389 ) -> Cow<'settings, ConfigSettings> {
390 if let Some(package_settings) = config_settings_package.get(name) {
391 Cow::Owned(package_settings.clone().merge(config_settings.clone()))
392 } else {
393 Cow::Borrowed(config_settings)
394 }
395 }
396
397 fn extra_build_requires_for<'settings>(
399 name: &PackageName,
400 extra_build_requires: &'settings ExtraBuildRequires,
401 ) -> &'settings [ExtraBuildRequirement] {
402 extra_build_requires
403 .get(name)
404 .map(Vec::as_slice)
405 .unwrap_or(&[])
406 }
407
408 fn extra_build_variables_for<'settings>(
410 name: &PackageName,
411 extra_build_variables: &'settings ExtraBuildVariables,
412 ) -> Option<&'settings BuildVariables> {
413 extra_build_variables.get(name)
414 }
415}