1use std::borrow::Cow;
2
3use uv_cache::{Cache, CacheBucket, CacheShard, WheelCache};
4use uv_cache_info::CacheInfo;
5use uv_distribution_types::{
6 BuildInfo, BuildVariables, ConfigSettings, DirectUrlSourceDist, DirectorySourceDist,
7 ExtraBuildRequirement, ExtraBuildRequires, ExtraBuildVariables, GitDirectorySourceDist,
8 GitPathSourceDist, Hashed, PackageConfigSettings, PathSourceDist,
9};
10use uv_normalize::PackageName;
11use uv_platform_tags::Tags;
12use uv_pypi_types::HashDigests;
13use uv_types::HashStrategy;
14
15use crate::Error;
16use crate::index::cached_wheel::{CachedWheel, ResolvedWheel};
17use crate::source::{
18 HASHES, HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer,
19 RevisionHashes,
20};
21
22#[derive(Debug)]
24pub struct BuiltWheelIndex<'a> {
25 cache: &'a Cache,
26 tags: &'a Tags,
27 hasher: &'a HashStrategy,
28 config_settings: &'a ConfigSettings,
29 config_settings_package: &'a PackageConfigSettings,
30 extra_build_requires: &'a ExtraBuildRequires,
31 extra_build_variables: &'a ExtraBuildVariables,
32}
33
34impl<'a> BuiltWheelIndex<'a> {
35 pub fn new(
37 cache: &'a Cache,
38 tags: &'a Tags,
39 hasher: &'a HashStrategy,
40 config_settings: &'a ConfigSettings,
41 config_settings_package: &'a PackageConfigSettings,
42 extra_build_requires: &'a ExtraBuildRequires,
43 extra_build_variables: &'a ExtraBuildVariables,
44 ) -> Self {
45 Self {
46 cache,
47 tags,
48 hasher,
49 config_settings,
50 config_settings_package,
51 extra_build_requires,
52 extra_build_variables,
53 }
54 }
55
56 pub fn url(&self, source_dist: &DirectUrlSourceDist) -> Result<Option<CachedWheel>, Error> {
61 let cache_shard = self.cache.shard(
63 CacheBucket::SourceDistributions,
64 WheelCache::Url(source_dist.url.raw()).root(),
65 );
66
67 let Some(pointer) = HttpRevisionPointer::read_from(cache_shard.entry(HTTP_REVISION))?
69 else {
70 return Ok(None);
71 };
72
73 let revision = pointer.into_revision();
75 if !revision.satisfies(self.hasher.get(source_dist)) {
76 return Ok(None);
77 }
78
79 let cache_shard = cache_shard.shard(revision.id());
80
81 let config_settings = self.config_settings_for(&source_dist.name);
83 let extra_build_deps = self.extra_build_requires_for(&source_dist.name);
84 let extra_build_vars = self.extra_build_variables_for(&source_dist.name);
85 let build_info = BuildInfo::from_settings(
86 config_settings.into_owned(),
87 extra_build_deps.to_vec(),
88 extra_build_vars.cloned(),
89 );
90 let cache_shard = build_info
91 .cache_shard()
92 .map(|digest| cache_shard.shard(digest))
93 .unwrap_or(cache_shard);
94
95 Ok(self.find(&cache_shard).map(|wheel| {
96 CachedWheel::from_entry(
97 wheel,
98 revision.into_hashes(),
99 CacheInfo::default(),
100 build_info,
101 )
102 }))
103 }
104
105 pub fn path(&self, source_dist: &PathSourceDist) -> Result<Option<CachedWheel>, Error> {
107 let cache_shard = self.cache.shard(
108 CacheBucket::SourceDistributions,
109 WheelCache::Path(&source_dist.url).root(),
110 );
111
112 let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))?
114 else {
115 return Ok(None);
116 };
117
118 let cache_info =
120 CacheInfo::from_file(&source_dist.install_path).map_err(Error::CacheRead)?;
121 if cache_info != *pointer.cache_info() {
122 return Ok(None);
123 }
124
125 let revision = pointer.into_revision();
127 if !revision.satisfies(self.hasher.get(source_dist)) {
128 return Ok(None);
129 }
130
131 let cache_shard = cache_shard.shard(revision.id());
132
133 let config_settings = self.config_settings_for(&source_dist.name);
135 let extra_build_deps = self.extra_build_requires_for(&source_dist.name);
136 let extra_build_vars = self.extra_build_variables_for(&source_dist.name);
137 let build_info = BuildInfo::from_settings(
138 config_settings.into_owned(),
139 extra_build_deps.to_vec(),
140 extra_build_vars.cloned(),
141 );
142 let cache_shard = build_info
143 .cache_shard()
144 .map(|digest| cache_shard.shard(digest))
145 .unwrap_or(cache_shard);
146
147 Ok(self.find(&cache_shard).map(|wheel| {
148 CachedWheel::from_entry(wheel, revision.into_hashes(), cache_info, build_info)
149 }))
150 }
151
152 pub fn directory(
155 &self,
156 source_dist: &DirectorySourceDist,
157 ) -> Result<Option<CachedWheel>, Error> {
158 let cache_shard = self.cache.shard(
159 CacheBucket::SourceDistributions,
160 if source_dist.editable.unwrap_or(false) {
161 WheelCache::Editable(&source_dist.url).root()
162 } else {
163 WheelCache::Path(&source_dist.url).root()
164 },
165 );
166
167 let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))?
169 else {
170 return Ok(None);
171 };
172
173 let cache_info = CacheInfo::from_directory(&source_dist.install_path)?;
175 if cache_info != *pointer.cache_info() {
176 return Ok(None);
177 }
178
179 let revision = pointer.into_revision();
181 if !revision.satisfies(self.hasher.get(source_dist)) {
182 return Ok(None);
183 }
184
185 let cache_shard = cache_shard.shard(revision.id());
186
187 let config_settings = self.config_settings_for(&source_dist.name);
189 let extra_build_deps = self.extra_build_requires_for(&source_dist.name);
190 let extra_build_vars = self.extra_build_variables_for(&source_dist.name);
191 let build_info = BuildInfo::from_settings(
192 config_settings.into_owned(),
193 extra_build_deps.to_vec(),
194 extra_build_vars.cloned(),
195 );
196 let cache_shard = build_info
197 .cache_shard()
198 .map(|digest| cache_shard.shard(digest))
199 .unwrap_or(cache_shard);
200
201 Ok(self.find(&cache_shard).map(|wheel| {
202 CachedWheel::from_entry(wheel, revision.into_hashes(), cache_info, build_info)
203 }))
204 }
205
206 pub fn git_directory(&self, source_dist: &GitDirectorySourceDist) -> Option<CachedWheel> {
208 if self.hasher.get(source_dist).requires_validation() {
210 return None;
211 }
212
213 let git_sha = source_dist.git.precise()?;
214
215 let cache_shard = self.cache.shard(
216 CacheBucket::SourceDistributions,
217 WheelCache::Git(&source_dist.url, git_sha.as_short_str()).root(),
218 );
219
220 let config_settings = self.config_settings_for(&source_dist.name);
222 let extra_build_deps = self.extra_build_requires_for(&source_dist.name);
223 let extra_build_vars = self.extra_build_variables_for(&source_dist.name);
224 let build_info = BuildInfo::from_settings(
225 config_settings.into_owned(),
226 extra_build_deps.to_vec(),
227 extra_build_vars.cloned(),
228 );
229 let cache_shard = build_info
230 .cache_shard()
231 .map(|digest| cache_shard.shard(digest))
232 .unwrap_or(cache_shard);
233
234 self.find(&cache_shard).map(|wheel| {
235 CachedWheel::from_entry(
236 wheel,
237 HashDigests::empty(),
238 CacheInfo::default(),
239 build_info,
240 )
241 })
242 }
243
244 pub fn git_path(&self, source_dist: &GitPathSourceDist) -> Result<Option<CachedWheel>, Error> {
246 let Some(git_sha) = source_dist.git.precise() else {
247 return Ok(None);
248 };
249
250 let cache_shard = self.cache.shard(
251 CacheBucket::SourceDistributions,
252 WheelCache::Git(&source_dist.url, git_sha.as_short_str()).root(),
253 );
254
255 let Some(revision) = RevisionHashes::read_from(cache_shard.entry(HASHES))? else {
257 return Ok(None);
258 };
259
260 if !revision.satisfies(self.hasher.get(source_dist)) {
262 return Ok(None);
263 }
264
265 let config_settings = self.config_settings_for(&source_dist.name);
267 let extra_build_deps = self.extra_build_requires_for(&source_dist.name);
268 let extra_build_vars = self.extra_build_variables_for(&source_dist.name);
269 let build_info = BuildInfo::from_settings(
270 config_settings.into_owned(),
271 extra_build_deps.to_vec(),
272 extra_build_vars.cloned(),
273 );
274 let cache_shard = build_info
275 .cache_shard()
276 .map(|digest| cache_shard.shard(digest))
277 .unwrap_or(cache_shard);
278
279 Ok(self.find(&cache_shard).map(|wheel| {
280 CachedWheel::from_entry(
281 wheel,
282 revision.into_hashes(),
283 CacheInfo::default(),
284 build_info,
285 )
286 }))
287 }
288
289 fn find(&self, shard: &CacheShard) -> Option<ResolvedWheel> {
306 let mut candidate: Option<ResolvedWheel> = None;
307
308 for wheel_dir in uv_fs::entries(shard).ok().into_iter().flatten() {
310 if wheel_dir
312 .extension()
313 .is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
314 {
315 continue;
316 }
317
318 match ResolvedWheel::from_built_source(&wheel_dir, self.cache) {
319 None => {}
320 Some(dist_info) => {
321 let compatibility = dist_info.filename.compatibility(self.tags);
323
324 if !compatibility.is_compatible() {
326 continue;
327 }
328
329 if let Some(existing) = candidate.as_ref() {
330 if dist_info.filename.version > existing.filename.version
332 || compatibility > existing.filename.compatibility(self.tags)
333 {
334 candidate = Some(dist_info);
335 }
336 } else {
337 candidate = Some(dist_info);
338 }
339 }
340 }
341 }
342
343 candidate
344 }
345
346 fn config_settings_for(&self, name: &PackageName) -> Cow<'_, ConfigSettings> {
348 if let Some(package_settings) = self.config_settings_package.get(name) {
349 Cow::Owned(package_settings.clone().merge(self.config_settings.clone()))
350 } else {
351 Cow::Borrowed(self.config_settings)
352 }
353 }
354
355 fn extra_build_requires_for(&self, name: &PackageName) -> &[ExtraBuildRequirement] {
357 self.extra_build_requires
358 .get(name)
359 .map(Vec::as_slice)
360 .unwrap_or(&[])
361 }
362
363 fn extra_build_variables_for(&self, name: &PackageName) -> Option<&BuildVariables> {
365 self.extra_build_variables.get(name)
366 }
367}