1#![expect(clippy::redundant_closure_for_method_calls)]
10
11use std::borrow::Cow;
12use std::ops::Bound;
13use std::path::Path;
14use std::str::FromStr;
15use std::sync::Arc;
16
17use fs_err::tokio as fs;
18use futures::{FutureExt, TryStreamExt};
19use reqwest::{Response, StatusCode};
20use tokio_util::compat::FuturesAsyncReadCompatExt;
21use tracing::{Instrument, debug, info_span, instrument, warn};
22use url::Url;
23
24use uv_auth::CredentialsCache;
25use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache};
26use uv_cache_info::CacheInfo;
27use uv_client::{
28 BaseClientBuilder, CacheControl, CachedClientError, Connectivity, DataWithCachePolicy,
29 RegistryClient,
30};
31use uv_configuration::{BuildKind, BuildOutput, NoSources};
32use uv_distribution_filename::{SourceDistExtension, WheelFilename};
33use uv_distribution_types::{
34 BuildInfo, BuildVariables, BuildableSource, ConfigSettings, DirectorySourceUrl,
35 ExtraBuildRequirement, GitDirectorySourceUrl, GitPathSourceUrl, HashPolicy, Hashed, IndexUrl,
36 PathSourceUrl, RemoteSource, RequirementSource, RequiresPython, SourceDist, SourceUrl,
37};
38use uv_fs::{Simplified, rename_with_retry, write_atomic};
39use uv_git::{Fetch, GIT_LFS, GitError, GitHttpSettings, GitResolver};
40use uv_git_types::{GitHubRepository, GitOid, GitUrl};
41use uv_metadata::read_archive_metadata;
42use uv_normalize::PackageName;
43use uv_pep440::{Version, release_specifiers_to_ranges};
44use uv_platform_tags::Tags;
45use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, PyProjectToml, ResolutionMetadata};
46use uv_redacted::DisplaySafeUrl;
47use uv_types::{BuildContext, BuildKey, BuildStack, SourceBuildTrait};
48use uv_workspace::pyproject::ToolUvSources;
49
50use crate::distribution_database::ManagedClient;
51use crate::error::Error;
52use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata};
53use crate::source::built_wheel_metadata::{BuiltWheelFile, BuiltWheelMetadata};
54use crate::source::revision::Revision;
55use crate::source::validated_archive::{ArchiveValidation, ValidatedSourceArchive};
56use crate::{Reporter, RequiresDist};
57
58mod built_wheel_metadata;
59mod revision;
60mod validated_archive;
61
62pub struct StaticMetadataDatabase<'a, 'client> {
67 client_builder: &'a BaseClientBuilder<'client>,
68 git: &'a GitResolver,
69 cache: &'a Cache,
70}
71
72#[derive(Debug)]
74struct MaterializedSourceTree(Box<Path>);
75
76impl MaterializedSourceTree {
77 fn path(&self) -> &Path {
79 &self.0
80 }
81}
82
83impl<'a, 'client> StaticMetadataDatabase<'a, 'client> {
84 pub fn new(
86 client_builder: &'a BaseClientBuilder<'client>,
87 git: &'a GitResolver,
88 cache: &'a Cache,
89 ) -> Self {
90 Self {
91 client_builder,
92 git,
93 cache,
94 }
95 }
96
97 async fn materialize_source_tree(
102 &self,
103 source: &RequirementSource,
104 ) -> Result<Option<MaterializedSourceTree>, Error> {
105 match source {
106 RequirementSource::Directory { install_path, .. } => Ok(Some(MaterializedSourceTree(
107 install_path.to_path_buf().into_boxed_path(),
108 ))),
109 RequirementSource::GitDirectory {
110 git,
111 subdirectory,
112 url,
113 } => {
114 let client = self.client_builder.build()?;
115 let fetch = fetch_git_source_tree(
116 self.git,
117 git,
118 url.to_url(),
119 subdirectory.as_deref(),
120 client.git_http_settings(git.url()),
121 self.cache,
122 None,
123 )
124 .await?;
125
126 if let Some(subdirectory) = subdirectory {
127 let source_tree = fetch.path().join(subdirectory);
128 Ok(Some(MaterializedSourceTree(source_tree.into_boxed_path())))
129 } else {
130 Ok(Some(MaterializedSourceTree(
131 fetch.path().to_path_buf().into_boxed_path(),
132 )))
133 }
134 }
135 _ => Ok(None),
136 }
137 }
138
139 async fn source_tree_requires_python(
141 &self,
142 source_tree: &MaterializedSourceTree,
143 ) -> Result<Option<RequiresPython>, Error> {
144 let pyproject_toml = match read_pyproject_toml(source_tree.path(), None).await {
145 Ok(pyproject_toml) => pyproject_toml,
146 Err(Error::MissingPyprojectToml) => return Ok(None),
147 Err(err) => return Err(err),
148 };
149
150 match pyproject_toml.requires_python() {
151 Ok(Some(requires_python)) => Ok(Some(RequiresPython::from_specifiers(requires_python))),
152 Ok(None) | Err(uv_pypi_types::MetadataError::FieldNotFound("project")) => Ok(None),
153 Err(uv_pypi_types::MetadataError::DynamicField("requires-python")) => {
154 debug!("Ignoring dynamic `requires-python` in source tree");
155 Ok(None)
156 }
157 Err(err) => Err(Error::PyprojectToml(err)),
158 }
159 }
160
161 pub async fn requires_python(
163 &self,
164 source: &RequirementSource,
165 ) -> Result<Option<RequiresPython>, Error> {
166 let Some(source_tree) = self.materialize_source_tree(source).await? else {
167 return Ok(None);
168 };
169 self.source_tree_requires_python(&source_tree).await
170 }
171}
172
173async fn fetch_git_source_tree(
175 git_resolver: &GitResolver,
176 git: &GitUrl,
177 url: DisplaySafeUrl,
178 subdirectory: Option<&Path>,
179 http_settings: GitHttpSettings,
180 cache: &Cache,
181 reporter: Option<Arc<dyn uv_git::Reporter>>,
182) -> Result<Fetch, Error> {
183 let fetch = git_resolver
184 .fetch(git, http_settings, cache.bucket(CacheBucket::Git), reporter)
185 .await?;
186
187 if let Some(subdirectory) = subdirectory
188 && !fetch.path().join(subdirectory).is_dir()
189 {
190 return Err(Error::MissingSubdirectory(url, subdirectory.to_path_buf()));
191 }
192
193 if git.lfs().enabled() && !fetch.lfs_ready() {
194 if GIT_LFS.is_err() {
195 return Err(Error::MissingSourceDistGitLfsArtifacts(
196 url,
197 GitError::GitLfsNotFound,
198 ));
199 }
200 return Err(Error::MissingSourceDistGitLfsArtifacts(
201 url,
202 GitError::GitLfsNotConfigured,
203 ));
204 }
205
206 Ok(fetch)
207}
208
209pub(crate) struct SourceDistributionBuilder<'a, T: BuildContext> {
211 build_context: &'a T,
212 build_stack: Option<&'a BuildStack>,
213 reporter: Option<Arc<dyn Reporter>>,
214}
215
216pub(crate) const HTTP_REVISION: &str = "revision.http";
218
219pub(crate) const LOCAL_REVISION: &str = "revision.rev";
221
222pub(crate) const HASHES: &str = "hashes.msgpack";
224
225const METADATA: &str = "metadata.msgpack";
227
228const SOURCE: &str = "src";
230
231impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
232 pub(crate) fn new(build_context: &'a T) -> Self {
234 Self {
235 build_context,
236 build_stack: None,
237 reporter: None,
238 }
239 }
240
241 #[must_use]
243 pub(crate) fn with_build_stack(self, build_stack: &'a BuildStack) -> Self {
244 Self {
245 build_stack: Some(build_stack),
246 ..self
247 }
248 }
249
250 #[must_use]
252 pub(crate) fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
253 Self {
254 reporter: Some(reporter),
255 ..self
256 }
257 }
258
259 pub(crate) async fn download_and_build(
261 &self,
262 source: &BuildableSource<'_>,
263 tags: &Tags,
264 hashes: HashPolicy<'_>,
265 client: &ManagedClient<'_>,
266 ) -> Result<BuiltWheelMetadata, Error> {
267 let built_wheel_metadata = match &source {
268 BuildableSource::Dist(SourceDist::Registry(dist)) => {
269 let cache_shard = self.build_context.cache().shard(
272 CacheBucket::SourceDistributions,
273 WheelCache::Index(&dist.index)
274 .wheel_dir(dist.name.as_ref())
275 .join(dist.version.to_string()),
276 );
277
278 let url = dist.file.url.to_url()?;
279
280 if url.scheme() == "file" {
282 let path = url
283 .to_file_path()
284 .map_err(|()| Error::NonFileUrl(url.clone()))?;
285 return self
286 .archive(
287 source,
288 &PathSourceUrl {
289 url: &url,
290 path: Cow::Owned(path),
291 ext: dist.ext,
292 },
293 &cache_shard,
294 tags,
295 hashes,
296 )
297 .boxed_local()
298 .await;
299 }
300
301 self.url(
302 source,
303 &url,
304 Some(&dist.index),
305 &cache_shard,
306 None,
307 dist.ext,
308 tags,
309 hashes,
310 client,
311 )
312 .boxed_local()
313 .await?
314 }
315 BuildableSource::Dist(SourceDist::DirectUrl(dist)) => {
316 let cache_shard = self.build_context.cache().shard(
318 CacheBucket::SourceDistributions,
319 WheelCache::Url(&dist.url).root(),
320 );
321
322 self.url(
323 source,
324 &dist.url,
325 None,
326 &cache_shard,
327 dist.subdirectory.as_deref(),
328 dist.ext,
329 tags,
330 hashes,
331 client,
332 )
333 .boxed_local()
334 .await?
335 }
336 BuildableSource::Dist(SourceDist::GitDirectory(dist)) => {
337 self.git_source_tree(
338 source,
339 &GitDirectorySourceUrl::from(dist),
340 tags,
341 hashes,
342 client,
343 )
344 .boxed_local()
345 .await?
346 }
347 BuildableSource::Dist(SourceDist::GitPath(dist)) => {
348 self.git_archive(source, &GitPathSourceUrl::from(dist), tags, hashes, client)
349 .boxed_local()
350 .await?
351 }
352 BuildableSource::Dist(SourceDist::Directory(dist)) => {
353 self.source_tree(source, &DirectorySourceUrl::from(dist), tags, hashes)
354 .boxed_local()
355 .await?
356 }
357 BuildableSource::Dist(SourceDist::Path(dist)) => {
358 let cache_shard = self.build_context.cache().shard(
359 CacheBucket::SourceDistributions,
360 WheelCache::Path(&dist.url).root(),
361 );
362 self.archive(
363 source,
364 &PathSourceUrl::from(dist),
365 &cache_shard,
366 tags,
367 hashes,
368 )
369 .boxed_local()
370 .await?
371 }
372 BuildableSource::Url(SourceUrl::Direct(resource)) => {
373 let cache_shard = self.build_context.cache().shard(
375 CacheBucket::SourceDistributions,
376 WheelCache::Url(resource.url).root(),
377 );
378
379 self.url(
380 source,
381 resource.url,
382 None,
383 &cache_shard,
384 resource.subdirectory,
385 resource.ext,
386 tags,
387 hashes,
388 client,
389 )
390 .boxed_local()
391 .await?
392 }
393 BuildableSource::Url(SourceUrl::GitDirectory(resource)) => {
394 self.git_source_tree(source, resource, tags, hashes, client)
395 .boxed_local()
396 .await?
397 }
398 BuildableSource::Url(SourceUrl::GitPath(resource)) => {
399 self.git_archive(source, resource, tags, hashes, client)
400 .boxed_local()
401 .await?
402 }
403 BuildableSource::Url(SourceUrl::Directory(resource)) => {
404 self.source_tree(source, resource, tags, hashes)
405 .boxed_local()
406 .await?
407 }
408 BuildableSource::Url(SourceUrl::Path(resource)) => {
409 let cache_shard = self.build_context.cache().shard(
410 CacheBucket::SourceDistributions,
411 WheelCache::Path(resource.url).root(),
412 );
413 self.archive(source, resource, &cache_shard, tags, hashes)
414 .boxed_local()
415 .await?
416 }
417 };
418
419 Ok(built_wheel_metadata)
420 }
421
422 pub(crate) async fn download_and_build_metadata(
426 &self,
427 source: &BuildableSource<'_>,
428 hashes: HashPolicy<'_>,
429 client: &ManagedClient<'_>,
430 ) -> Result<ArchiveMetadata, Error> {
431 let metadata = match &source {
432 BuildableSource::Dist(SourceDist::Registry(dist)) => {
433 let cache_shard = self.build_context.cache().shard(
435 CacheBucket::SourceDistributions,
436 WheelCache::Index(&dist.index)
437 .wheel_dir(dist.name.as_ref())
438 .join(dist.version.to_string()),
439 );
440
441 let url = dist.file.url.to_url()?;
442
443 if url.scheme() == "file" {
445 let path = url
446 .to_file_path()
447 .map_err(|()| Error::NonFileUrl(url.clone()))?;
448 return self
449 .archive_metadata(
450 source,
451 &PathSourceUrl {
452 url: &url,
453 path: Cow::Owned(path),
454 ext: dist.ext,
455 },
456 &cache_shard,
457 hashes,
458 )
459 .boxed_local()
460 .await;
461 }
462
463 self.url_metadata(
464 source,
465 &url,
466 Some(&dist.index),
467 &cache_shard,
468 None,
469 dist.ext,
470 hashes,
471 client,
472 )
473 .boxed_local()
474 .await?
475 }
476 BuildableSource::Dist(SourceDist::DirectUrl(dist)) => {
477 let cache_shard = self.build_context.cache().shard(
479 CacheBucket::SourceDistributions,
480 WheelCache::Url(&dist.url).root(),
481 );
482
483 self.url_metadata(
484 source,
485 &dist.url,
486 None,
487 &cache_shard,
488 dist.subdirectory.as_deref(),
489 dist.ext,
490 hashes,
491 client,
492 )
493 .boxed_local()
494 .await?
495 }
496 BuildableSource::Dist(SourceDist::GitDirectory(dist)) => {
497 self.git_source_tree_metadata(
498 source,
499 &GitDirectorySourceUrl::from(dist),
500 hashes,
501 client,
502 client.unmanaged.credentials_cache(),
503 )
504 .boxed_local()
505 .await?
506 }
507 BuildableSource::Dist(SourceDist::GitPath(dist)) => {
508 self.git_archive_metadata(source, &GitPathSourceUrl::from(dist), hashes, client)
509 .boxed_local()
510 .await?
511 }
512 BuildableSource::Dist(SourceDist::Directory(dist)) => {
513 self.source_tree_metadata(
514 source,
515 &DirectorySourceUrl::from(dist),
516 hashes,
517 client.unmanaged.credentials_cache(),
518 )
519 .boxed_local()
520 .await?
521 }
522 BuildableSource::Dist(SourceDist::Path(dist)) => {
523 let cache_shard = self.build_context.cache().shard(
524 CacheBucket::SourceDistributions,
525 WheelCache::Path(&dist.url).root(),
526 );
527 self.archive_metadata(source, &PathSourceUrl::from(dist), &cache_shard, hashes)
528 .boxed_local()
529 .await?
530 }
531 BuildableSource::Url(SourceUrl::Direct(resource)) => {
532 let cache_shard = self.build_context.cache().shard(
534 CacheBucket::SourceDistributions,
535 WheelCache::Url(resource.url).root(),
536 );
537
538 self.url_metadata(
539 source,
540 resource.url,
541 None,
542 &cache_shard,
543 resource.subdirectory,
544 resource.ext,
545 hashes,
546 client,
547 )
548 .boxed_local()
549 .await?
550 }
551 BuildableSource::Url(SourceUrl::GitDirectory(resource)) => {
552 self.git_source_tree_metadata(
553 source,
554 resource,
555 hashes,
556 client,
557 client.unmanaged.credentials_cache(),
558 )
559 .boxed_local()
560 .await?
561 }
562 BuildableSource::Url(SourceUrl::GitPath(resource)) => {
563 self.git_archive_metadata(source, resource, hashes, client)
564 .boxed_local()
565 .await?
566 }
567 BuildableSource::Url(SourceUrl::Directory(resource)) => {
568 self.source_tree_metadata(
569 source,
570 resource,
571 hashes,
572 client.unmanaged.credentials_cache(),
573 )
574 .boxed_local()
575 .await?
576 }
577 BuildableSource::Url(SourceUrl::Path(resource)) => {
578 let cache_shard = self.build_context.cache().shard(
579 CacheBucket::SourceDistributions,
580 WheelCache::Path(resource.url).root(),
581 );
582 self.archive_metadata(source, resource, &cache_shard, hashes)
583 .boxed_local()
584 .await?
585 }
586 };
587
588 Ok(metadata)
589 }
590
591 fn config_settings_for(&self, name: Option<&PackageName>) -> Cow<'_, ConfigSettings> {
593 if let Some(name) = name {
594 if let Some(package_settings) = self.build_context.config_settings_package().get(name) {
595 Cow::Owned(
596 package_settings
597 .clone()
598 .merge(self.build_context.config_settings().clone()),
599 )
600 } else {
601 Cow::Borrowed(self.build_context.config_settings())
602 }
603 } else {
604 Cow::Borrowed(self.build_context.config_settings())
605 }
606 }
607
608 fn extra_build_dependencies_for(&self, name: Option<&PackageName>) -> &[ExtraBuildRequirement] {
610 name.and_then(|name| {
611 self.build_context
612 .extra_build_requires()
613 .get(name)
614 .map(Vec::as_slice)
615 })
616 .unwrap_or(&[])
617 }
618
619 fn extra_build_variables_for(&self, name: Option<&PackageName>) -> Option<&BuildVariables> {
621 name.and_then(|name| self.build_context.extra_build_variables().get(name))
622 }
623
624 async fn url<'data>(
626 &self,
627 source: &BuildableSource<'data>,
628 url: &'data DisplaySafeUrl,
629 index: Option<&'data IndexUrl>,
630 cache_shard: &CacheShard,
631 subdirectory: Option<&'data Path>,
632 ext: SourceDistExtension,
633 tags: &Tags,
634 hashes: HashPolicy<'_>,
635 client: &ManagedClient<'_>,
636 ) -> Result<BuiltWheelMetadata, Error> {
637 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
638
639 let revision = self
641 .url_revision(source, ext, url, index, cache_shard, hashes, client)
642 .await?;
643
644 if !revision.satisfies(hashes) {
646 return Err(Error::hash_mismatch(
647 source.to_string(),
648 hashes.digests(),
649 revision.hashes(),
650 ));
651 }
652
653 let cache_shard = cache_shard.shard(revision.id());
656 let source_dist_entry = cache_shard.entry(SOURCE);
657
658 let cache_info = CacheInfo::default();
661
662 let config_settings = self.config_settings_for(source.name());
664 let extra_build_deps = self.extra_build_dependencies_for(source.name());
665 let extra_build_variables = self.extra_build_variables_for(source.name());
666 let build_info = BuildInfo::from_settings(
667 config_settings.into_owned(),
668 extra_build_deps.to_vec(),
669 extra_build_variables.cloned(),
670 );
671 let cache_shard = build_info
672 .cache_shard()
673 .map(|digest| cache_shard.shard(digest))
674 .unwrap_or(cache_shard);
675
676 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
678 .ok()
679 .flatten()
680 .filter(|file| file.matches(source.name(), source.version()))
681 {
682 return Ok(BuiltWheelMetadata::from_file(
683 file,
684 revision.into_hashes(),
685 cache_info,
686 build_info,
687 ));
688 }
689
690 let revision = if source_dist_entry.path().is_dir() {
692 revision
693 } else {
694 self.heal_url_revision(
695 source,
696 ext,
697 url,
698 index,
699 &source_dist_entry,
700 revision,
701 hashes,
702 client,
703 )
704 .await?
705 };
706
707 if let Some(subdirectory) = subdirectory {
709 if !source_dist_entry.path().join(subdirectory).is_dir() {
710 return Err(Error::MissingSubdirectory(
711 url.clone(),
712 subdirectory.to_path_buf(),
713 ));
714 }
715 }
716
717 let task = self
718 .reporter
719 .as_ref()
720 .map(|reporter| reporter.on_build_start(source));
721
722 let (disk_filename, wheel_filename, metadata) = self
724 .build_distribution(
725 source,
726 source_dist_entry.path(),
727 subdirectory,
728 &cache_shard,
729 NoSources::None,
730 )
731 .await?;
732
733 if let Some(task) = task {
734 if let Some(reporter) = self.reporter.as_ref() {
735 reporter.on_build_complete(source, task);
736 }
737 }
738
739 let metadata_entry = cache_shard.entry(METADATA);
741 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
742 .await
743 .map_err(Error::CacheWrite)?;
744
745 Ok(BuiltWheelMetadata {
746 path: cache_shard.join(&disk_filename).into_boxed_path(),
747 target: cache_shard.join(wheel_filename.stem()).into_boxed_path(),
748 filename: wheel_filename,
749 hashes: revision.into_hashes(),
750 cache_info,
751 build_info,
752 })
753 }
754
755 async fn url_metadata<'data>(
760 &self,
761 source: &BuildableSource<'data>,
762 url: &'data DisplaySafeUrl,
763 index: Option<&'data IndexUrl>,
764 cache_shard: &CacheShard,
765 subdirectory: Option<&'data Path>,
766 ext: SourceDistExtension,
767 hashes: HashPolicy<'_>,
768 client: &ManagedClient<'_>,
769 ) -> Result<ArchiveMetadata, Error> {
770 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
771
772 let revision = self
774 .url_revision(source, ext, url, index, cache_shard, hashes, client)
775 .await?;
776
777 if !revision.satisfies(hashes) {
779 return Err(Error::hash_mismatch(
780 source.to_string(),
781 hashes.digests(),
782 revision.hashes(),
783 ));
784 }
785
786 let cache_shard = cache_shard.shard(revision.id());
789 let source_dist_entry = cache_shard.entry(SOURCE);
790
791 let dynamic =
793 match StaticMetadata::read(source, source_dist_entry.path(), subdirectory).await? {
794 StaticMetadata::Some(metadata) => {
795 return Ok(ArchiveMetadata {
796 metadata: Metadata::from_metadata23(metadata),
797 hashes: revision.into_hashes(),
798 });
799 }
800 StaticMetadata::Dynamic => true,
801 StaticMetadata::None => false,
802 };
803
804 let metadata_entry = cache_shard.entry(METADATA);
806 match CachedMetadata::read(&metadata_entry).await {
807 Ok(Some(metadata)) => {
808 if metadata.matches(source.name(), source.version()) {
809 debug!("Using cached metadata for: {source}");
810 return Ok(ArchiveMetadata {
811 metadata: Metadata::from_metadata23(metadata.into()),
812 hashes: revision.into_hashes(),
813 });
814 }
815 debug!("Cached metadata does not match expected name and version for: {source}");
816 }
817 Ok(None) => {}
818 Err(err) => {
819 debug!("Failed to deserialize cached metadata for: {source} ({err})");
820 }
821 }
822
823 let revision = if source_dist_entry.path().is_dir() {
825 revision
826 } else {
827 self.heal_url_revision(
828 source,
829 ext,
830 url,
831 index,
832 &source_dist_entry,
833 revision,
834 hashes,
835 client,
836 )
837 .await?
838 };
839
840 if let Some(subdirectory) = subdirectory {
842 if !source_dist_entry.path().join(subdirectory).is_dir() {
843 return Err(Error::MissingSubdirectory(
844 url.clone(),
845 subdirectory.to_path_buf(),
846 ));
847 }
848 }
849
850 if let Some(metadata) = self
853 .build_metadata(
854 source,
855 source_dist_entry.path(),
856 subdirectory,
857 NoSources::None,
858 )
859 .boxed_local()
860 .await?
861 {
862 let metadata = if dynamic {
864 ResolutionMetadata {
865 dynamic: true,
866 ..metadata
867 }
868 } else {
869 metadata
870 };
871
872 fs::create_dir_all(metadata_entry.dir())
874 .await
875 .map_err(Error::CacheWrite)?;
876 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
877 .await
878 .map_err(Error::CacheWrite)?;
879
880 return Ok(ArchiveMetadata {
881 metadata: Metadata::from_metadata23(metadata),
882 hashes: revision.into_hashes(),
883 });
884 }
885
886 let config_settings = self.config_settings_for(source.name());
888 let extra_build_deps = self.extra_build_dependencies_for(source.name());
889 let extra_build_variables = self.extra_build_variables_for(source.name());
890 let build_info = BuildInfo::from_settings(
891 config_settings.into_owned(),
892 extra_build_deps.to_vec(),
893 extra_build_variables.cloned(),
894 );
895 let cache_shard = build_info
896 .cache_shard()
897 .map(|digest| cache_shard.shard(digest))
898 .unwrap_or(cache_shard);
899
900 let task = self
901 .reporter
902 .as_ref()
903 .map(|reporter| reporter.on_build_start(source));
904
905 let (_disk_filename, _wheel_filename, metadata) = self
907 .build_distribution(
908 source,
909 source_dist_entry.path(),
910 subdirectory,
911 &cache_shard,
912 NoSources::None,
913 )
914 .await?;
915
916 if let Some(task) = task {
917 if let Some(reporter) = self.reporter.as_ref() {
918 reporter.on_build_complete(source, task);
919 }
920 }
921
922 let metadata = if dynamic {
924 ResolutionMetadata {
925 dynamic: true,
926 ..metadata
927 }
928 } else {
929 metadata
930 };
931
932 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
934 .await
935 .map_err(Error::CacheWrite)?;
936
937 Ok(ArchiveMetadata {
938 metadata: Metadata::from_metadata23(metadata),
939 hashes: revision.into_hashes(),
940 })
941 }
942
943 async fn url_revision(
945 &self,
946 source: &BuildableSource<'_>,
947 ext: SourceDistExtension,
948 url: &DisplaySafeUrl,
949 index: Option<&IndexUrl>,
950 cache_shard: &CacheShard,
951 hashes: HashPolicy<'_>,
952 client: &ManagedClient<'_>,
953 ) -> Result<Revision, Error> {
954 let cache_entry = cache_shard.entry(HTTP_REVISION);
955
956 let cache_control = match client.unmanaged.connectivity() {
958 Connectivity::Online
959 if let Some(header) = index.and_then(|index| {
960 self.build_context
961 .locations()
962 .artifact_cache_control_for(index)
963 }) =>
964 {
965 CacheControl::Override(header)
966 }
967 Connectivity::Online => CacheControl::from(
968 self.build_context
969 .cache()
970 .freshness(&cache_entry, source.name(), source.source_tree())
971 .map_err(Error::CacheRead)?,
972 ),
973 Connectivity::Offline => CacheControl::AllowStale,
974 };
975
976 let download = |response| {
977 async {
978 let revision = Revision::new();
981
982 debug!("Downloading source distribution: {source}");
984 let entry = cache_shard.shard(revision.id()).entry(SOURCE);
985 let (hashes, size) = self
986 .download_archive(response, source, ext, entry.path(), hashes, &[])
987 .await?;
988
989 Ok(revision
990 .with_hashes(HashDigests::from(hashes))
991 .with_size(size))
992 }
993 .boxed_local()
994 .instrument(info_span!("download", source_dist = %source))
995 };
996 let req = Self::request(url.clone(), client.unmanaged)?;
997 let revision = client
998 .managed(|client| {
999 client.cached_client().get_serde_with_retry(
1000 req,
1001 &cache_entry,
1002 cache_control.clone(),
1003 download,
1004 )
1005 })
1006 .await
1007 .map_err(|err| match err {
1008 CachedClientError::Callback { err, .. } => err,
1009 CachedClientError::Client(err) => Error::Client(err),
1010 })?;
1011
1012 let expected_size = match source {
1013 BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => {
1014 dist.size()
1015 }
1016 BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(),
1017 _ => None,
1018 };
1019 if let (Some(expected), Some(actual)) = (expected_size, revision.size())
1020 && expected != actual
1021 {
1022 return Err(Error::MismatchedSize {
1023 distribution: source.to_string(),
1024 expected,
1025 actual,
1026 });
1027 }
1028
1029 if revision.has_digests(hashes) && (expected_size.is_none() || revision.size().is_some()) {
1031 Ok(revision)
1032 } else {
1033 client
1034 .managed(async |client| {
1035 client
1036 .cached_client()
1037 .skip_cache_with_retry(
1038 Self::request(url.clone(), client)?,
1039 &cache_entry,
1040 cache_control,
1041 download,
1042 )
1043 .await
1044 .map_err(|err| match err {
1045 CachedClientError::Callback { err, .. } => err,
1046 CachedClientError::Client(err) => Error::Client(err),
1047 })
1048 })
1049 .await
1050 }
1051 }
1052
1053 async fn archive(
1055 &self,
1056 source: &BuildableSource<'_>,
1057 resource: &PathSourceUrl<'_>,
1058 cache_shard: &CacheShard,
1059 tags: &Tags,
1060 hashes: HashPolicy<'_>,
1061 ) -> Result<BuiltWheelMetadata, Error> {
1062 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1063
1064 let LocalRevisionPointer {
1066 cache_info,
1067 revision,
1068 } = self
1069 .archive_revision(source, resource, cache_shard, hashes)
1070 .await?;
1071
1072 if !revision.satisfies(hashes) {
1074 return Err(Error::hash_mismatch(
1075 source.to_string(),
1076 hashes.digests(),
1077 revision.hashes(),
1078 ));
1079 }
1080
1081 let cache_shard = cache_shard.shard(revision.id());
1084 let source_entry = cache_shard.entry(SOURCE);
1085
1086 let config_settings = self.config_settings_for(source.name());
1088 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1089 let extra_build_variables = self.extra_build_variables_for(source.name());
1090 let build_info = BuildInfo::from_settings(
1091 config_settings.into_owned(),
1092 extra_build_deps.to_vec(),
1093 extra_build_variables.cloned(),
1094 );
1095 let cache_shard = build_info
1096 .cache_shard()
1097 .map(|digest| cache_shard.shard(digest))
1098 .unwrap_or(cache_shard);
1099
1100 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1102 .ok()
1103 .flatten()
1104 .filter(|file| file.matches(source.name(), source.version()))
1105 {
1106 return Ok(BuiltWheelMetadata::from_file(
1107 file,
1108 revision.into_hashes(),
1109 cache_info,
1110 build_info,
1111 ));
1112 }
1113
1114 let revision = if source_entry.path().is_dir() {
1116 revision
1117 } else {
1118 self.heal_archive_revision(source, resource, &source_entry, revision, hashes)
1119 .await?
1120 };
1121
1122 let task = self
1123 .reporter
1124 .as_ref()
1125 .map(|reporter| reporter.on_build_start(source));
1126
1127 let (disk_filename, filename, metadata) = self
1128 .build_distribution(
1129 source,
1130 source_entry.path(),
1131 None,
1132 &cache_shard,
1133 NoSources::None,
1134 )
1135 .await?;
1136
1137 if let Some(task) = task {
1138 if let Some(reporter) = self.reporter.as_ref() {
1139 reporter.on_build_complete(source, task);
1140 }
1141 }
1142
1143 let metadata_entry = cache_shard.entry(METADATA);
1145 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1146 .await
1147 .map_err(Error::CacheWrite)?;
1148
1149 Ok(BuiltWheelMetadata {
1150 path: cache_shard.join(&disk_filename).into_boxed_path(),
1151 target: cache_shard.join(filename.stem()).into_boxed_path(),
1152 filename,
1153 hashes: revision.into_hashes(),
1154 cache_info,
1155 build_info,
1156 })
1157 }
1158
1159 async fn archive_metadata(
1164 &self,
1165 source: &BuildableSource<'_>,
1166 resource: &PathSourceUrl<'_>,
1167 cache_shard: &CacheShard,
1168 hashes: HashPolicy<'_>,
1169 ) -> Result<ArchiveMetadata, Error> {
1170 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1171
1172 let LocalRevisionPointer { revision, .. } = self
1174 .archive_revision(source, resource, cache_shard, hashes)
1175 .await?;
1176
1177 if !revision.satisfies(hashes) {
1179 return Err(Error::hash_mismatch(
1180 source.to_string(),
1181 hashes.digests(),
1182 revision.hashes(),
1183 ));
1184 }
1185
1186 let cache_shard = cache_shard.shard(revision.id());
1189 let source_entry = cache_shard.entry(SOURCE);
1190
1191 let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
1193 StaticMetadata::Some(metadata) => {
1194 return Ok(ArchiveMetadata {
1195 metadata: Metadata::from_metadata23(metadata),
1196 hashes: revision.into_hashes(),
1197 });
1198 }
1199 StaticMetadata::Dynamic => true,
1200 StaticMetadata::None => false,
1201 };
1202
1203 let metadata_entry = cache_shard.entry(METADATA);
1205 match CachedMetadata::read(&metadata_entry).await {
1206 Ok(Some(metadata)) => {
1207 if metadata.matches(source.name(), source.version()) {
1208 debug!("Using cached metadata for: {source}");
1209 return Ok(ArchiveMetadata {
1210 metadata: Metadata::from_metadata23(metadata.into()),
1211 hashes: revision.into_hashes(),
1212 });
1213 }
1214 debug!("Cached metadata does not match expected name and version for: {source}");
1215 }
1216 Ok(None) => {}
1217 Err(err) => {
1218 debug!("Failed to deserialize cached metadata for: {source} ({err})");
1219 }
1220 }
1221
1222 let revision = if source_entry.path().is_dir() {
1224 revision
1225 } else {
1226 self.heal_archive_revision(source, resource, &source_entry, revision, hashes)
1227 .await?
1228 };
1229
1230 if let Some(metadata) = self
1232 .build_metadata(source, source_entry.path(), None, NoSources::None)
1233 .boxed_local()
1234 .await?
1235 {
1236 let metadata = if dynamic {
1238 ResolutionMetadata {
1239 dynamic: true,
1240 ..metadata
1241 }
1242 } else {
1243 metadata
1244 };
1245
1246 fs::create_dir_all(metadata_entry.dir())
1248 .await
1249 .map_err(Error::CacheWrite)?;
1250 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1251 .await
1252 .map_err(Error::CacheWrite)?;
1253
1254 return Ok(ArchiveMetadata {
1255 metadata: Metadata::from_metadata23(metadata),
1256 hashes: revision.into_hashes(),
1257 });
1258 }
1259
1260 let config_settings = self.config_settings_for(source.name());
1262 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1263 let extra_build_variables = self.extra_build_variables_for(source.name());
1264 let build_info = BuildInfo::from_settings(
1265 config_settings.into_owned(),
1266 extra_build_deps.to_vec(),
1267 extra_build_variables.cloned(),
1268 );
1269 let cache_shard = build_info
1270 .cache_shard()
1271 .map(|digest| cache_shard.shard(digest))
1272 .unwrap_or(cache_shard);
1273
1274 let task = self
1276 .reporter
1277 .as_ref()
1278 .map(|reporter| reporter.on_build_start(source));
1279
1280 let (_disk_filename, _filename, metadata) = self
1281 .build_distribution(
1282 source,
1283 source_entry.path(),
1284 None,
1285 &cache_shard,
1286 NoSources::None,
1287 )
1288 .await?;
1289
1290 if let Some(task) = task {
1291 if let Some(reporter) = self.reporter.as_ref() {
1292 reporter.on_build_complete(source, task);
1293 }
1294 }
1295
1296 let metadata = if dynamic {
1298 ResolutionMetadata {
1299 dynamic: true,
1300 ..metadata
1301 }
1302 } else {
1303 metadata
1304 };
1305
1306 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1308 .await
1309 .map_err(Error::CacheWrite)?;
1310
1311 Ok(ArchiveMetadata {
1312 metadata: Metadata::from_metadata23(metadata),
1313 hashes: revision.into_hashes(),
1314 })
1315 }
1316
1317 async fn archive_revision(
1319 &self,
1320 source: &BuildableSource<'_>,
1321 resource: &PathSourceUrl<'_>,
1322 cache_shard: &CacheShard,
1323 hashes: HashPolicy<'_>,
1324 ) -> Result<LocalRevisionPointer, Error> {
1325 if !resource.path.is_file() {
1327 return Err(Error::NotFound(resource.url.clone()));
1328 }
1329
1330 let cache_info = CacheInfo::from_file(&resource.path).map_err(Error::CacheRead)?;
1332
1333 let revision_entry = cache_shard.entry(LOCAL_REVISION);
1335
1336 if let Some(pointer) = LocalRevisionPointer::read_from(&revision_entry)? {
1339 if *pointer.cache_info() == cache_info {
1340 if pointer.revision().has_digests(hashes) {
1341 return Ok(pointer);
1342 }
1343 }
1344 }
1345
1346 let revision = Revision::new();
1348
1349 debug!("Unpacking source distribution: {source}");
1351 let entry = cache_shard.shard(revision.id()).entry(SOURCE);
1352 let hashes = self
1353 .persist_archive(
1354 source,
1355 &resource.path,
1356 resource.ext,
1357 entry.path(),
1358 hashes,
1359 &[],
1360 )
1361 .await?;
1362
1363 let revision = revision.with_hashes(HashDigests::from(hashes));
1365
1366 let pointer = LocalRevisionPointer {
1368 cache_info,
1369 revision,
1370 };
1371 pointer.write_to(&revision_entry).await?;
1372
1373 Ok(pointer)
1374 }
1375
1376 async fn source_tree(
1379 &self,
1380 source: &BuildableSource<'_>,
1381 resource: &DirectorySourceUrl<'_>,
1382 tags: &Tags,
1383 hashes: HashPolicy<'_>,
1384 ) -> Result<BuiltWheelMetadata, Error> {
1385 if hashes.requires_validation() {
1387 return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
1388 }
1389
1390 let cache_shard = self.build_context.cache().shard(
1391 CacheBucket::SourceDistributions,
1392 if resource.editable.unwrap_or(false) {
1393 WheelCache::Editable(resource.url).root()
1394 } else {
1395 WheelCache::Path(resource.url).root()
1396 },
1397 );
1398
1399 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1401
1402 let LocalRevisionPointer {
1404 cache_info,
1405 revision,
1406 } = self
1407 .source_tree_revision(source, resource, &cache_shard)
1408 .await?;
1409
1410 let cache_shard = cache_shard.shard(revision.id());
1413
1414 let config_settings = self.config_settings_for(source.name());
1416 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1417 let extra_build_variables = self.extra_build_variables_for(source.name());
1418 let build_info = BuildInfo::from_settings(
1419 config_settings.into_owned(),
1420 extra_build_deps.to_vec(),
1421 extra_build_variables.cloned(),
1422 );
1423 let cache_shard = build_info
1424 .cache_shard()
1425 .map(|digest| cache_shard.shard(digest))
1426 .unwrap_or(cache_shard);
1427
1428 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1430 .ok()
1431 .flatten()
1432 .filter(|file| file.matches(source.name(), source.version()))
1433 {
1434 return Ok(BuiltWheelMetadata::from_file(
1435 file,
1436 revision.into_hashes(),
1437 cache_info,
1438 build_info,
1439 ));
1440 }
1441
1442 let task = self
1444 .reporter
1445 .as_ref()
1446 .map(|reporter| reporter.on_build_start(source));
1447
1448 let (disk_filename, filename, metadata) = self
1449 .build_distribution(
1450 source,
1451 resource.install_path,
1452 None,
1453 &cache_shard,
1454 self.build_context.sources().clone(),
1455 )
1456 .await?;
1457
1458 if let Some(task) = task {
1459 if let Some(reporter) = self.reporter.as_ref() {
1460 reporter.on_build_complete(source, task);
1461 }
1462 }
1463
1464 let metadata_entry = cache_shard.entry(METADATA);
1466 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1467 .await
1468 .map_err(Error::CacheWrite)?;
1469
1470 Ok(BuiltWheelMetadata {
1471 path: cache_shard.join(&disk_filename).into_boxed_path(),
1472 target: cache_shard.join(filename.stem()).into_boxed_path(),
1473 filename,
1474 hashes: revision.into_hashes(),
1475 cache_info,
1476 build_info,
1477 })
1478 }
1479
1480 async fn source_tree_metadata(
1486 &self,
1487 source: &BuildableSource<'_>,
1488 resource: &DirectorySourceUrl<'_>,
1489 hashes: HashPolicy<'_>,
1490 credentials_cache: &CredentialsCache,
1491 ) -> Result<ArchiveMetadata, Error> {
1492 if hashes.requires_validation() {
1494 return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
1495 }
1496
1497 let editable = self
1501 .build_context
1502 .source_tree_editable_policy()
1503 .workspace_member_editable(resource.editable);
1504
1505 let dynamic = match StaticMetadata::read(source, resource.install_path, None).await? {
1507 StaticMetadata::Some(metadata) => {
1508 return Ok(ArchiveMetadata::from(
1509 Metadata::from_workspace(
1510 metadata,
1511 resource.install_path,
1512 None,
1513 self.build_context.locations(),
1514 self.build_context.sources().clone(),
1515 editable,
1516 self.build_context.cache(),
1517 self.build_context.workspace_cache(),
1518 credentials_cache,
1519 )
1520 .await?,
1521 ));
1522 }
1523 StaticMetadata::Dynamic => true,
1524 StaticMetadata::None => false,
1525 };
1526
1527 let cache_shard = self.build_context.cache().shard(
1528 CacheBucket::SourceDistributions,
1529 if resource.editable.unwrap_or(false) {
1530 WheelCache::Editable(resource.url).root()
1531 } else {
1532 WheelCache::Path(resource.url).root()
1533 },
1534 );
1535
1536 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1538
1539 let LocalRevisionPointer { revision, .. } = self
1541 .source_tree_revision(source, resource, &cache_shard)
1542 .await?;
1543
1544 let cache_shard = cache_shard.shard(revision.id());
1547
1548 let metadata_entry = cache_shard.entry(METADATA);
1550 match CachedMetadata::read(&metadata_entry).await {
1551 Ok(Some(metadata)) => {
1552 if metadata.matches(source.name(), source.version()) {
1553 debug!("Using cached metadata for: {source}");
1554
1555 let metadata = if dynamic {
1557 ResolutionMetadata {
1558 dynamic: true,
1559 ..metadata.into()
1560 }
1561 } else {
1562 metadata.into()
1563 };
1564 return Ok(ArchiveMetadata::from(
1565 Metadata::from_workspace(
1566 metadata,
1567 resource.install_path,
1568 None,
1569 self.build_context.locations(),
1570 self.build_context.sources().clone(),
1571 editable,
1572 self.build_context.cache(),
1573 self.build_context.workspace_cache(),
1574 credentials_cache,
1575 )
1576 .await?,
1577 ));
1578 }
1579 debug!("Cached metadata does not match expected name and version for: {source}");
1580 }
1581 Ok(None) => {}
1582 Err(err) => {
1583 debug!("Failed to deserialize cached metadata for: {source} ({err})");
1584 }
1585 }
1586
1587 if let Some(metadata) = self
1589 .build_metadata(
1590 source,
1591 resource.install_path,
1592 None,
1593 self.build_context.sources().clone(),
1594 )
1595 .boxed_local()
1596 .await?
1597 {
1598 fs::create_dir_all(metadata_entry.dir())
1600 .await
1601 .map_err(Error::CacheWrite)?;
1602 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1603 .await
1604 .map_err(Error::CacheWrite)?;
1605
1606 let metadata = if dynamic {
1608 ResolutionMetadata {
1609 dynamic: true,
1610 ..metadata
1611 }
1612 } else {
1613 metadata
1614 };
1615
1616 return Ok(ArchiveMetadata::from(
1617 Metadata::from_workspace(
1618 metadata,
1619 resource.install_path,
1620 None,
1621 self.build_context.locations(),
1622 self.build_context.sources().clone(),
1623 editable,
1624 self.build_context.cache(),
1625 self.build_context.workspace_cache(),
1626 credentials_cache,
1627 )
1628 .await?,
1629 ));
1630 }
1631
1632 let config_settings = self.config_settings_for(source.name());
1634 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1635 let extra_build_variables = self.extra_build_variables_for(source.name());
1636 let build_info = BuildInfo::from_settings(
1637 config_settings.into_owned(),
1638 extra_build_deps.to_vec(),
1639 extra_build_variables.cloned(),
1640 );
1641 let cache_shard = build_info
1642 .cache_shard()
1643 .map(|digest| cache_shard.shard(digest))
1644 .unwrap_or(cache_shard);
1645
1646 let task = self
1648 .reporter
1649 .as_ref()
1650 .map(|reporter| reporter.on_build_start(source));
1651
1652 let (_disk_filename, _filename, metadata) = self
1653 .build_distribution(
1654 source,
1655 resource.install_path,
1656 None,
1657 &cache_shard,
1658 self.build_context.sources().clone(),
1659 )
1660 .await?;
1661
1662 if let Some(task) = task {
1663 if let Some(reporter) = self.reporter.as_ref() {
1664 reporter.on_build_complete(source, task);
1665 }
1666 }
1667
1668 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1670 .await
1671 .map_err(Error::CacheWrite)?;
1672
1673 let metadata = if dynamic {
1675 ResolutionMetadata {
1676 dynamic: true,
1677 ..metadata
1678 }
1679 } else {
1680 metadata
1681 };
1682
1683 Ok(ArchiveMetadata::from(
1684 Metadata::from_workspace(
1685 metadata,
1686 resource.install_path,
1687 None,
1688 self.build_context.locations(),
1689 self.build_context.sources().clone(),
1690 editable,
1691 self.build_context.cache(),
1692 self.build_context.workspace_cache(),
1693 credentials_cache,
1694 )
1695 .await?,
1696 ))
1697 }
1698
1699 async fn source_tree_revision(
1701 &self,
1702 source: &BuildableSource<'_>,
1703 resource: &DirectorySourceUrl<'_>,
1704 cache_shard: &CacheShard,
1705 ) -> Result<LocalRevisionPointer, Error> {
1706 if !resource.install_path.is_dir() {
1708 return Err(Error::NotFound(resource.url.clone()));
1709 }
1710
1711 let cache_info = CacheInfo::from_directory(resource.install_path)?;
1713
1714 let entry = cache_shard.entry(LOCAL_REVISION);
1716
1717 if self
1719 .build_context
1720 .cache()
1721 .freshness(&entry, source.name(), source.source_tree())
1722 .map_err(Error::CacheRead)?
1723 .is_fresh()
1724 {
1725 match LocalRevisionPointer::read_from(&entry) {
1726 Ok(Some(pointer)) => {
1727 if *pointer.cache_info() == cache_info {
1728 return Ok(pointer);
1729 }
1730
1731 debug!("Cached revision does not match expected cache info for: {source}");
1732 }
1733 Ok(None) => {}
1734 Err(err) => {
1735 debug!("Failed to deserialize cached revision for: {source} ({err})");
1736 }
1737 }
1738 }
1739
1740 let revision = Revision::new();
1742 let pointer = LocalRevisionPointer {
1743 cache_info,
1744 revision,
1745 };
1746 pointer.write_to(&entry).await?;
1747
1748 Ok(pointer)
1749 }
1750
1751 pub(crate) async fn source_tree_requires_dist(
1753 &self,
1754 path: &Path,
1755 pyproject_toml: &PyProjectToml,
1756 credentials_cache: &CredentialsCache,
1757 ) -> Result<Option<RequiresDist>, Error> {
1758 match uv_pypi_types::RequiresDist::from_pyproject_toml(pyproject_toml.clone()) {
1760 Ok(requires_dist) => {
1761 debug!("Found static `requires-dist` for: {}", path.display());
1762 let requires_dist = RequiresDist::from_project_maybe_workspace(
1763 requires_dist,
1764 path,
1765 None,
1766 self.build_context.locations(),
1767 self.build_context.sources().clone(),
1768 self.build_context
1769 .source_tree_editable_policy()
1770 .workspace_member_editable(None),
1771 self.build_context.cache(),
1772 self.build_context.workspace_cache(),
1773 credentials_cache,
1774 )
1775 .await?;
1776 Ok(Some(requires_dist))
1777 }
1778 Err(
1779 err @ (uv_pypi_types::MetadataError::Pep508Error(_)
1780 | uv_pypi_types::MetadataError::DynamicField(_)
1781 | uv_pypi_types::MetadataError::FieldNotFound(_)
1782 | uv_pypi_types::MetadataError::PoetrySyntax),
1783 ) => {
1784 debug!(
1785 "No static `requires-dist` available for: {} ({err:?})",
1786 path.display()
1787 );
1788 Ok(None)
1789 }
1790 Err(err) => Err(Error::PyprojectToml(err)),
1791 }
1792 }
1793
1794 async fn git_archive_revision(
1796 &self,
1797 source: &BuildableSource<'_>,
1798 resource: &GitPathSourceUrl<'_>,
1799 fetch: &Fetch,
1800 cache_shard: &CacheShard,
1801 hashes: HashPolicy<'_>,
1802 ) -> Result<RevisionHashes, Error> {
1803 if resource.git.lfs().enabled() && !fetch.lfs_ready() {
1805 if GIT_LFS.is_err() {
1806 return Err(Error::MissingSourceDistGitLfsArtifacts(
1807 resource.url.to_url(),
1808 GitError::GitLfsNotFound,
1809 ));
1810 }
1811 return Err(Error::MissingSourceDistGitLfsArtifacts(
1812 resource.url.to_url(),
1813 GitError::GitLfsNotConfigured,
1814 ));
1815 }
1816
1817 let install_path = fetch.path().join(&resource.path);
1819 if !install_path.is_file() {
1820 return Err(Error::NotFound(resource.url.to_url()));
1821 }
1822
1823 let revision_entry = cache_shard.entry(HASHES);
1825
1826 if let Some(revision) = RevisionHashes::read_from(&revision_entry)? {
1829 if revision.has_digests(hashes) {
1830 return Ok(revision);
1831 }
1832 }
1833
1834 debug!("Unpacking source distribution: {source}");
1836 let entry = cache_shard.entry(SOURCE);
1837 let hashes = self
1838 .persist_archive(
1839 source,
1840 &install_path,
1841 resource.ext,
1842 entry.path(),
1843 hashes,
1844 &[],
1845 )
1846 .await?;
1847
1848 let revision = RevisionHashes { hashes };
1850 revision.write_to(&revision_entry).await?;
1851
1852 Ok(revision)
1853 }
1854
1855 async fn git_archive(
1857 &self,
1858 source: &BuildableSource<'_>,
1859 resource: &GitPathSourceUrl<'_>,
1860 tags: &Tags,
1861 hashes: HashPolicy<'_>,
1862 client: &ManagedClient<'_>,
1863 ) -> Result<BuiltWheelMetadata, Error> {
1864 let fetch = self
1866 .build_context
1867 .git()
1868 .fetch(
1869 resource.git,
1870 client.unmanaged.git_http_settings(resource.git.url()),
1871 self.build_context.cache().bucket(CacheBucket::Git),
1872 self.reporter
1873 .clone()
1874 .map(|reporter| reporter.into_git_reporter()),
1875 )
1876 .await?;
1877
1878 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
1879 let cache_shard = self.build_context.cache().shard(
1880 CacheBucket::SourceDistributions,
1881 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
1882 );
1883
1884 let revision = self
1886 .git_archive_revision(source, resource, &fetch, &cache_shard, hashes)
1887 .await?;
1888
1889 if !revision.satisfies(hashes) {
1891 return Err(Error::hash_mismatch(
1892 source.to_string(),
1893 hashes.digests(),
1894 revision.hashes(),
1895 ));
1896 }
1897
1898 let source_entry = cache_shard.entry(SOURCE);
1899
1900 let config_settings = self.config_settings_for(source.name());
1902 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1903 let extra_build_variables = self.extra_build_variables_for(source.name());
1904 let build_info = BuildInfo::from_settings(
1905 config_settings.into_owned(),
1906 extra_build_deps.to_vec(),
1907 extra_build_variables.cloned(),
1908 );
1909 let cache_shard = build_info
1910 .cache_shard()
1911 .map(|digest| cache_shard.shard(digest))
1912 .unwrap_or(cache_shard);
1913
1914 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1916 .ok()
1917 .flatten()
1918 .filter(|file| file.matches(source.name(), source.version()))
1919 {
1920 return Ok(BuiltWheelMetadata::from_file(
1921 file,
1922 revision.into_hashes(),
1923 CacheInfo::default(),
1924 build_info,
1925 ));
1926 }
1927
1928 let task = self
1930 .reporter
1931 .as_ref()
1932 .map(|reporter| reporter.on_build_start(source));
1933
1934 let (disk_filename, filename, metadata) = self
1935 .build_distribution(
1936 source,
1937 source_entry.path(),
1938 None,
1939 &cache_shard,
1940 NoSources::None,
1941 )
1942 .await?;
1943
1944 if let Some(task) = task {
1945 if let Some(reporter) = self.reporter.as_ref() {
1946 reporter.on_build_complete(source, task);
1947 }
1948 }
1949
1950 let metadata_entry = cache_shard.entry(METADATA);
1952 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1953 .await
1954 .map_err(Error::CacheWrite)?;
1955
1956 Ok(BuiltWheelMetadata {
1957 path: cache_shard.join(&disk_filename).into_boxed_path(),
1958 target: cache_shard.join(filename.stem()).into_boxed_path(),
1959 filename,
1960 hashes: revision.into_hashes(),
1961 cache_info: CacheInfo::default(),
1962 build_info,
1963 })
1964 }
1965
1966 async fn git_archive_metadata(
1968 &self,
1969 source: &BuildableSource<'_>,
1970 resource: &GitPathSourceUrl<'_>,
1971 hashes: HashPolicy<'_>,
1972 client: &ManagedClient<'_>,
1973 ) -> Result<ArchiveMetadata, Error> {
1974 let fetch = self
1976 .build_context
1977 .git()
1978 .fetch(
1979 resource.git,
1980 client.unmanaged.git_http_settings(resource.git.url()),
1981 self.build_context.cache().bucket(CacheBucket::Git),
1982 self.reporter
1983 .clone()
1984 .map(|reporter| reporter.into_git_reporter()),
1985 )
1986 .await?;
1987
1988 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
1989 let cache_shard = self.build_context.cache().shard(
1990 CacheBucket::SourceDistributions,
1991 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
1992 );
1993
1994 let revision = self
1996 .git_archive_revision(source, resource, &fetch, &cache_shard, hashes)
1997 .await?;
1998
1999 if !revision.satisfies(hashes) {
2001 return Err(Error::hash_mismatch(
2002 source.to_string(),
2003 hashes.digests(),
2004 revision.hashes(),
2005 ));
2006 }
2007
2008 let source_entry = cache_shard.entry(SOURCE);
2009
2010 let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
2012 StaticMetadata::Some(metadata) => {
2013 return Ok(ArchiveMetadata {
2014 metadata: Metadata::from_metadata23(metadata),
2015 hashes: revision.into_hashes(),
2016 });
2017 }
2018 StaticMetadata::Dynamic => true,
2019 StaticMetadata::None => false,
2020 };
2021
2022 let metadata_entry = cache_shard.entry(METADATA);
2024 match CachedMetadata::read(&metadata_entry).await {
2025 Ok(Some(metadata)) => {
2026 if metadata.matches(source.name(), source.version()) {
2027 debug!("Using cached metadata for: {source}");
2028 return Ok(ArchiveMetadata {
2029 metadata: Metadata::from_metadata23(metadata.into()),
2030 hashes: revision.into_hashes(),
2031 });
2032 }
2033 debug!("Cached metadata does not match expected name and version for: {source}");
2034 }
2035 Ok(None) => {}
2036 Err(err) => {
2037 debug!("Failed to deserialize cached metadata for: {source} ({err})");
2038 }
2039 }
2040
2041 if let Some(metadata) = self
2043 .build_metadata(source, source_entry.path(), None, NoSources::None)
2044 .boxed_local()
2045 .await?
2046 {
2047 let metadata = if dynamic {
2049 ResolutionMetadata {
2050 dynamic: true,
2051 ..metadata
2052 }
2053 } else {
2054 metadata
2055 };
2056
2057 fs::create_dir_all(metadata_entry.dir())
2059 .await
2060 .map_err(Error::CacheWrite)?;
2061 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2062 .await
2063 .map_err(Error::CacheWrite)?;
2064
2065 return Ok(ArchiveMetadata {
2066 metadata: Metadata::from_metadata23(metadata),
2067 hashes: revision.into_hashes(),
2068 });
2069 }
2070
2071 let config_settings = self.config_settings_for(source.name());
2073 let extra_build_deps = self.extra_build_dependencies_for(source.name());
2074 let extra_build_variables = self.extra_build_variables_for(source.name());
2075 let build_info = BuildInfo::from_settings(
2076 config_settings.into_owned(),
2077 extra_build_deps.to_vec(),
2078 extra_build_variables.cloned(),
2079 );
2080 let cache_shard = build_info
2081 .cache_shard()
2082 .map(|digest| cache_shard.shard(digest))
2083 .unwrap_or(cache_shard);
2084
2085 let task = self
2087 .reporter
2088 .as_ref()
2089 .map(|reporter| reporter.on_build_start(source));
2090
2091 let (_disk_filename, _filename, metadata) = self
2092 .build_distribution(
2093 source,
2094 source_entry.path(),
2095 None,
2096 &cache_shard,
2097 NoSources::None,
2098 )
2099 .await?;
2100
2101 if let Some(task) = task {
2102 if let Some(reporter) = self.reporter.as_ref() {
2103 reporter.on_build_complete(source, task);
2104 }
2105 }
2106
2107 let metadata = if dynamic {
2109 ResolutionMetadata {
2110 dynamic: true,
2111 ..metadata
2112 }
2113 } else {
2114 metadata
2115 };
2116
2117 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2119 .await
2120 .map_err(Error::CacheWrite)?;
2121
2122 Ok(ArchiveMetadata {
2123 metadata: Metadata::from_metadata23(metadata),
2124 hashes: revision.into_hashes(),
2125 })
2126 }
2127
2128 async fn git_source_tree(
2130 &self,
2131 source: &BuildableSource<'_>,
2132 resource: &GitDirectorySourceUrl<'_>,
2133 tags: &Tags,
2134 hashes: HashPolicy<'_>,
2135 client: &ManagedClient<'_>,
2136 ) -> Result<BuiltWheelMetadata, Error> {
2137 if hashes.requires_validation() {
2139 return Err(Error::HashesNotSupportedGit(source.to_string()));
2140 }
2141
2142 let fetch = fetch_git_source_tree(
2143 self.build_context.git(),
2144 resource.git,
2145 resource.url.to_url(),
2146 resource.subdirectory,
2147 client.unmanaged.git_http_settings(resource.git.url()),
2148 self.build_context.cache(),
2149 self.reporter
2150 .clone()
2151 .map(|reporter| reporter.into_git_reporter()),
2152 )
2153 .await?;
2154
2155 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
2156 let cache_shard = self.build_context.cache().shard(
2157 CacheBucket::SourceDistributions,
2158 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
2159 );
2160 let metadata_entry = cache_shard.entry(METADATA);
2161
2162 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
2164
2165 let cache_info = CacheInfo::default();
2168
2169 let hashes = HashDigests::empty();
2172
2173 let config_settings = self.config_settings_for(source.name());
2175 let extra_build_deps = self.extra_build_dependencies_for(source.name());
2176 let extra_build_variables = self.extra_build_variables_for(source.name());
2177 let build_info = BuildInfo::from_settings(
2178 config_settings.into_owned(),
2179 extra_build_deps.to_vec(),
2180 extra_build_variables.cloned(),
2181 );
2182 let cache_shard = build_info
2183 .cache_shard()
2184 .map(|digest| cache_shard.shard(digest))
2185 .unwrap_or(cache_shard);
2186
2187 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
2189 .ok()
2190 .flatten()
2191 .filter(|file| file.matches(source.name(), source.version()))
2192 {
2193 return Ok(BuiltWheelMetadata::from_file(
2194 file, hashes, cache_info, build_info,
2195 ));
2196 }
2197
2198 let task = self
2199 .reporter
2200 .as_ref()
2201 .map(|reporter| reporter.on_build_start(source));
2202
2203 let (disk_filename, filename, metadata) = self
2204 .build_distribution(
2205 source,
2206 fetch.path(),
2207 resource.subdirectory,
2208 &cache_shard,
2209 self.build_context.sources().clone(),
2210 )
2211 .await?;
2212
2213 if let Some(task) = task {
2214 if let Some(reporter) = self.reporter.as_ref() {
2215 reporter.on_build_complete(source, task);
2216 }
2217 }
2218
2219 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2221 .await
2222 .map_err(Error::CacheWrite)?;
2223
2224 Ok(BuiltWheelMetadata {
2225 path: cache_shard.join(&disk_filename).into_boxed_path(),
2226 target: cache_shard.join(filename.stem()).into_boxed_path(),
2227 filename,
2228 hashes,
2229 cache_info,
2230 build_info,
2231 })
2232 }
2233
2234 async fn git_source_tree_metadata(
2239 &self,
2240 source: &BuildableSource<'_>,
2241 resource: &GitDirectorySourceUrl<'_>,
2242 hashes: HashPolicy<'_>,
2243 client: &ManagedClient<'_>,
2244 credentials_cache: &CredentialsCache,
2245 ) -> Result<ArchiveMetadata, Error> {
2246 if hashes.requires_validation() {
2248 return Err(Error::HashesNotSupportedGit(source.to_string()));
2249 }
2250
2251 let cache_shard = resource
2254 .git
2255 .reference()
2256 .as_str()
2257 .and_then(|reference| GitOid::from_str(reference).ok())
2258 .map(|oid| {
2259 self.build_context.cache().shard(
2260 CacheBucket::SourceDistributions,
2261 WheelCache::Git(resource.url, oid.as_short_str()).root(),
2262 )
2263 });
2264 if cache_shard
2265 .as_ref()
2266 .is_some_and(|cache_shard| cache_shard.is_dir())
2267 {
2268 debug!("Skipping GitHub fast path for: {source} (shard exists)");
2269 } else {
2270 debug!("Attempting GitHub fast path for: {source}");
2271
2272 match self
2274 .build_context
2275 .git()
2276 .github_fast_path(
2277 resource.git,
2278 client
2279 .unmanaged
2280 .uncached_client(resource.git.url())
2281 .raw_client(),
2282 )
2283 .await
2284 {
2285 Ok(Some(precise)) => {
2286 match self
2293 .github_metadata(precise, source, resource, client)
2294 .await
2295 {
2296 Ok(Some(metadata)) => {
2297 match validate_metadata(source, &metadata) {
2299 Ok(()) => {
2300 debug!(
2301 "Found static metadata via GitHub fast path for: {source}"
2302 );
2303 return Ok(ArchiveMetadata {
2304 metadata: Metadata::from_metadata23(metadata),
2305 hashes: HashDigests::empty(),
2306 });
2307 }
2308 Err(err) => {
2309 debug!(
2310 "Ignoring `pyproject.toml` from GitHub for {source}: {err}"
2311 );
2312 }
2313 }
2314 }
2315 Ok(None) => {
2316 }
2318 Err(err) => {
2319 debug!(
2320 "Failed to fetch `pyproject.toml` via GitHub fast path for: {source} ({err})"
2321 );
2322 }
2323 }
2324 }
2325 Ok(None) => {
2326 }
2328 Err(err) => {
2329 debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})");
2330 }
2331 }
2332 }
2333
2334 let fetch = fetch_git_source_tree(
2335 self.build_context.git(),
2336 resource.git,
2337 resource.url.to_url(),
2338 resource.subdirectory,
2339 client.unmanaged.git_http_settings(resource.git.url()),
2340 self.build_context.cache(),
2341 self.reporter
2342 .clone()
2343 .map(|reporter| reporter.into_git_reporter()),
2344 )
2345 .await?;
2346
2347 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
2348 let cache_shard = self.build_context.cache().shard(
2349 CacheBucket::SourceDistributions,
2350 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
2351 );
2352 let metadata_entry = cache_shard.entry(METADATA);
2353
2354 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
2356
2357 let path = if let Some(subdirectory) = resource.subdirectory {
2358 Cow::Owned(fetch.path().join(subdirectory))
2359 } else {
2360 Cow::Borrowed(fetch.path())
2361 };
2362
2363 let git_member = GitWorkspaceMember {
2364 fetch_root: fetch.path(),
2365 git_source: resource,
2366 };
2367
2368 let dynamic =
2370 match StaticMetadata::read(source, fetch.path(), resource.subdirectory).await? {
2371 StaticMetadata::Some(metadata) => {
2372 return Ok(ArchiveMetadata::from(
2373 Metadata::from_workspace(
2374 metadata,
2375 &path,
2376 Some(&git_member),
2377 self.build_context.locations(),
2378 self.build_context.sources().clone(),
2379 self.build_context
2380 .source_tree_editable_policy()
2381 .workspace_member_editable(None),
2382 self.build_context.cache(),
2383 self.build_context.workspace_cache(),
2384 credentials_cache,
2385 )
2386 .await?,
2387 ));
2388 }
2389 StaticMetadata::Dynamic => true,
2390 StaticMetadata::None => false,
2391 };
2392
2393 if self
2395 .build_context
2396 .cache()
2397 .freshness(&metadata_entry, source.name(), source.source_tree())
2398 .map_err(Error::CacheRead)?
2399 .is_fresh()
2400 {
2401 match CachedMetadata::read(&metadata_entry).await {
2402 Ok(Some(metadata)) => {
2403 if metadata.matches(source.name(), source.version()) {
2404 debug!("Using cached metadata for: {source}");
2405
2406 let git_member = GitWorkspaceMember {
2407 fetch_root: fetch.path(),
2408 git_source: resource,
2409 };
2410 return Ok(ArchiveMetadata::from(
2411 Metadata::from_workspace(
2412 metadata.into(),
2413 &path,
2414 Some(&git_member),
2415 self.build_context.locations(),
2416 self.build_context.sources().clone(),
2417 self.build_context
2418 .source_tree_editable_policy()
2419 .workspace_member_editable(None),
2420 self.build_context.cache(),
2421 self.build_context.workspace_cache(),
2422 credentials_cache,
2423 )
2424 .await?,
2425 ));
2426 }
2427 debug!(
2428 "Cached metadata does not match expected name and version for: {source}"
2429 );
2430 }
2431 Ok(None) => {}
2432 Err(err) => {
2433 debug!("Failed to deserialize cached metadata for: {source} ({err})");
2434 }
2435 }
2436 }
2437
2438 if let Some(metadata) = self
2440 .build_metadata(
2441 source,
2442 fetch.path(),
2443 resource.subdirectory,
2444 self.build_context.sources().clone(),
2445 )
2446 .boxed_local()
2447 .await?
2448 {
2449 let metadata = if dynamic {
2451 ResolutionMetadata {
2452 dynamic: true,
2453 ..metadata
2454 }
2455 } else {
2456 metadata
2457 };
2458
2459 fs::create_dir_all(metadata_entry.dir())
2461 .await
2462 .map_err(Error::CacheWrite)?;
2463 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2464 .await
2465 .map_err(Error::CacheWrite)?;
2466
2467 return Ok(ArchiveMetadata::from(
2468 Metadata::from_workspace(
2469 metadata,
2470 &path,
2471 Some(&git_member),
2472 self.build_context.locations(),
2473 self.build_context.sources().clone(),
2474 self.build_context
2475 .source_tree_editable_policy()
2476 .workspace_member_editable(None),
2477 self.build_context.cache(),
2478 self.build_context.workspace_cache(),
2479 credentials_cache,
2480 )
2481 .await?,
2482 ));
2483 }
2484
2485 let config_settings = self.config_settings_for(source.name());
2487 let extra_build_deps = self.extra_build_dependencies_for(source.name());
2488 let extra_build_variables = self.extra_build_variables_for(source.name());
2489 let build_info = BuildInfo::from_settings(
2490 config_settings.into_owned(),
2491 extra_build_deps.to_vec(),
2492 extra_build_variables.cloned(),
2493 );
2494 let cache_shard = build_info
2495 .cache_shard()
2496 .map(|digest| cache_shard.shard(digest))
2497 .unwrap_or(cache_shard);
2498
2499 let task = self
2501 .reporter
2502 .as_ref()
2503 .map(|reporter| reporter.on_build_start(source));
2504
2505 let (_disk_filename, _filename, metadata) = self
2506 .build_distribution(
2507 source,
2508 fetch.path(),
2509 resource.subdirectory,
2510 &cache_shard,
2511 self.build_context.sources().clone(),
2512 )
2513 .await?;
2514
2515 if let Some(task) = task {
2516 if let Some(reporter) = self.reporter.as_ref() {
2517 reporter.on_build_complete(source, task);
2518 }
2519 }
2520
2521 let metadata = if dynamic {
2523 ResolutionMetadata {
2524 dynamic: true,
2525 ..metadata
2526 }
2527 } else {
2528 metadata
2529 };
2530
2531 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2533 .await
2534 .map_err(Error::CacheWrite)?;
2535
2536 Ok(ArchiveMetadata::from(
2537 Metadata::from_workspace(
2538 metadata,
2539 fetch.path(),
2540 Some(&git_member),
2541 self.build_context.locations(),
2542 self.build_context.sources().clone(),
2543 self.build_context
2544 .source_tree_editable_policy()
2545 .workspace_member_editable(None),
2546 self.build_context.cache(),
2547 self.build_context.workspace_cache(),
2548 credentials_cache,
2549 )
2550 .await?,
2551 ))
2552 }
2553
2554 pub(crate) async fn resolve_revision(
2556 &self,
2557 source: &BuildableSource<'_>,
2558 client: &ManagedClient<'_>,
2559 ) -> Result<Option<GitOid>, Error> {
2560 let git = match source {
2561 BuildableSource::Dist(SourceDist::GitDirectory(source)) => &*source.git,
2562 BuildableSource::Dist(SourceDist::GitPath(source)) => &*source.git,
2563 BuildableSource::Url(SourceUrl::GitDirectory(source)) => source.git,
2564 BuildableSource::Url(SourceUrl::GitPath(source)) => source.git,
2565 _ => {
2566 return Ok(None);
2567 }
2568 };
2569
2570 if let Some(precise) = self.build_context.git().get_precise(git) {
2572 debug!("Precise commit already known: {source}");
2573 return Ok(Some(precise));
2574 }
2575
2576 if let Some(precise) = self
2578 .build_context
2579 .git()
2580 .github_fast_path(
2581 git,
2582 client.unmanaged.uncached_client(git.url()).raw_client(),
2583 )
2584 .await?
2585 {
2586 debug!("Resolved to precise commit via GitHub fast path: {source}");
2587 return Ok(Some(precise));
2588 }
2589
2590 let fetch = self
2592 .build_context
2593 .git()
2594 .fetch(
2595 git,
2596 client.unmanaged.git_http_settings(git.url()),
2597 self.build_context.cache().bucket(CacheBucket::Git),
2598 self.reporter
2599 .clone()
2600 .map(|reporter| reporter.into_git_reporter()),
2601 )
2602 .await?;
2603
2604 Ok(fetch.git().precise())
2605 }
2606
2607 async fn github_metadata(
2611 &self,
2612 commit: GitOid,
2613 source: &BuildableSource<'_>,
2614 resource: &GitDirectorySourceUrl<'_>,
2615 client: &ManagedClient<'_>,
2616 ) -> Result<Option<ResolutionMetadata>, Error> {
2617 let GitDirectorySourceUrl {
2618 git, subdirectory, ..
2619 } = resource;
2620
2621 if subdirectory.is_some() {
2625 return Ok(None);
2626 }
2627
2628 let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(git.repository())
2629 else {
2630 return Ok(None);
2631 };
2632
2633 let url =
2635 format!("https://raw.githubusercontent.com/{owner}/{repo}/{commit}/pyproject.toml");
2636
2637 debug!("Attempting to fetch `pyproject.toml` from: {url}");
2638
2639 let content = client
2640 .managed(async |client| {
2641 let response = client.uncached_client(git.url()).get(&url).send().await?;
2642
2643 if response.status() == StatusCode::NOT_FOUND {
2645 return Ok::<Option<String>, Error>(None);
2646 }
2647 response.error_for_status_ref()?;
2648
2649 let content = response.text().await?;
2650 Ok::<Option<String>, Error>(Some(content))
2651 })
2652 .await?;
2653
2654 let Some(content) = content else {
2655 debug!("GitHub API returned a 404 for: {url}");
2656 return Ok(None);
2657 };
2658
2659 let pyproject_toml = match PyProjectToml::from_toml(&content, source) {
2661 Ok(metadata) => metadata,
2662 Err(
2663 uv_pypi_types::MetadataError::InvalidPyprojectTomlSyntax(..)
2664 | uv_pypi_types::MetadataError::InvalidPyprojectTomlSchema(..),
2665 ) => {
2666 debug!("Failed to read `pyproject.toml` from GitHub API for: {url}");
2667 return Ok(None);
2668 }
2669 Err(err) => return Err(err.into()),
2670 };
2671
2672 let metadata =
2674 match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) {
2675 Ok(metadata) => metadata,
2676 Err(
2677 uv_pypi_types::MetadataError::Pep508Error(..)
2678 | uv_pypi_types::MetadataError::DynamicField(..)
2679 | uv_pypi_types::MetadataError::FieldNotFound(..)
2680 | uv_pypi_types::MetadataError::PoetrySyntax,
2681 ) => {
2682 debug!("Failed to extract static metadata from GitHub API for: {url}");
2683 return Ok(None);
2684 }
2685 Err(err) => return Err(err.into()),
2686 };
2687
2688 match has_sources(&content) {
2697 Ok(false) => {}
2698 Ok(true) => {
2699 debug!("Skipping GitHub fast path; `pyproject.toml` has sources: {url}");
2700 return Ok(None);
2701 }
2702 Err(err) => {
2703 debug!("Failed to parse `tool.uv.sources` from GitHub API for: {url} ({err})");
2704 return Ok(None);
2705 }
2706 }
2707
2708 Ok(Some(metadata))
2709 }
2710
2711 async fn heal_archive_revision(
2713 &self,
2714 source: &BuildableSource<'_>,
2715 resource: &PathSourceUrl<'_>,
2716 entry: &CacheEntry,
2717 revision: Revision,
2718 hashes: HashPolicy<'_>,
2719 ) -> Result<Revision, Error> {
2720 warn!("Re-extracting missing source distribution: {source}");
2721
2722 let hashes = self
2723 .persist_archive(
2724 source,
2725 &resource.path,
2726 resource.ext,
2727 entry.path(),
2728 hashes,
2729 revision.hashes(),
2730 )
2731 .await?;
2732 Ok(revision.with_hashes(HashDigests::from(hashes)))
2733 }
2734
2735 async fn heal_url_revision(
2737 &self,
2738 source: &BuildableSource<'_>,
2739 ext: SourceDistExtension,
2740 url: &DisplaySafeUrl,
2741 index: Option<&IndexUrl>,
2742 entry: &CacheEntry,
2743 revision: Revision,
2744 hashes: HashPolicy<'_>,
2745 client: &ManagedClient<'_>,
2746 ) -> Result<Revision, Error> {
2747 warn!("Re-downloading missing source distribution: {source}");
2748 let cache_entry = entry.shard().entry(HTTP_REVISION);
2749
2750 let cache_control = match client.unmanaged.connectivity() {
2752 Connectivity::Online
2753 if let Some(header) = index.and_then(|index| {
2754 self.build_context
2755 .locations()
2756 .artifact_cache_control_for(index)
2757 }) =>
2758 {
2759 CacheControl::Override(header)
2760 }
2761 Connectivity::Online => CacheControl::from(
2762 self.build_context
2763 .cache()
2764 .freshness(&cache_entry, source.name(), source.source_tree())
2765 .map_err(Error::CacheRead)?,
2766 ),
2767 Connectivity::Offline => CacheControl::AllowStale,
2768 };
2769
2770 let download = |response| {
2771 async {
2772 let (hashes, size) = self
2773 .download_archive(
2774 response,
2775 source,
2776 ext,
2777 entry.path(),
2778 hashes,
2779 revision.hashes(),
2780 )
2781 .await?;
2782 Ok(revision
2783 .clone()
2784 .with_hashes(HashDigests::from(hashes))
2785 .with_size(size))
2786 }
2787 .boxed_local()
2788 .instrument(info_span!("download", source_dist = %source))
2789 };
2790 client
2791 .managed(async |client| {
2792 client
2793 .cached_client()
2794 .skip_cache_with_retry(
2795 Self::request(url.clone(), client)?,
2796 &cache_entry,
2797 cache_control.clone(),
2798 download,
2799 )
2800 .await
2801 .map_err(|err| match err {
2802 CachedClientError::Callback { err, .. } => err,
2803 CachedClientError::Client(err) => Error::Client(err),
2804 })
2805 })
2806 .await
2807 }
2808
2809 async fn download_archive(
2811 &self,
2812 response: Response,
2813 source: &BuildableSource<'_>,
2814 ext: SourceDistExtension,
2815 target: &Path,
2816 hash_policy: HashPolicy<'_>,
2817 existing_hashes: &[HashDigest],
2818 ) -> Result<(Vec<HashDigest>, u64), Error> {
2819 let reader = response
2820 .bytes_stream()
2821 .map_err(std::io::Error::other)
2822 .into_async_read();
2823 let expected_size = match source {
2824 BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => {
2825 dist.size()
2826 }
2827 BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(),
2828 _ => None,
2829 };
2830
2831 let archive = ValidatedSourceArchive::extract(
2832 reader.compat(),
2833 source,
2834 ext,
2835 self.build_context.cache(),
2836 ArchiveValidation {
2837 extra_algorithms: &[HashAlgorithm::Sha256],
2838 hash_policy,
2839 existing_hashes,
2840 expected_size,
2841 },
2842 )
2843 .instrument(info_span!("download_source_dist", source_dist = %source))
2844 .await?;
2845 let metadata = archive.persist(target).await?;
2846 Ok((metadata.hashes, metadata.size))
2847 }
2848
2849 async fn persist_archive(
2851 &self,
2852 source: &BuildableSource<'_>,
2853 path: &Path,
2854 ext: SourceDistExtension,
2855 target: &Path,
2856 hash_policy: HashPolicy<'_>,
2857 existing_hashes: &[HashDigest],
2858 ) -> Result<Vec<HashDigest>, Error> {
2859 debug!("Unpacking for build: {}", path.display());
2860 let reader = fs_err::tokio::File::open(path)
2861 .await
2862 .map_err(Error::CacheRead)?;
2863 let archive = ValidatedSourceArchive::extract(
2864 reader,
2865 source,
2866 ext,
2867 self.build_context.cache(),
2868 ArchiveValidation {
2869 extra_algorithms: &[],
2870 hash_policy,
2871 existing_hashes,
2872 expected_size: None,
2873 },
2874 )
2875 .await?;
2876 Ok(archive.persist(target).await?.hashes)
2877 }
2878
2879 fn stop_discovery_at<'path>(
2882 source: &BuildableSource<'_>,
2883 source_root: &'path Path,
2884 ) -> Option<&'path Path> {
2885 if matches!(
2886 source,
2887 BuildableSource::Dist(SourceDist::GitDirectory(_))
2888 | BuildableSource::Url(SourceUrl::GitDirectory(_))
2889 ) {
2890 Some(source_root)
2891 } else {
2892 None
2893 }
2894 }
2895
2896 #[instrument(skip_all, fields(dist = %source))]
2900 async fn build_distribution(
2901 &self,
2902 source: &BuildableSource<'_>,
2903 source_root: &Path,
2904 subdirectory: Option<&Path>,
2905 cache_shard: &CacheShard,
2906 no_sources: NoSources,
2907 ) -> Result<(String, WheelFilename, ResolutionMetadata), Error> {
2908 debug!("Building: {source}");
2909
2910 if self
2912 .build_context
2913 .build_options()
2914 .no_build_requirement(source.name())
2915 {
2916 if source.is_editable() || source.is_first_party() {
2917 debug!("Allowing build for first-party or editable source distribution: {source}");
2918 } else {
2919 return Err(Error::NoBuild);
2920 }
2921 }
2922
2923 let temp_dir = self
2925 .build_context
2926 .cache()
2927 .build_dir()
2928 .map_err(Error::CacheWrite)?;
2929
2930 fs::create_dir_all(&cache_shard)
2932 .await
2933 .map_err(Error::CacheWrite)?;
2934
2935 let disk_filename = if let Some(name) = self
2937 .build_context
2938 .direct_build(
2939 source_root,
2940 subdirectory,
2941 temp_dir.path(),
2942 no_sources.clone(),
2943 if source.is_editable() {
2944 BuildKind::Editable
2945 } else {
2946 BuildKind::Wheel
2947 },
2948 Some(&source.to_string()),
2949 )
2950 .await
2951 .map_err(|err| Error::Build(err.into()))?
2952 {
2953 name.to_string()
2955 } else {
2956 let base_python = if cfg!(unix) {
2958 self.build_context
2959 .interpreter()
2960 .await
2961 .find_base_python()
2962 .map_err(Error::BaseInterpreter)?
2963 } else {
2964 self.build_context
2965 .interpreter()
2966 .await
2967 .to_base_python()
2968 .map_err(Error::BaseInterpreter)?
2969 };
2970
2971 let build_kind = if source.is_editable() {
2972 BuildKind::Editable
2973 } else {
2974 BuildKind::Wheel
2975 };
2976
2977 let install_path = if let Some(subdirectory) = subdirectory {
2978 source_root.join(subdirectory)
2979 } else {
2980 source_root.to_path_buf()
2981 };
2982
2983 let stop_discovery_at = Self::stop_discovery_at(source, source_root);
2984
2985 let build_key = BuildKey {
2986 base_python: base_python.into_boxed_path(),
2987 source_root: source_root.to_path_buf().into_boxed_path(),
2988 subdirectory: subdirectory
2989 .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()),
2990 no_sources: no_sources.clone(),
2991 build_kind,
2992 };
2993
2994 if let Some(builder) = self.build_context.build_arena().remove(&build_key) {
2995 debug!("Reusing existing build environment for: {source}");
2996 let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
2997
2998 self.build_context.build_arena().insert(build_key, builder);
3000
3001 wheel
3002 } else {
3003 debug!("Creating build environment for: {source}");
3004
3005 let builder = self
3006 .build_context
3007 .setup_build(
3008 source_root,
3009 subdirectory,
3010 &install_path,
3011 stop_discovery_at,
3012 Some(&source.to_string()),
3013 source.as_dist(),
3014 &no_sources,
3015 if source.is_editable() {
3016 BuildKind::Editable
3017 } else {
3018 BuildKind::Wheel
3019 },
3020 if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
3021 BuildOutput::Quiet
3022 } else {
3023 BuildOutput::Debug
3024 },
3025 self.build_stack.cloned().unwrap_or_default(),
3026 )
3027 .await
3028 .map_err(|err| Error::Build(err.into()))?;
3029
3030 let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
3032
3033 self.build_context.build_arena().insert(build_key, builder);
3035
3036 wheel
3037 }
3038 };
3039
3040 let filename = WheelFilename::from_str(&disk_filename)?;
3042 let metadata = read_wheel_metadata(&filename, &temp_dir.path().join(&disk_filename))?;
3043
3044 validate_metadata(source, &metadata)?;
3046 validate_filename(&filename, &metadata)?;
3047
3048 rename_with_retry(
3050 temp_dir.path().join(&disk_filename),
3051 cache_shard.join(&disk_filename),
3052 )
3053 .await
3054 .map_err(Error::CacheWrite)?;
3055
3056 debug!("Built `{source}` into `{disk_filename}`");
3057 Ok((disk_filename, filename, metadata))
3058 }
3059
3060 #[instrument(skip_all, fields(dist = %source))]
3062 async fn build_metadata(
3063 &self,
3064 source: &BuildableSource<'_>,
3065 source_root: &Path,
3066 subdirectory: Option<&Path>,
3067 no_sources: NoSources,
3068 ) -> Result<Option<ResolutionMetadata>, Error> {
3069 debug!("Preparing metadata for: {source}");
3070
3071 let source_name = source.name();
3072 if self
3073 .build_context
3074 .build_options()
3075 .no_build_requirement(source_name)
3076 && !(source_name.is_none() && source.is_editable())
3079 {
3080 return if let Some(name) = source_name {
3081 Err(Error::NoBuildPackage(name.clone()))
3082 } else {
3083 Err(Error::NoBuild)
3084 };
3085 }
3086
3087 if let Some(requires_python) = source.requires_python() {
3090 let installed = self.build_context.interpreter().await.python_version();
3091 let target = release_specifiers_to_ranges(requires_python.clone())
3092 .bounding_range()
3093 .map(|bounding_range| bounding_range.0.cloned())
3094 .unwrap_or(Bound::Unbounded);
3095 let is_compatible = match target {
3096 Bound::Included(target) => *installed >= target,
3097 Bound::Excluded(target) => *installed > target,
3098 Bound::Unbounded => true,
3099 };
3100 if !is_compatible {
3101 return Err(Error::RequiresPython(
3102 requires_python.clone(),
3103 installed.clone(),
3104 ));
3105 }
3106 }
3107
3108 let base_python = if cfg!(unix) {
3110 self.build_context
3111 .interpreter()
3112 .await
3113 .find_base_python()
3114 .map_err(Error::BaseInterpreter)?
3115 } else {
3116 self.build_context
3117 .interpreter()
3118 .await
3119 .to_base_python()
3120 .map_err(Error::BaseInterpreter)?
3121 };
3122
3123 let build_kind = if source.is_editable() {
3125 BuildKind::Editable
3126 } else {
3127 BuildKind::Wheel
3128 };
3129
3130 let install_path = if let Some(subdirectory) = subdirectory {
3131 source_root.join(subdirectory)
3132 } else {
3133 source_root.to_path_buf()
3134 };
3135
3136 let stop_discovery_at = Self::stop_discovery_at(source, source_root);
3137
3138 let mut builder = self
3140 .build_context
3141 .setup_build(
3142 source_root,
3143 subdirectory,
3144 &install_path,
3145 stop_discovery_at,
3146 Some(&source.to_string()),
3147 source.as_dist(),
3148 &no_sources,
3149 build_kind,
3150 if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
3151 BuildOutput::Quiet
3152 } else {
3153 BuildOutput::Debug
3154 },
3155 self.build_stack.cloned().unwrap_or_default(),
3156 )
3157 .await
3158 .map_err(|err| Error::Build(err.into()))?;
3159
3160 let dist_info = builder.metadata().await.map_err(Error::Build)?;
3162
3163 self.build_context.build_arena().insert(
3165 BuildKey {
3166 base_python: base_python.into_boxed_path(),
3167 source_root: source_root.to_path_buf().into_boxed_path(),
3168 subdirectory: subdirectory
3169 .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()),
3170 no_sources,
3171 build_kind,
3172 },
3173 builder,
3174 );
3175
3176 let Some(dist_info) = dist_info else {
3178 return Ok(None);
3179 };
3180
3181 debug!("Prepared metadata for: {source}");
3183 let content = fs::read(dist_info.join("METADATA"))
3184 .await
3185 .map_err(Error::CacheRead)?;
3186 let metadata = ResolutionMetadata::parse_metadata(&content)?;
3187
3188 validate_metadata(source, &metadata)?;
3190
3191 Ok(Some(metadata))
3192 }
3193
3194 fn request(
3196 url: DisplaySafeUrl,
3197 client: &RegistryClient,
3198 ) -> Result<reqwest::Request, reqwest::Error> {
3199 client
3200 .uncached_client(&url)
3201 .get(Url::from(url))
3202 .header(
3203 "accept-encoding",
3207 reqwest::header::HeaderValue::from_static("identity"),
3208 )
3209 .build()
3210 }
3211}
3212
3213pub fn prune(cache: &Cache) -> Result<Removal, Error> {
3215 let mut removal = cache.removal();
3216
3217 let bucket = cache.bucket(CacheBucket::SourceDistributions);
3218 if bucket.is_dir() {
3219 for entry in walkdir::WalkDir::new(bucket) {
3220 let entry = entry.map_err(Error::CacheWalk)?;
3221
3222 if !entry.file_type().is_dir() {
3223 continue;
3224 }
3225
3226 let revision = entry.path().join("revision.http");
3229 if revision.is_file() {
3230 if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision) {
3231 for sibling in entry.path().read_dir().map_err(Error::CacheRead)? {
3233 let sibling = sibling.map_err(Error::CacheRead)?;
3234 if sibling.file_type().map_err(Error::CacheRead)?.is_dir() {
3235 let sibling_name = sibling.file_name();
3236 if sibling_name != pointer.revision.id().as_str() {
3237 debug!(
3238 "Removing dangling source revision: {}",
3239 sibling.path().display()
3240 );
3241 removal += cache
3242 .remove_path(sibling.path())
3243 .map_err(Error::CacheWrite)?;
3244 }
3245 }
3246 }
3247 }
3248 }
3249
3250 let revision = entry.path().join("revision.rev");
3253 if revision.is_file() {
3254 if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision) {
3255 for sibling in entry.path().read_dir().map_err(Error::CacheRead)? {
3257 let sibling = sibling.map_err(Error::CacheRead)?;
3258 if sibling.file_type().map_err(Error::CacheRead)?.is_dir() {
3259 let sibling_name = sibling.file_name();
3260 if sibling_name != pointer.revision.id().as_str() {
3261 debug!(
3262 "Removing dangling source revision: {}",
3263 sibling.path().display()
3264 );
3265 removal += cache
3266 .remove_path(sibling.path())
3267 .map_err(Error::CacheWrite)?;
3268 }
3269 }
3270 }
3271 }
3272 }
3273 }
3274 }
3275
3276 Ok(removal)
3277}
3278
3279#[derive(Debug)]
3281enum StaticMetadata {
3282 Some(ResolutionMetadata),
3284 Dynamic,
3286 None,
3288}
3289
3290impl StaticMetadata {
3291 async fn read(
3293 source: &BuildableSource<'_>,
3294 source_root: &Path,
3295 subdirectory: Option<&Path>,
3296 ) -> Result<Self, Error> {
3297 let pyproject_toml = match read_pyproject_toml(source_root, subdirectory).await {
3299 Ok(pyproject_toml) => Some(pyproject_toml),
3300 Err(Error::MissingPyprojectToml) => {
3301 debug!("No `pyproject.toml` available for: {source}");
3302 None
3303 }
3304 Err(err) => return Err(err),
3305 };
3306
3307 let dynamic = pyproject_toml.as_ref().is_some_and(|pyproject_toml| {
3309 pyproject_toml.project.as_ref().is_some_and(|project| {
3310 project
3311 .dynamic
3312 .as_ref()
3313 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
3314 })
3315 });
3316
3317 if let Some(pyproject_toml) = pyproject_toml {
3319 match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) {
3320 Ok(metadata) => {
3321 debug!("Found static `pyproject.toml` for: {source}");
3322
3323 match validate_metadata(source, &metadata) {
3325 Ok(()) => {
3326 return Ok(Self::Some(metadata));
3327 }
3328 Err(err) => {
3329 debug!("Ignoring `pyproject.toml` for {source}: {err}");
3330 }
3331 }
3332 }
3333 Err(
3334 err @ (uv_pypi_types::MetadataError::Pep508Error(_)
3335 | uv_pypi_types::MetadataError::DynamicField(_)
3336 | uv_pypi_types::MetadataError::FieldNotFound(_)
3337 | uv_pypi_types::MetadataError::PoetrySyntax),
3338 ) => {
3339 debug!("No static `pyproject.toml` available for: {source} ({err:?})");
3340 }
3341 Err(err) => return Err(Error::PyprojectToml(err)),
3342 }
3343 }
3344
3345 if source.is_source_tree() {
3348 return Ok(if dynamic { Self::Dynamic } else { Self::None });
3349 }
3350
3351 match read_pkg_info(source_root, subdirectory).await {
3353 Ok(metadata) => {
3354 debug!("Found static `PKG-INFO` for: {source}");
3355
3356 match validate_metadata(source, &metadata) {
3358 Ok(()) => {
3359 let metadata = if dynamic {
3361 ResolutionMetadata {
3362 dynamic: true,
3363 ..metadata
3364 }
3365 } else {
3366 metadata
3367 };
3368 return Ok(Self::Some(metadata));
3369 }
3370 Err(err) => {
3371 debug!("Ignoring `PKG-INFO` for {source}: {err}");
3372 }
3373 }
3374 }
3375 Err(
3376 err @ (Error::MissingPkgInfo
3377 | Error::PkgInfo(
3378 uv_pypi_types::MetadataError::Pep508Error(_)
3379 | uv_pypi_types::MetadataError::DynamicField(_)
3380 | uv_pypi_types::MetadataError::FieldNotFound(_)
3381 | uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
3382 )),
3383 ) => {
3384 debug!("No static `PKG-INFO` available for: {source} ({err:?})");
3385 }
3386 Err(err) => return Err(err),
3387 }
3388
3389 Ok(Self::None)
3390 }
3391}
3392
3393fn has_sources(content: &str) -> Result<bool, toml::de::Error> {
3395 #[derive(serde::Deserialize)]
3396 struct PyProjectToml {
3397 tool: Option<Tool>,
3398 }
3399
3400 #[derive(serde::Deserialize)]
3401 struct Tool {
3402 uv: Option<ToolUv>,
3403 }
3404
3405 #[derive(serde::Deserialize)]
3406 struct ToolUv {
3407 sources: Option<ToolUvSources>,
3408 }
3409
3410 let pyproject_toml =
3411 info_span!("toml::from_str has sources").in_scope(|| toml::from_str(content))?;
3412 if let PyProjectToml { tool: Some(tool) } = pyproject_toml {
3413 if let Some(uv) = tool.uv {
3414 if let Some(sources) = uv.sources {
3415 if !sources.inner().is_empty() {
3416 return Ok(true);
3417 }
3418 }
3419 }
3420 }
3421
3422 Ok(false)
3423}
3424
3425fn validate_metadata(
3427 source: &BuildableSource<'_>,
3428 metadata: &ResolutionMetadata,
3429) -> Result<(), Error> {
3430 if let Some(name) = source.name() {
3431 if metadata.name != *name {
3432 return Err(Error::WheelMetadataNameMismatch {
3433 metadata: metadata.name.clone(),
3434 given: name.clone(),
3435 });
3436 }
3437 }
3438
3439 if let Some(version) = source.version() {
3440 if *version != metadata.version && *version != metadata.version.clone().without_local() {
3441 return Err(Error::WheelMetadataVersionMismatch {
3442 metadata: metadata.version.clone(),
3443 given: version.clone(),
3444 });
3445 }
3446 }
3447
3448 Ok(())
3449}
3450
3451fn validate_filename(filename: &WheelFilename, metadata: &ResolutionMetadata) -> Result<(), Error> {
3453 if metadata.name != filename.name {
3454 return Err(Error::WheelFilenameNameMismatch {
3455 metadata: metadata.name.clone(),
3456 filename: filename.name.clone(),
3457 });
3458 }
3459
3460 if metadata.version != filename.version {
3461 return Err(Error::WheelFilenameVersionMismatch {
3462 metadata: metadata.version.clone(),
3463 filename: filename.version.clone(),
3464 });
3465 }
3466
3467 Ok(())
3468}
3469
3470#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3474pub(crate) struct HttpRevisionPointer {
3475 revision: Revision,
3476}
3477
3478impl HttpRevisionPointer {
3479 pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3481 match fs_err::File::open(path.as_ref()) {
3482 Ok(file) => {
3483 let data = DataWithCachePolicy::from_reader(file)?.data;
3484 let revision = rmp_serde::from_slice::<Revision>(&data)?;
3485 Ok(Some(Self { revision }))
3486 }
3487 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3488 Err(err) => Err(Error::CacheRead(err)),
3489 }
3490 }
3491
3492 pub(crate) fn into_revision(self) -> Revision {
3494 self.revision
3495 }
3496}
3497
3498#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3502pub(crate) struct LocalRevisionPointer {
3503 cache_info: CacheInfo,
3504 revision: Revision,
3505}
3506
3507impl LocalRevisionPointer {
3508 pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3510 match fs_err::read(path) {
3511 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
3512 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3513 Err(err) => Err(Error::CacheRead(err)),
3514 }
3515 }
3516
3517 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
3519 fs::create_dir_all(&entry.dir())
3520 .await
3521 .map_err(Error::CacheWrite)?;
3522 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
3523 .await
3524 .map_err(Error::CacheWrite)
3525 }
3526
3527 pub(crate) fn cache_info(&self) -> &CacheInfo {
3529 &self.cache_info
3530 }
3531
3532 fn revision(&self) -> &Revision {
3534 &self.revision
3535 }
3536
3537 pub(crate) fn into_revision(self) -> Revision {
3539 self.revision
3540 }
3541}
3542
3543#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3547pub(crate) struct RevisionHashes {
3548 hashes: Vec<HashDigest>,
3549}
3550
3551impl RevisionHashes {
3552 pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3554 match fs_err::read(path) {
3555 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
3556 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3557 Err(err) => Err(Error::CacheRead(err)),
3558 }
3559 }
3560
3561 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
3563 fs::create_dir_all(&entry.dir())
3564 .await
3565 .map_err(Error::CacheWrite)?;
3566 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
3567 .await
3568 .map_err(Error::CacheWrite)
3569 }
3570
3571 pub(crate) fn into_hashes(self) -> HashDigests {
3573 HashDigests::from(self.hashes)
3574 }
3575}
3576
3577impl Hashed for RevisionHashes {
3578 fn hashes(&self) -> &[HashDigest] {
3579 &self.hashes
3580 }
3581}
3582
3583async fn read_pkg_info(
3587 source_tree: &Path,
3588 subdirectory: Option<&Path>,
3589) -> Result<ResolutionMetadata, Error> {
3590 let pkg_info = match subdirectory {
3592 Some(subdirectory) => source_tree.join(subdirectory).join("PKG-INFO"),
3593 None => source_tree.join("PKG-INFO"),
3594 };
3595 let content = match fs::read(pkg_info).await {
3596 Ok(content) => content,
3597 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3598 return Err(Error::MissingPkgInfo);
3599 }
3600 Err(err) => return Err(Error::CacheRead(err)),
3601 };
3602
3603 let metadata = ResolutionMetadata::parse_pkg_info(&content).map_err(Error::PkgInfo)?;
3605
3606 Ok(metadata)
3607}
3608
3609async fn read_pyproject_toml(
3612 source_tree: &Path,
3613 subdirectory: Option<&Path>,
3614) -> Result<PyProjectToml, Error> {
3615 let pyproject_toml = match subdirectory {
3617 Some(subdirectory) => source_tree.join(subdirectory).join("pyproject.toml"),
3618 None => source_tree.join("pyproject.toml"),
3619 };
3620 let content = match fs::read_to_string(&pyproject_toml).await {
3621 Ok(content) => content,
3622 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3623 return Err(Error::MissingPyprojectToml);
3624 }
3625 Err(err) => return Err(Error::CacheRead(err)),
3626 };
3627
3628 let pyproject_toml = PyProjectToml::from_toml(&content, pyproject_toml.simplified_display())?;
3629
3630 Ok(pyproject_toml)
3631}
3632
3633#[derive(Debug, Clone)]
3635struct CachedMetadata(ResolutionMetadata);
3636
3637impl CachedMetadata {
3638 async fn read(cache_entry: &CacheEntry) -> Result<Option<Self>, Error> {
3640 match fs::read(&cache_entry.path()).await {
3641 Ok(cached) => Ok(Some(Self(rmp_serde::from_slice(&cached)?))),
3642 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3643 Err(err) => Err(Error::CacheRead(err)),
3644 }
3645 }
3646
3647 fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool {
3649 name.is_none_or(|name| self.0.name == *name)
3650 && version.is_none_or(|version| self.0.version == *version)
3651 }
3652}
3653
3654impl From<CachedMetadata> for ResolutionMetadata {
3655 fn from(value: CachedMetadata) -> Self {
3656 value.0
3657 }
3658}
3659
3660fn read_wheel_metadata(
3662 filename: &WheelFilename,
3663 wheel: &Path,
3664) -> Result<ResolutionMetadata, Error> {
3665 let file = fs_err::File::open(wheel).map_err(Error::CacheRead)?;
3666 let reader = std::io::BufReader::new(file);
3667 let dist_info = read_archive_metadata(filename, reader)
3668 .map_err(|err| Error::WheelMetadata(wheel.to_path_buf(), Box::new(err)))?;
3669 Ok(ResolutionMetadata::parse_metadata(&dist_info)?)
3670}