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_types::{
9 BuildInfo, BuildVariables, CachedRegistryDist, ConfigSettings, ExtraBuildRequirement,
10 ExtraBuildRequires, ExtraBuildVariables, Hashed, Index, IndexLocations, IndexUrl,
11 PackageConfigSettings,
12};
13use uv_fs::{directories, files};
14use uv_normalize::PackageName;
15use uv_platform_tags::Tags;
16use uv_types::HashStrategy;
17
18use crate::index::cached_wheel::{CachedWheel, ResolvedWheel};
19use crate::source::{HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer};
20
21#[derive(Debug, Clone, Hash, PartialEq, Eq)]
23pub struct IndexEntry<'index> {
24 pub dist: CachedRegistryDist,
26 pub built: bool,
28 pub index: &'index Index,
30}
31
32#[derive(Debug)]
34pub struct RegistryWheelIndex<'a> {
35 cache: &'a Cache,
36 tags: &'a Tags,
37 index_locations: &'a IndexLocations,
38 hasher: &'a HashStrategy,
39 index: FxHashMap<&'a PackageName, Vec<IndexEntry<'a>>>,
40 config_settings: &'a ConfigSettings,
41 config_settings_package: &'a PackageConfigSettings,
42 extra_build_requires: &'a ExtraBuildRequires,
43 extra_build_variables: &'a ExtraBuildVariables,
44}
45
46impl<'a> RegistryWheelIndex<'a> {
47 pub fn new(
49 cache: &'a Cache,
50 tags: &'a Tags,
51 index_locations: &'a IndexLocations,
52 hasher: &'a HashStrategy,
53 config_settings: &'a ConfigSettings,
54 config_settings_package: &'a PackageConfigSettings,
55 extra_build_requires: &'a ExtraBuildRequires,
56 extra_build_variables: &'a ExtraBuildVariables,
57 ) -> Self {
58 Self {
59 cache,
60 tags,
61 index_locations,
62 hasher,
63 config_settings,
64 config_settings_package,
65 extra_build_requires,
66 extra_build_variables,
67 index: FxHashMap::default(),
68 }
69 }
70
71 pub fn get(&mut self, name: &'a PackageName) -> impl Iterator<Item = &IndexEntry<'_>> {
75 self.get_impl(name).iter().rev()
76 }
77
78 fn get_impl(&mut self, name: &'a PackageName) -> &[IndexEntry<'_>] {
80 (match self.index.entry(name) {
81 Entry::Occupied(entry) => entry.into_mut(),
82 Entry::Vacant(entry) => entry.insert(Self::index(
83 name,
84 self.cache,
85 self.tags,
86 self.index_locations,
87 self.hasher,
88 self.config_settings,
89 self.config_settings_package,
90 self.extra_build_requires,
91 self.extra_build_variables,
92 )),
93 }) as _
94 }
95
96 fn index<'index>(
98 package: &PackageName,
99 cache: &Cache,
100 tags: &Tags,
101 index_locations: &'index IndexLocations,
102 hasher: &HashStrategy,
103 config_settings: &ConfigSettings,
104 config_settings_package: &PackageConfigSettings,
105 extra_build_requires: &ExtraBuildRequires,
106 extra_build_variables: &ExtraBuildVariables,
107 ) -> Vec<IndexEntry<'index>> {
108 let mut entries = vec![];
109
110 let mut seen = FxHashSet::default();
111 for index in index_locations.allowed_indexes() {
112 if !seen.insert(index.url()) {
113 continue;
114 }
115
116 let wheel_dir = cache.shard(
118 CacheBucket::Wheels,
119 WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
120 );
121
122 for file in files(&wheel_dir).ok().into_iter().flatten() {
125 match index.url() {
126 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
128 if file
129 .extension()
130 .is_some_and(|ext| ext.eq_ignore_ascii_case("http"))
131 {
132 if let Some(wheel) =
133 CachedWheel::from_http_pointer(wheel_dir.join(file), cache)
134 {
135 if wheel.filename.compatibility(tags).is_compatible() {
136 if wheel.satisfies(
138 hasher.get_package(
139 &wheel.filename.name,
140 &wheel.filename.version,
141 ),
142 ) {
143 entries.push(IndexEntry {
144 dist: wheel.into_registry_dist(),
145 index,
146 built: false,
147 });
148 }
149 }
150 }
151 }
152 }
153 IndexUrl::Path(_) => {
155 if file
156 .extension()
157 .is_some_and(|ext| ext.eq_ignore_ascii_case("rev"))
158 {
159 if let Some(wheel) =
160 CachedWheel::from_local_pointer(wheel_dir.join(file), cache)
161 {
162 if wheel.filename.compatibility(tags).is_compatible() {
163 if wheel.satisfies(
165 hasher.get_package(
166 &wheel.filename.name,
167 &wheel.filename.version,
168 ),
169 ) {
170 entries.push(IndexEntry {
171 dist: wheel.into_registry_dist(),
172 index,
173 built: false,
174 });
175 }
176 }
177 }
178 }
179 }
180 }
181 }
182
183 let cache_shard = cache.shard(
186 CacheBucket::SourceDistributions,
187 WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
188 );
189
190 for shard in directories(&cache_shard).ok().into_iter().flatten() {
192 let cache_shard = cache_shard.shard(shard);
193
194 let revision = match index.url() {
196 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
198 let revision_entry = cache_shard.entry(HTTP_REVISION);
199 if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision_entry) {
200 Some(pointer.into_revision())
201 } else {
202 None
203 }
204 }
205 IndexUrl::Path(_) => {
207 let revision_entry = cache_shard.entry(LOCAL_REVISION);
208 if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision_entry) {
209 Some(pointer.into_revision())
210 } else {
211 None
212 }
213 }
214 };
215
216 if let Some(revision) = revision {
217 let cache_shard = cache_shard.shard(revision.id());
218
219 let extra_build_deps =
221 Self::extra_build_requires_for(package, extra_build_requires);
222 let extra_build_vars =
223 Self::extra_build_variables_for(package, extra_build_variables);
224 let config_settings = Self::config_settings_for(
225 package,
226 config_settings,
227 config_settings_package,
228 );
229 let build_info = BuildInfo::from_settings(
230 &config_settings,
231 extra_build_deps,
232 extra_build_vars,
233 );
234 let cache_shard = build_info
235 .cache_shard()
236 .map(|digest| cache_shard.shard(digest))
237 .unwrap_or(cache_shard);
238
239 for wheel_dir in uv_fs::entries(cache_shard).ok().into_iter().flatten() {
240 if wheel_dir
242 .extension()
243 .is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
244 {
245 continue;
246 }
247
248 if let Some(wheel) = ResolvedWheel::from_built_source(wheel_dir, cache) {
249 if wheel.filename.compatibility(tags).is_compatible() {
250 if revision.satisfies(
252 hasher
253 .get_package(&wheel.filename.name, &wheel.filename.version),
254 ) {
255 let wheel = CachedWheel::from_entry(
256 wheel,
257 revision.hashes().into(),
258 CacheInfo::default(),
259 build_info.clone(),
260 );
261 entries.push(IndexEntry {
262 dist: wheel.into_registry_dist(),
263 index,
264 built: true,
265 });
266 }
267 }
268 }
269 }
270 }
271 }
272 }
273
274 entries.sort_unstable_by(|a, b| {
278 a.dist
279 .filename
280 .version
281 .cmp(&b.dist.filename.version)
282 .then_with(|| {
283 a.dist
284 .filename
285 .compatibility(tags)
286 .cmp(&b.dist.filename.compatibility(tags))
287 .then_with(|| a.built.cmp(&b.built))
288 })
289 });
290
291 entries
292 }
293
294 fn config_settings_for<'settings>(
296 name: &PackageName,
297 config_settings: &'settings ConfigSettings,
298 config_settings_package: &PackageConfigSettings,
299 ) -> Cow<'settings, ConfigSettings> {
300 if let Some(package_settings) = config_settings_package.get(name) {
301 Cow::Owned(package_settings.clone().merge(config_settings.clone()))
302 } else {
303 Cow::Borrowed(config_settings)
304 }
305 }
306
307 fn extra_build_requires_for<'settings>(
309 name: &PackageName,
310 extra_build_requires: &'settings ExtraBuildRequires,
311 ) -> &'settings [ExtraBuildRequirement] {
312 extra_build_requires
313 .get(name)
314 .map(Vec::as_slice)
315 .unwrap_or(&[])
316 }
317
318 fn extra_build_variables_for<'settings>(
320 name: &PackageName,
321 extra_build_variables: &'settings ExtraBuildVariables,
322 ) -> Option<&'settings BuildVariables> {
323 extra_build_variables.get(name)
324 }
325}