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_extract::hash::Hasher;
39use uv_fs::{Simplified, rename_with_retry, write_atomic};
40use uv_git::{Fetch, GIT_LFS, GitError, GitHttpSettings, GitResolver};
41use uv_git_types::{GitHubRepository, GitOid, GitUrl};
42use uv_metadata::read_archive_metadata;
43use uv_normalize::PackageName;
44use uv_pep440::{Version, release_specifiers_to_ranges};
45use uv_platform_tags::Tags;
46use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, PyProjectToml, ResolutionMetadata};
47use uv_redacted::DisplaySafeUrl;
48use uv_types::{BuildContext, BuildKey, BuildStack, SourceBuildTrait};
49use uv_workspace::pyproject::ToolUvSources;
50
51use crate::distribution_database::ManagedClient;
52use crate::error::Error;
53use crate::hash::http_hash_algorithms;
54use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata};
55use crate::source::built_wheel_metadata::{BuiltWheelFile, BuiltWheelMetadata};
56use crate::source::revision::Revision;
57use crate::{Reporter, RequiresDist};
58
59mod built_wheel_metadata;
60mod revision;
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 algorithms = http_hash_algorithms(hashes);
986 let (hashes, size) = self
987 .download_archive(response, source, ext, entry.path(), &algorithms)
988 .await?;
989
990 Ok(revision
991 .with_hashes(HashDigests::from(hashes))
992 .with_size(size))
993 }
994 .boxed_local()
995 .instrument(info_span!("download", source_dist = %source))
996 };
997 let req = Self::request(url.clone(), client.unmanaged)?;
998 let revision = client
999 .managed(|client| {
1000 client.cached_client().get_serde_with_retry(
1001 req,
1002 &cache_entry,
1003 cache_control.clone(),
1004 download,
1005 )
1006 })
1007 .await
1008 .map_err(|err| match err {
1009 CachedClientError::Callback { err, .. } => err,
1010 CachedClientError::Client(err) => Error::Client(err),
1011 })?;
1012
1013 let expected_size = match source {
1014 BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => {
1015 dist.size()
1016 }
1017 BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(),
1018 _ => None,
1019 };
1020 if let (Some(expected), Some(actual)) = (expected_size, revision.size())
1021 && expected != actual
1022 {
1023 return Err(Error::MismatchedSize {
1024 distribution: source.to_string(),
1025 expected,
1026 actual,
1027 });
1028 }
1029
1030 if revision.has_digests(hashes) && (expected_size.is_none() || revision.size().is_some()) {
1032 Ok(revision)
1033 } else {
1034 client
1035 .managed(async |client| {
1036 client
1037 .cached_client()
1038 .skip_cache_with_retry(
1039 Self::request(url.clone(), client)?,
1040 &cache_entry,
1041 cache_control,
1042 download,
1043 )
1044 .await
1045 .map_err(|err| match err {
1046 CachedClientError::Callback { err, .. } => err,
1047 CachedClientError::Client(err) => Error::Client(err),
1048 })
1049 })
1050 .await
1051 }
1052 }
1053
1054 async fn archive(
1056 &self,
1057 source: &BuildableSource<'_>,
1058 resource: &PathSourceUrl<'_>,
1059 cache_shard: &CacheShard,
1060 tags: &Tags,
1061 hashes: HashPolicy<'_>,
1062 ) -> Result<BuiltWheelMetadata, Error> {
1063 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1064
1065 let LocalRevisionPointer {
1067 cache_info,
1068 revision,
1069 } = self
1070 .archive_revision(source, resource, cache_shard, hashes)
1071 .await?;
1072
1073 if !revision.satisfies(hashes) {
1075 return Err(Error::hash_mismatch(
1076 source.to_string(),
1077 hashes.digests(),
1078 revision.hashes(),
1079 ));
1080 }
1081
1082 let cache_shard = cache_shard.shard(revision.id());
1085 let source_entry = cache_shard.entry(SOURCE);
1086
1087 let config_settings = self.config_settings_for(source.name());
1089 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1090 let extra_build_variables = self.extra_build_variables_for(source.name());
1091 let build_info = BuildInfo::from_settings(
1092 config_settings.into_owned(),
1093 extra_build_deps.to_vec(),
1094 extra_build_variables.cloned(),
1095 );
1096 let cache_shard = build_info
1097 .cache_shard()
1098 .map(|digest| cache_shard.shard(digest))
1099 .unwrap_or(cache_shard);
1100
1101 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1103 .ok()
1104 .flatten()
1105 .filter(|file| file.matches(source.name(), source.version()))
1106 {
1107 return Ok(BuiltWheelMetadata::from_file(
1108 file,
1109 revision.into_hashes(),
1110 cache_info,
1111 build_info,
1112 ));
1113 }
1114
1115 let revision = if source_entry.path().is_dir() {
1117 revision
1118 } else {
1119 self.heal_archive_revision(source, resource, &source_entry, revision, hashes)
1120 .await?
1121 };
1122
1123 let task = self
1124 .reporter
1125 .as_ref()
1126 .map(|reporter| reporter.on_build_start(source));
1127
1128 let (disk_filename, filename, metadata) = self
1129 .build_distribution(
1130 source,
1131 source_entry.path(),
1132 None,
1133 &cache_shard,
1134 NoSources::None,
1135 )
1136 .await?;
1137
1138 if let Some(task) = task {
1139 if let Some(reporter) = self.reporter.as_ref() {
1140 reporter.on_build_complete(source, task);
1141 }
1142 }
1143
1144 let metadata_entry = cache_shard.entry(METADATA);
1146 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1147 .await
1148 .map_err(Error::CacheWrite)?;
1149
1150 Ok(BuiltWheelMetadata {
1151 path: cache_shard.join(&disk_filename).into_boxed_path(),
1152 target: cache_shard.join(filename.stem()).into_boxed_path(),
1153 filename,
1154 hashes: revision.into_hashes(),
1155 cache_info,
1156 build_info,
1157 })
1158 }
1159
1160 async fn archive_metadata(
1165 &self,
1166 source: &BuildableSource<'_>,
1167 resource: &PathSourceUrl<'_>,
1168 cache_shard: &CacheShard,
1169 hashes: HashPolicy<'_>,
1170 ) -> Result<ArchiveMetadata, Error> {
1171 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1172
1173 let LocalRevisionPointer { revision, .. } = self
1175 .archive_revision(source, resource, cache_shard, hashes)
1176 .await?;
1177
1178 if !revision.satisfies(hashes) {
1180 return Err(Error::hash_mismatch(
1181 source.to_string(),
1182 hashes.digests(),
1183 revision.hashes(),
1184 ));
1185 }
1186
1187 let cache_shard = cache_shard.shard(revision.id());
1190 let source_entry = cache_shard.entry(SOURCE);
1191
1192 let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
1194 StaticMetadata::Some(metadata) => {
1195 return Ok(ArchiveMetadata {
1196 metadata: Metadata::from_metadata23(metadata),
1197 hashes: revision.into_hashes(),
1198 });
1199 }
1200 StaticMetadata::Dynamic => true,
1201 StaticMetadata::None => false,
1202 };
1203
1204 let metadata_entry = cache_shard.entry(METADATA);
1206 match CachedMetadata::read(&metadata_entry).await {
1207 Ok(Some(metadata)) => {
1208 if metadata.matches(source.name(), source.version()) {
1209 debug!("Using cached metadata for: {source}");
1210 return Ok(ArchiveMetadata {
1211 metadata: Metadata::from_metadata23(metadata.into()),
1212 hashes: revision.into_hashes(),
1213 });
1214 }
1215 debug!("Cached metadata does not match expected name and version for: {source}");
1216 }
1217 Ok(None) => {}
1218 Err(err) => {
1219 debug!("Failed to deserialize cached metadata for: {source} ({err})");
1220 }
1221 }
1222
1223 let revision = if source_entry.path().is_dir() {
1225 revision
1226 } else {
1227 self.heal_archive_revision(source, resource, &source_entry, revision, hashes)
1228 .await?
1229 };
1230
1231 if let Some(metadata) = self
1233 .build_metadata(source, source_entry.path(), None, NoSources::None)
1234 .boxed_local()
1235 .await?
1236 {
1237 let metadata = if dynamic {
1239 ResolutionMetadata {
1240 dynamic: true,
1241 ..metadata
1242 }
1243 } else {
1244 metadata
1245 };
1246
1247 fs::create_dir_all(metadata_entry.dir())
1249 .await
1250 .map_err(Error::CacheWrite)?;
1251 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1252 .await
1253 .map_err(Error::CacheWrite)?;
1254
1255 return Ok(ArchiveMetadata {
1256 metadata: Metadata::from_metadata23(metadata),
1257 hashes: revision.into_hashes(),
1258 });
1259 }
1260
1261 let config_settings = self.config_settings_for(source.name());
1263 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1264 let extra_build_variables = self.extra_build_variables_for(source.name());
1265 let build_info = BuildInfo::from_settings(
1266 config_settings.into_owned(),
1267 extra_build_deps.to_vec(),
1268 extra_build_variables.cloned(),
1269 );
1270 let cache_shard = build_info
1271 .cache_shard()
1272 .map(|digest| cache_shard.shard(digest))
1273 .unwrap_or(cache_shard);
1274
1275 let task = self
1277 .reporter
1278 .as_ref()
1279 .map(|reporter| reporter.on_build_start(source));
1280
1281 let (_disk_filename, _filename, metadata) = self
1282 .build_distribution(
1283 source,
1284 source_entry.path(),
1285 None,
1286 &cache_shard,
1287 NoSources::None,
1288 )
1289 .await?;
1290
1291 if let Some(task) = task {
1292 if let Some(reporter) = self.reporter.as_ref() {
1293 reporter.on_build_complete(source, task);
1294 }
1295 }
1296
1297 let metadata = if dynamic {
1299 ResolutionMetadata {
1300 dynamic: true,
1301 ..metadata
1302 }
1303 } else {
1304 metadata
1305 };
1306
1307 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1309 .await
1310 .map_err(Error::CacheWrite)?;
1311
1312 Ok(ArchiveMetadata {
1313 metadata: Metadata::from_metadata23(metadata),
1314 hashes: revision.into_hashes(),
1315 })
1316 }
1317
1318 async fn archive_revision(
1320 &self,
1321 source: &BuildableSource<'_>,
1322 resource: &PathSourceUrl<'_>,
1323 cache_shard: &CacheShard,
1324 hashes: HashPolicy<'_>,
1325 ) -> Result<LocalRevisionPointer, Error> {
1326 if !resource.path.is_file() {
1328 return Err(Error::NotFound(resource.url.clone()));
1329 }
1330
1331 let cache_info = CacheInfo::from_file(&resource.path).map_err(Error::CacheRead)?;
1333
1334 let revision_entry = cache_shard.entry(LOCAL_REVISION);
1336
1337 if let Some(pointer) = LocalRevisionPointer::read_from(&revision_entry)? {
1340 if *pointer.cache_info() == cache_info {
1341 if pointer.revision().has_digests(hashes) {
1342 return Ok(pointer);
1343 }
1344 }
1345 }
1346
1347 let revision = Revision::new();
1349
1350 debug!("Unpacking source distribution: {source}");
1352 let entry = cache_shard.shard(revision.id()).entry(SOURCE);
1353 let algorithms = hashes.algorithms();
1354 let hashes = self
1355 .persist_archive(&resource.path, resource.ext, entry.path(), &algorithms)
1356 .await?;
1357
1358 let revision = revision.with_hashes(HashDigests::from(hashes));
1360
1361 let pointer = LocalRevisionPointer {
1363 cache_info,
1364 revision,
1365 };
1366 pointer.write_to(&revision_entry).await?;
1367
1368 Ok(pointer)
1369 }
1370
1371 async fn source_tree(
1374 &self,
1375 source: &BuildableSource<'_>,
1376 resource: &DirectorySourceUrl<'_>,
1377 tags: &Tags,
1378 hashes: HashPolicy<'_>,
1379 ) -> Result<BuiltWheelMetadata, Error> {
1380 if hashes.requires_validation() {
1382 return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
1383 }
1384
1385 let cache_shard = self.build_context.cache().shard(
1386 CacheBucket::SourceDistributions,
1387 if resource.editable.unwrap_or(false) {
1388 WheelCache::Editable(resource.url).root()
1389 } else {
1390 WheelCache::Path(resource.url).root()
1391 },
1392 );
1393
1394 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1396
1397 let LocalRevisionPointer {
1399 cache_info,
1400 revision,
1401 } = self
1402 .source_tree_revision(source, resource, &cache_shard)
1403 .await?;
1404
1405 let cache_shard = cache_shard.shard(revision.id());
1408
1409 let config_settings = self.config_settings_for(source.name());
1411 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1412 let extra_build_variables = self.extra_build_variables_for(source.name());
1413 let build_info = BuildInfo::from_settings(
1414 config_settings.into_owned(),
1415 extra_build_deps.to_vec(),
1416 extra_build_variables.cloned(),
1417 );
1418 let cache_shard = build_info
1419 .cache_shard()
1420 .map(|digest| cache_shard.shard(digest))
1421 .unwrap_or(cache_shard);
1422
1423 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1425 .ok()
1426 .flatten()
1427 .filter(|file| file.matches(source.name(), source.version()))
1428 {
1429 return Ok(BuiltWheelMetadata::from_file(
1430 file,
1431 revision.into_hashes(),
1432 cache_info,
1433 build_info,
1434 ));
1435 }
1436
1437 let task = self
1439 .reporter
1440 .as_ref()
1441 .map(|reporter| reporter.on_build_start(source));
1442
1443 let (disk_filename, filename, metadata) = self
1444 .build_distribution(
1445 source,
1446 resource.install_path,
1447 None,
1448 &cache_shard,
1449 self.build_context.sources().clone(),
1450 )
1451 .await?;
1452
1453 if let Some(task) = task {
1454 if let Some(reporter) = self.reporter.as_ref() {
1455 reporter.on_build_complete(source, task);
1456 }
1457 }
1458
1459 let metadata_entry = cache_shard.entry(METADATA);
1461 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1462 .await
1463 .map_err(Error::CacheWrite)?;
1464
1465 Ok(BuiltWheelMetadata {
1466 path: cache_shard.join(&disk_filename).into_boxed_path(),
1467 target: cache_shard.join(filename.stem()).into_boxed_path(),
1468 filename,
1469 hashes: revision.into_hashes(),
1470 cache_info,
1471 build_info,
1472 })
1473 }
1474
1475 async fn source_tree_metadata(
1481 &self,
1482 source: &BuildableSource<'_>,
1483 resource: &DirectorySourceUrl<'_>,
1484 hashes: HashPolicy<'_>,
1485 credentials_cache: &CredentialsCache,
1486 ) -> Result<ArchiveMetadata, Error> {
1487 if hashes.requires_validation() {
1489 return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
1490 }
1491
1492 let editable = self
1496 .build_context
1497 .source_tree_editable_policy()
1498 .workspace_member_editable(resource.editable);
1499
1500 let dynamic = match StaticMetadata::read(source, resource.install_path, None).await? {
1502 StaticMetadata::Some(metadata) => {
1503 return Ok(ArchiveMetadata::from(
1504 Metadata::from_workspace(
1505 metadata,
1506 resource.install_path,
1507 None,
1508 self.build_context.locations(),
1509 self.build_context.sources().clone(),
1510 editable,
1511 self.build_context.cache(),
1512 self.build_context.workspace_cache(),
1513 credentials_cache,
1514 )
1515 .await?,
1516 ));
1517 }
1518 StaticMetadata::Dynamic => true,
1519 StaticMetadata::None => false,
1520 };
1521
1522 let cache_shard = self.build_context.cache().shard(
1523 CacheBucket::SourceDistributions,
1524 if resource.editable.unwrap_or(false) {
1525 WheelCache::Editable(resource.url).root()
1526 } else {
1527 WheelCache::Path(resource.url).root()
1528 },
1529 );
1530
1531 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1533
1534 let LocalRevisionPointer { revision, .. } = self
1536 .source_tree_revision(source, resource, &cache_shard)
1537 .await?;
1538
1539 let cache_shard = cache_shard.shard(revision.id());
1542
1543 let metadata_entry = cache_shard.entry(METADATA);
1545 match CachedMetadata::read(&metadata_entry).await {
1546 Ok(Some(metadata)) => {
1547 if metadata.matches(source.name(), source.version()) {
1548 debug!("Using cached metadata for: {source}");
1549
1550 let metadata = if dynamic {
1552 ResolutionMetadata {
1553 dynamic: true,
1554 ..metadata.into()
1555 }
1556 } else {
1557 metadata.into()
1558 };
1559 return Ok(ArchiveMetadata::from(
1560 Metadata::from_workspace(
1561 metadata,
1562 resource.install_path,
1563 None,
1564 self.build_context.locations(),
1565 self.build_context.sources().clone(),
1566 editable,
1567 self.build_context.cache(),
1568 self.build_context.workspace_cache(),
1569 credentials_cache,
1570 )
1571 .await?,
1572 ));
1573 }
1574 debug!("Cached metadata does not match expected name and version for: {source}");
1575 }
1576 Ok(None) => {}
1577 Err(err) => {
1578 debug!("Failed to deserialize cached metadata for: {source} ({err})");
1579 }
1580 }
1581
1582 if let Some(metadata) = self
1584 .build_metadata(
1585 source,
1586 resource.install_path,
1587 None,
1588 self.build_context.sources().clone(),
1589 )
1590 .boxed_local()
1591 .await?
1592 {
1593 fs::create_dir_all(metadata_entry.dir())
1595 .await
1596 .map_err(Error::CacheWrite)?;
1597 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1598 .await
1599 .map_err(Error::CacheWrite)?;
1600
1601 let metadata = if dynamic {
1603 ResolutionMetadata {
1604 dynamic: true,
1605 ..metadata
1606 }
1607 } else {
1608 metadata
1609 };
1610
1611 return Ok(ArchiveMetadata::from(
1612 Metadata::from_workspace(
1613 metadata,
1614 resource.install_path,
1615 None,
1616 self.build_context.locations(),
1617 self.build_context.sources().clone(),
1618 editable,
1619 self.build_context.cache(),
1620 self.build_context.workspace_cache(),
1621 credentials_cache,
1622 )
1623 .await?,
1624 ));
1625 }
1626
1627 let config_settings = self.config_settings_for(source.name());
1629 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1630 let extra_build_variables = self.extra_build_variables_for(source.name());
1631 let build_info = BuildInfo::from_settings(
1632 config_settings.into_owned(),
1633 extra_build_deps.to_vec(),
1634 extra_build_variables.cloned(),
1635 );
1636 let cache_shard = build_info
1637 .cache_shard()
1638 .map(|digest| cache_shard.shard(digest))
1639 .unwrap_or(cache_shard);
1640
1641 let task = self
1643 .reporter
1644 .as_ref()
1645 .map(|reporter| reporter.on_build_start(source));
1646
1647 let (_disk_filename, _filename, metadata) = self
1648 .build_distribution(
1649 source,
1650 resource.install_path,
1651 None,
1652 &cache_shard,
1653 self.build_context.sources().clone(),
1654 )
1655 .await?;
1656
1657 if let Some(task) = task {
1658 if let Some(reporter) = self.reporter.as_ref() {
1659 reporter.on_build_complete(source, task);
1660 }
1661 }
1662
1663 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1665 .await
1666 .map_err(Error::CacheWrite)?;
1667
1668 let metadata = if dynamic {
1670 ResolutionMetadata {
1671 dynamic: true,
1672 ..metadata
1673 }
1674 } else {
1675 metadata
1676 };
1677
1678 Ok(ArchiveMetadata::from(
1679 Metadata::from_workspace(
1680 metadata,
1681 resource.install_path,
1682 None,
1683 self.build_context.locations(),
1684 self.build_context.sources().clone(),
1685 editable,
1686 self.build_context.cache(),
1687 self.build_context.workspace_cache(),
1688 credentials_cache,
1689 )
1690 .await?,
1691 ))
1692 }
1693
1694 async fn source_tree_revision(
1696 &self,
1697 source: &BuildableSource<'_>,
1698 resource: &DirectorySourceUrl<'_>,
1699 cache_shard: &CacheShard,
1700 ) -> Result<LocalRevisionPointer, Error> {
1701 if !resource.install_path.is_dir() {
1703 return Err(Error::NotFound(resource.url.clone()));
1704 }
1705
1706 let cache_info = CacheInfo::from_directory(resource.install_path)?;
1708
1709 let entry = cache_shard.entry(LOCAL_REVISION);
1711
1712 if self
1714 .build_context
1715 .cache()
1716 .freshness(&entry, source.name(), source.source_tree())
1717 .map_err(Error::CacheRead)?
1718 .is_fresh()
1719 {
1720 match LocalRevisionPointer::read_from(&entry) {
1721 Ok(Some(pointer)) => {
1722 if *pointer.cache_info() == cache_info {
1723 return Ok(pointer);
1724 }
1725
1726 debug!("Cached revision does not match expected cache info for: {source}");
1727 }
1728 Ok(None) => {}
1729 Err(err) => {
1730 debug!("Failed to deserialize cached revision for: {source} ({err})");
1731 }
1732 }
1733 }
1734
1735 let revision = Revision::new();
1737 let pointer = LocalRevisionPointer {
1738 cache_info,
1739 revision,
1740 };
1741 pointer.write_to(&entry).await?;
1742
1743 Ok(pointer)
1744 }
1745
1746 pub(crate) async fn source_tree_requires_dist(
1748 &self,
1749 path: &Path,
1750 pyproject_toml: &PyProjectToml,
1751 credentials_cache: &CredentialsCache,
1752 ) -> Result<Option<RequiresDist>, Error> {
1753 match uv_pypi_types::RequiresDist::from_pyproject_toml(pyproject_toml.clone()) {
1755 Ok(requires_dist) => {
1756 debug!("Found static `requires-dist` for: {}", path.display());
1757 let requires_dist = RequiresDist::from_project_maybe_workspace(
1758 requires_dist,
1759 path,
1760 None,
1761 self.build_context.locations(),
1762 self.build_context.sources().clone(),
1763 self.build_context
1764 .source_tree_editable_policy()
1765 .workspace_member_editable(None),
1766 self.build_context.cache(),
1767 self.build_context.workspace_cache(),
1768 credentials_cache,
1769 )
1770 .await?;
1771 Ok(Some(requires_dist))
1772 }
1773 Err(
1774 err @ (uv_pypi_types::MetadataError::Pep508Error(_)
1775 | uv_pypi_types::MetadataError::DynamicField(_)
1776 | uv_pypi_types::MetadataError::FieldNotFound(_)
1777 | uv_pypi_types::MetadataError::PoetrySyntax),
1778 ) => {
1779 debug!(
1780 "No static `requires-dist` available for: {} ({err:?})",
1781 path.display()
1782 );
1783 Ok(None)
1784 }
1785 Err(err) => Err(Error::PyprojectToml(err)),
1786 }
1787 }
1788
1789 async fn git_archive_revision(
1791 &self,
1792 source: &BuildableSource<'_>,
1793 resource: &GitPathSourceUrl<'_>,
1794 fetch: &Fetch,
1795 cache_shard: &CacheShard,
1796 hashes: HashPolicy<'_>,
1797 ) -> Result<RevisionHashes, Error> {
1798 if resource.git.lfs().enabled() && !fetch.lfs_ready() {
1800 if GIT_LFS.is_err() {
1801 return Err(Error::MissingSourceDistGitLfsArtifacts(
1802 resource.url.to_url(),
1803 GitError::GitLfsNotFound,
1804 ));
1805 }
1806 return Err(Error::MissingSourceDistGitLfsArtifacts(
1807 resource.url.to_url(),
1808 GitError::GitLfsNotConfigured,
1809 ));
1810 }
1811
1812 let install_path = fetch.path().join(&resource.path);
1814 if !install_path.is_file() {
1815 return Err(Error::NotFound(resource.url.to_url()));
1816 }
1817
1818 let revision_entry = cache_shard.entry(HASHES);
1820
1821 if let Some(revision) = RevisionHashes::read_from(&revision_entry)? {
1824 if revision.has_digests(hashes) {
1825 return Ok(revision);
1826 }
1827 }
1828
1829 debug!("Unpacking source distribution: {source}");
1831 let entry = cache_shard.entry(SOURCE);
1832 let algorithms = hashes.algorithms();
1833 let hashes = self
1834 .persist_archive(&install_path, resource.ext, entry.path(), &algorithms)
1835 .await?;
1836
1837 let revision = RevisionHashes { hashes };
1839 revision.write_to(&revision_entry).await?;
1840
1841 Ok(revision)
1842 }
1843
1844 async fn git_archive(
1846 &self,
1847 source: &BuildableSource<'_>,
1848 resource: &GitPathSourceUrl<'_>,
1849 tags: &Tags,
1850 hashes: HashPolicy<'_>,
1851 client: &ManagedClient<'_>,
1852 ) -> Result<BuiltWheelMetadata, Error> {
1853 let fetch = self
1855 .build_context
1856 .git()
1857 .fetch(
1858 resource.git,
1859 client.unmanaged.git_http_settings(resource.git.url()),
1860 self.build_context.cache().bucket(CacheBucket::Git),
1861 self.reporter
1862 .clone()
1863 .map(|reporter| reporter.into_git_reporter()),
1864 )
1865 .await?;
1866
1867 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
1868 let cache_shard = self.build_context.cache().shard(
1869 CacheBucket::SourceDistributions,
1870 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
1871 );
1872
1873 let revision = self
1875 .git_archive_revision(source, resource, &fetch, &cache_shard, hashes)
1876 .await?;
1877
1878 if !revision.satisfies(hashes) {
1880 return Err(Error::hash_mismatch(
1881 source.to_string(),
1882 hashes.digests(),
1883 revision.hashes(),
1884 ));
1885 }
1886
1887 let source_entry = cache_shard.entry(SOURCE);
1888
1889 let config_settings = self.config_settings_for(source.name());
1891 let extra_build_deps = self.extra_build_dependencies_for(source.name());
1892 let extra_build_variables = self.extra_build_variables_for(source.name());
1893 let build_info = BuildInfo::from_settings(
1894 config_settings.into_owned(),
1895 extra_build_deps.to_vec(),
1896 extra_build_variables.cloned(),
1897 );
1898 let cache_shard = build_info
1899 .cache_shard()
1900 .map(|digest| cache_shard.shard(digest))
1901 .unwrap_or(cache_shard);
1902
1903 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1905 .ok()
1906 .flatten()
1907 .filter(|file| file.matches(source.name(), source.version()))
1908 {
1909 return Ok(BuiltWheelMetadata::from_file(
1910 file,
1911 revision.into_hashes(),
1912 CacheInfo::default(),
1913 build_info,
1914 ));
1915 }
1916
1917 let task = self
1919 .reporter
1920 .as_ref()
1921 .map(|reporter| reporter.on_build_start(source));
1922
1923 let (disk_filename, filename, metadata) = self
1924 .build_distribution(
1925 source,
1926 source_entry.path(),
1927 None,
1928 &cache_shard,
1929 NoSources::None,
1930 )
1931 .await?;
1932
1933 if let Some(task) = task {
1934 if let Some(reporter) = self.reporter.as_ref() {
1935 reporter.on_build_complete(source, task);
1936 }
1937 }
1938
1939 let metadata_entry = cache_shard.entry(METADATA);
1941 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1942 .await
1943 .map_err(Error::CacheWrite)?;
1944
1945 Ok(BuiltWheelMetadata {
1946 path: cache_shard.join(&disk_filename).into_boxed_path(),
1947 target: cache_shard.join(filename.stem()).into_boxed_path(),
1948 filename,
1949 hashes: revision.into_hashes(),
1950 cache_info: CacheInfo::default(),
1951 build_info,
1952 })
1953 }
1954
1955 async fn git_archive_metadata(
1957 &self,
1958 source: &BuildableSource<'_>,
1959 resource: &GitPathSourceUrl<'_>,
1960 hashes: HashPolicy<'_>,
1961 client: &ManagedClient<'_>,
1962 ) -> Result<ArchiveMetadata, Error> {
1963 let fetch = self
1965 .build_context
1966 .git()
1967 .fetch(
1968 resource.git,
1969 client.unmanaged.git_http_settings(resource.git.url()),
1970 self.build_context.cache().bucket(CacheBucket::Git),
1971 self.reporter
1972 .clone()
1973 .map(|reporter| reporter.into_git_reporter()),
1974 )
1975 .await?;
1976
1977 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
1978 let cache_shard = self.build_context.cache().shard(
1979 CacheBucket::SourceDistributions,
1980 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
1981 );
1982
1983 let revision = self
1985 .git_archive_revision(source, resource, &fetch, &cache_shard, hashes)
1986 .await?;
1987
1988 if !revision.satisfies(hashes) {
1990 return Err(Error::hash_mismatch(
1991 source.to_string(),
1992 hashes.digests(),
1993 revision.hashes(),
1994 ));
1995 }
1996
1997 let source_entry = cache_shard.entry(SOURCE);
1998
1999 let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
2001 StaticMetadata::Some(metadata) => {
2002 return Ok(ArchiveMetadata {
2003 metadata: Metadata::from_metadata23(metadata),
2004 hashes: revision.into_hashes(),
2005 });
2006 }
2007 StaticMetadata::Dynamic => true,
2008 StaticMetadata::None => false,
2009 };
2010
2011 let metadata_entry = cache_shard.entry(METADATA);
2013 match CachedMetadata::read(&metadata_entry).await {
2014 Ok(Some(metadata)) => {
2015 if metadata.matches(source.name(), source.version()) {
2016 debug!("Using cached metadata for: {source}");
2017 return Ok(ArchiveMetadata {
2018 metadata: Metadata::from_metadata23(metadata.into()),
2019 hashes: revision.into_hashes(),
2020 });
2021 }
2022 debug!("Cached metadata does not match expected name and version for: {source}");
2023 }
2024 Ok(None) => {}
2025 Err(err) => {
2026 debug!("Failed to deserialize cached metadata for: {source} ({err})");
2027 }
2028 }
2029
2030 if let Some(metadata) = self
2032 .build_metadata(source, source_entry.path(), None, NoSources::None)
2033 .boxed_local()
2034 .await?
2035 {
2036 let metadata = if dynamic {
2038 ResolutionMetadata {
2039 dynamic: true,
2040 ..metadata
2041 }
2042 } else {
2043 metadata
2044 };
2045
2046 fs::create_dir_all(metadata_entry.dir())
2048 .await
2049 .map_err(Error::CacheWrite)?;
2050 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2051 .await
2052 .map_err(Error::CacheWrite)?;
2053
2054 return Ok(ArchiveMetadata {
2055 metadata: Metadata::from_metadata23(metadata),
2056 hashes: revision.into_hashes(),
2057 });
2058 }
2059
2060 let config_settings = self.config_settings_for(source.name());
2062 let extra_build_deps = self.extra_build_dependencies_for(source.name());
2063 let extra_build_variables = self.extra_build_variables_for(source.name());
2064 let build_info = BuildInfo::from_settings(
2065 config_settings.into_owned(),
2066 extra_build_deps.to_vec(),
2067 extra_build_variables.cloned(),
2068 );
2069 let cache_shard = build_info
2070 .cache_shard()
2071 .map(|digest| cache_shard.shard(digest))
2072 .unwrap_or(cache_shard);
2073
2074 let task = self
2076 .reporter
2077 .as_ref()
2078 .map(|reporter| reporter.on_build_start(source));
2079
2080 let (_disk_filename, _filename, metadata) = self
2081 .build_distribution(
2082 source,
2083 source_entry.path(),
2084 None,
2085 &cache_shard,
2086 NoSources::None,
2087 )
2088 .await?;
2089
2090 if let Some(task) = task {
2091 if let Some(reporter) = self.reporter.as_ref() {
2092 reporter.on_build_complete(source, task);
2093 }
2094 }
2095
2096 let metadata = if dynamic {
2098 ResolutionMetadata {
2099 dynamic: true,
2100 ..metadata
2101 }
2102 } else {
2103 metadata
2104 };
2105
2106 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2108 .await
2109 .map_err(Error::CacheWrite)?;
2110
2111 Ok(ArchiveMetadata {
2112 metadata: Metadata::from_metadata23(metadata),
2113 hashes: revision.into_hashes(),
2114 })
2115 }
2116
2117 async fn git_source_tree(
2119 &self,
2120 source: &BuildableSource<'_>,
2121 resource: &GitDirectorySourceUrl<'_>,
2122 tags: &Tags,
2123 hashes: HashPolicy<'_>,
2124 client: &ManagedClient<'_>,
2125 ) -> Result<BuiltWheelMetadata, Error> {
2126 if hashes.requires_validation() {
2128 return Err(Error::HashesNotSupportedGit(source.to_string()));
2129 }
2130
2131 let fetch = fetch_git_source_tree(
2132 self.build_context.git(),
2133 resource.git,
2134 resource.url.to_url(),
2135 resource.subdirectory,
2136 client.unmanaged.git_http_settings(resource.git.url()),
2137 self.build_context.cache(),
2138 self.reporter
2139 .clone()
2140 .map(|reporter| reporter.into_git_reporter()),
2141 )
2142 .await?;
2143
2144 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
2145 let cache_shard = self.build_context.cache().shard(
2146 CacheBucket::SourceDistributions,
2147 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
2148 );
2149 let metadata_entry = cache_shard.entry(METADATA);
2150
2151 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
2153
2154 let cache_info = CacheInfo::default();
2157
2158 let hashes = HashDigests::empty();
2161
2162 let config_settings = self.config_settings_for(source.name());
2164 let extra_build_deps = self.extra_build_dependencies_for(source.name());
2165 let extra_build_variables = self.extra_build_variables_for(source.name());
2166 let build_info = BuildInfo::from_settings(
2167 config_settings.into_owned(),
2168 extra_build_deps.to_vec(),
2169 extra_build_variables.cloned(),
2170 );
2171 let cache_shard = build_info
2172 .cache_shard()
2173 .map(|digest| cache_shard.shard(digest))
2174 .unwrap_or(cache_shard);
2175
2176 if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
2178 .ok()
2179 .flatten()
2180 .filter(|file| file.matches(source.name(), source.version()))
2181 {
2182 return Ok(BuiltWheelMetadata::from_file(
2183 file, hashes, cache_info, build_info,
2184 ));
2185 }
2186
2187 let task = self
2188 .reporter
2189 .as_ref()
2190 .map(|reporter| reporter.on_build_start(source));
2191
2192 let (disk_filename, filename, metadata) = self
2193 .build_distribution(
2194 source,
2195 fetch.path(),
2196 resource.subdirectory,
2197 &cache_shard,
2198 self.build_context.sources().clone(),
2199 )
2200 .await?;
2201
2202 if let Some(task) = task {
2203 if let Some(reporter) = self.reporter.as_ref() {
2204 reporter.on_build_complete(source, task);
2205 }
2206 }
2207
2208 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2210 .await
2211 .map_err(Error::CacheWrite)?;
2212
2213 Ok(BuiltWheelMetadata {
2214 path: cache_shard.join(&disk_filename).into_boxed_path(),
2215 target: cache_shard.join(filename.stem()).into_boxed_path(),
2216 filename,
2217 hashes,
2218 cache_info,
2219 build_info,
2220 })
2221 }
2222
2223 async fn git_source_tree_metadata(
2228 &self,
2229 source: &BuildableSource<'_>,
2230 resource: &GitDirectorySourceUrl<'_>,
2231 hashes: HashPolicy<'_>,
2232 client: &ManagedClient<'_>,
2233 credentials_cache: &CredentialsCache,
2234 ) -> Result<ArchiveMetadata, Error> {
2235 if hashes.requires_validation() {
2237 return Err(Error::HashesNotSupportedGit(source.to_string()));
2238 }
2239
2240 let cache_shard = resource
2243 .git
2244 .reference()
2245 .as_str()
2246 .and_then(|reference| GitOid::from_str(reference).ok())
2247 .map(|oid| {
2248 self.build_context.cache().shard(
2249 CacheBucket::SourceDistributions,
2250 WheelCache::Git(resource.url, oid.as_short_str()).root(),
2251 )
2252 });
2253 if cache_shard
2254 .as_ref()
2255 .is_some_and(|cache_shard| cache_shard.is_dir())
2256 {
2257 debug!("Skipping GitHub fast path for: {source} (shard exists)");
2258 } else {
2259 debug!("Attempting GitHub fast path for: {source}");
2260
2261 match self
2263 .build_context
2264 .git()
2265 .github_fast_path(
2266 resource.git,
2267 client
2268 .unmanaged
2269 .uncached_client(resource.git.url())
2270 .raw_client(),
2271 )
2272 .await
2273 {
2274 Ok(Some(precise)) => {
2275 match self
2282 .github_metadata(precise, source, resource, client)
2283 .await
2284 {
2285 Ok(Some(metadata)) => {
2286 match validate_metadata(source, &metadata) {
2288 Ok(()) => {
2289 debug!(
2290 "Found static metadata via GitHub fast path for: {source}"
2291 );
2292 return Ok(ArchiveMetadata {
2293 metadata: Metadata::from_metadata23(metadata),
2294 hashes: HashDigests::empty(),
2295 });
2296 }
2297 Err(err) => {
2298 debug!(
2299 "Ignoring `pyproject.toml` from GitHub for {source}: {err}"
2300 );
2301 }
2302 }
2303 }
2304 Ok(None) => {
2305 }
2307 Err(err) => {
2308 debug!(
2309 "Failed to fetch `pyproject.toml` via GitHub fast path for: {source} ({err})"
2310 );
2311 }
2312 }
2313 }
2314 Ok(None) => {
2315 }
2317 Err(err) => {
2318 debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})");
2319 }
2320 }
2321 }
2322
2323 let fetch = fetch_git_source_tree(
2324 self.build_context.git(),
2325 resource.git,
2326 resource.url.to_url(),
2327 resource.subdirectory,
2328 client.unmanaged.git_http_settings(resource.git.url()),
2329 self.build_context.cache(),
2330 self.reporter
2331 .clone()
2332 .map(|reporter| reporter.into_git_reporter()),
2333 )
2334 .await?;
2335
2336 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
2337 let cache_shard = self.build_context.cache().shard(
2338 CacheBucket::SourceDistributions,
2339 WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
2340 );
2341 let metadata_entry = cache_shard.entry(METADATA);
2342
2343 let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
2345
2346 let path = if let Some(subdirectory) = resource.subdirectory {
2347 Cow::Owned(fetch.path().join(subdirectory))
2348 } else {
2349 Cow::Borrowed(fetch.path())
2350 };
2351
2352 let git_member = GitWorkspaceMember {
2353 fetch_root: fetch.path(),
2354 git_source: resource,
2355 };
2356
2357 let dynamic =
2359 match StaticMetadata::read(source, fetch.path(), resource.subdirectory).await? {
2360 StaticMetadata::Some(metadata) => {
2361 return Ok(ArchiveMetadata::from(
2362 Metadata::from_workspace(
2363 metadata,
2364 &path,
2365 Some(&git_member),
2366 self.build_context.locations(),
2367 self.build_context.sources().clone(),
2368 self.build_context
2369 .source_tree_editable_policy()
2370 .workspace_member_editable(None),
2371 self.build_context.cache(),
2372 self.build_context.workspace_cache(),
2373 credentials_cache,
2374 )
2375 .await?,
2376 ));
2377 }
2378 StaticMetadata::Dynamic => true,
2379 StaticMetadata::None => false,
2380 };
2381
2382 if self
2384 .build_context
2385 .cache()
2386 .freshness(&metadata_entry, source.name(), source.source_tree())
2387 .map_err(Error::CacheRead)?
2388 .is_fresh()
2389 {
2390 match CachedMetadata::read(&metadata_entry).await {
2391 Ok(Some(metadata)) => {
2392 if metadata.matches(source.name(), source.version()) {
2393 debug!("Using cached metadata for: {source}");
2394
2395 let git_member = GitWorkspaceMember {
2396 fetch_root: fetch.path(),
2397 git_source: resource,
2398 };
2399 return Ok(ArchiveMetadata::from(
2400 Metadata::from_workspace(
2401 metadata.into(),
2402 &path,
2403 Some(&git_member),
2404 self.build_context.locations(),
2405 self.build_context.sources().clone(),
2406 self.build_context
2407 .source_tree_editable_policy()
2408 .workspace_member_editable(None),
2409 self.build_context.cache(),
2410 self.build_context.workspace_cache(),
2411 credentials_cache,
2412 )
2413 .await?,
2414 ));
2415 }
2416 debug!(
2417 "Cached metadata does not match expected name and version for: {source}"
2418 );
2419 }
2420 Ok(None) => {}
2421 Err(err) => {
2422 debug!("Failed to deserialize cached metadata for: {source} ({err})");
2423 }
2424 }
2425 }
2426
2427 if let Some(metadata) = self
2429 .build_metadata(
2430 source,
2431 fetch.path(),
2432 resource.subdirectory,
2433 self.build_context.sources().clone(),
2434 )
2435 .boxed_local()
2436 .await?
2437 {
2438 let metadata = if dynamic {
2440 ResolutionMetadata {
2441 dynamic: true,
2442 ..metadata
2443 }
2444 } else {
2445 metadata
2446 };
2447
2448 fs::create_dir_all(metadata_entry.dir())
2450 .await
2451 .map_err(Error::CacheWrite)?;
2452 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2453 .await
2454 .map_err(Error::CacheWrite)?;
2455
2456 return Ok(ArchiveMetadata::from(
2457 Metadata::from_workspace(
2458 metadata,
2459 &path,
2460 Some(&git_member),
2461 self.build_context.locations(),
2462 self.build_context.sources().clone(),
2463 self.build_context
2464 .source_tree_editable_policy()
2465 .workspace_member_editable(None),
2466 self.build_context.cache(),
2467 self.build_context.workspace_cache(),
2468 credentials_cache,
2469 )
2470 .await?,
2471 ));
2472 }
2473
2474 let config_settings = self.config_settings_for(source.name());
2476 let extra_build_deps = self.extra_build_dependencies_for(source.name());
2477 let extra_build_variables = self.extra_build_variables_for(source.name());
2478 let build_info = BuildInfo::from_settings(
2479 config_settings.into_owned(),
2480 extra_build_deps.to_vec(),
2481 extra_build_variables.cloned(),
2482 );
2483 let cache_shard = build_info
2484 .cache_shard()
2485 .map(|digest| cache_shard.shard(digest))
2486 .unwrap_or(cache_shard);
2487
2488 let task = self
2490 .reporter
2491 .as_ref()
2492 .map(|reporter| reporter.on_build_start(source));
2493
2494 let (_disk_filename, _filename, metadata) = self
2495 .build_distribution(
2496 source,
2497 fetch.path(),
2498 resource.subdirectory,
2499 &cache_shard,
2500 self.build_context.sources().clone(),
2501 )
2502 .await?;
2503
2504 if let Some(task) = task {
2505 if let Some(reporter) = self.reporter.as_ref() {
2506 reporter.on_build_complete(source, task);
2507 }
2508 }
2509
2510 let metadata = if dynamic {
2512 ResolutionMetadata {
2513 dynamic: true,
2514 ..metadata
2515 }
2516 } else {
2517 metadata
2518 };
2519
2520 write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2522 .await
2523 .map_err(Error::CacheWrite)?;
2524
2525 Ok(ArchiveMetadata::from(
2526 Metadata::from_workspace(
2527 metadata,
2528 fetch.path(),
2529 Some(&git_member),
2530 self.build_context.locations(),
2531 self.build_context.sources().clone(),
2532 self.build_context
2533 .source_tree_editable_policy()
2534 .workspace_member_editable(None),
2535 self.build_context.cache(),
2536 self.build_context.workspace_cache(),
2537 credentials_cache,
2538 )
2539 .await?,
2540 ))
2541 }
2542
2543 pub(crate) async fn resolve_revision(
2545 &self,
2546 source: &BuildableSource<'_>,
2547 client: &ManagedClient<'_>,
2548 ) -> Result<Option<GitOid>, Error> {
2549 let git = match source {
2550 BuildableSource::Dist(SourceDist::GitDirectory(source)) => &*source.git,
2551 BuildableSource::Dist(SourceDist::GitPath(source)) => &*source.git,
2552 BuildableSource::Url(SourceUrl::GitDirectory(source)) => source.git,
2553 BuildableSource::Url(SourceUrl::GitPath(source)) => source.git,
2554 _ => {
2555 return Ok(None);
2556 }
2557 };
2558
2559 if let Some(precise) = self.build_context.git().get_precise(git) {
2561 debug!("Precise commit already known: {source}");
2562 return Ok(Some(precise));
2563 }
2564
2565 if let Some(precise) = self
2567 .build_context
2568 .git()
2569 .github_fast_path(
2570 git,
2571 client.unmanaged.uncached_client(git.url()).raw_client(),
2572 )
2573 .await?
2574 {
2575 debug!("Resolved to precise commit via GitHub fast path: {source}");
2576 return Ok(Some(precise));
2577 }
2578
2579 let fetch = self
2581 .build_context
2582 .git()
2583 .fetch(
2584 git,
2585 client.unmanaged.git_http_settings(git.url()),
2586 self.build_context.cache().bucket(CacheBucket::Git),
2587 self.reporter
2588 .clone()
2589 .map(|reporter| reporter.into_git_reporter()),
2590 )
2591 .await?;
2592
2593 Ok(fetch.git().precise())
2594 }
2595
2596 async fn github_metadata(
2600 &self,
2601 commit: GitOid,
2602 source: &BuildableSource<'_>,
2603 resource: &GitDirectorySourceUrl<'_>,
2604 client: &ManagedClient<'_>,
2605 ) -> Result<Option<ResolutionMetadata>, Error> {
2606 let GitDirectorySourceUrl {
2607 git, subdirectory, ..
2608 } = resource;
2609
2610 if subdirectory.is_some() {
2614 return Ok(None);
2615 }
2616
2617 let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(git.repository())
2618 else {
2619 return Ok(None);
2620 };
2621
2622 let url =
2624 format!("https://raw.githubusercontent.com/{owner}/{repo}/{commit}/pyproject.toml");
2625
2626 debug!("Attempting to fetch `pyproject.toml` from: {url}");
2627
2628 let content = client
2629 .managed(async |client| {
2630 let response = client.uncached_client(git.url()).get(&url).send().await?;
2631
2632 if response.status() == StatusCode::NOT_FOUND {
2634 return Ok::<Option<String>, Error>(None);
2635 }
2636 response.error_for_status_ref()?;
2637
2638 let content = response.text().await?;
2639 Ok::<Option<String>, Error>(Some(content))
2640 })
2641 .await?;
2642
2643 let Some(content) = content else {
2644 debug!("GitHub API returned a 404 for: {url}");
2645 return Ok(None);
2646 };
2647
2648 let pyproject_toml = match PyProjectToml::from_toml(&content, source) {
2650 Ok(metadata) => metadata,
2651 Err(
2652 uv_pypi_types::MetadataError::InvalidPyprojectTomlSyntax(..)
2653 | uv_pypi_types::MetadataError::InvalidPyprojectTomlSchema(..),
2654 ) => {
2655 debug!("Failed to read `pyproject.toml` from GitHub API for: {url}");
2656 return Ok(None);
2657 }
2658 Err(err) => return Err(err.into()),
2659 };
2660
2661 let metadata =
2663 match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) {
2664 Ok(metadata) => metadata,
2665 Err(
2666 uv_pypi_types::MetadataError::Pep508Error(..)
2667 | uv_pypi_types::MetadataError::DynamicField(..)
2668 | uv_pypi_types::MetadataError::FieldNotFound(..)
2669 | uv_pypi_types::MetadataError::PoetrySyntax,
2670 ) => {
2671 debug!("Failed to extract static metadata from GitHub API for: {url}");
2672 return Ok(None);
2673 }
2674 Err(err) => return Err(err.into()),
2675 };
2676
2677 match has_sources(&content) {
2686 Ok(false) => {}
2687 Ok(true) => {
2688 debug!("Skipping GitHub fast path; `pyproject.toml` has sources: {url}");
2689 return Ok(None);
2690 }
2691 Err(err) => {
2692 debug!("Failed to parse `tool.uv.sources` from GitHub API for: {url} ({err})");
2693 return Ok(None);
2694 }
2695 }
2696
2697 Ok(Some(metadata))
2698 }
2699
2700 async fn heal_archive_revision(
2702 &self,
2703 source: &BuildableSource<'_>,
2704 resource: &PathSourceUrl<'_>,
2705 entry: &CacheEntry,
2706 revision: Revision,
2707 hashes: HashPolicy<'_>,
2708 ) -> Result<Revision, Error> {
2709 warn!("Re-extracting missing source distribution: {source}");
2710
2711 let algorithms = {
2713 let mut algorithms = hashes.algorithms();
2714 for digest in revision.hashes() {
2715 algorithms.push(digest.algorithm());
2716 }
2717 algorithms.sort();
2718 algorithms.dedup();
2719 algorithms
2720 };
2721
2722 let hashes = self
2723 .persist_archive(&resource.path, resource.ext, entry.path(), &algorithms)
2724 .await?;
2725 for existing in revision.hashes() {
2726 if !hashes.contains(existing) {
2727 return Err(Error::CacheHeal(source.to_string(), existing.algorithm()));
2728 }
2729 }
2730 Ok(revision.with_hashes(HashDigests::from(hashes)))
2731 }
2732
2733 async fn heal_url_revision(
2735 &self,
2736 source: &BuildableSource<'_>,
2737 ext: SourceDistExtension,
2738 url: &DisplaySafeUrl,
2739 index: Option<&IndexUrl>,
2740 entry: &CacheEntry,
2741 revision: Revision,
2742 hashes: HashPolicy<'_>,
2743 client: &ManagedClient<'_>,
2744 ) -> Result<Revision, Error> {
2745 warn!("Re-downloading missing source distribution: {source}");
2746 let cache_entry = entry.shard().entry(HTTP_REVISION);
2747
2748 let cache_control = match client.unmanaged.connectivity() {
2750 Connectivity::Online
2751 if let Some(header) = index.and_then(|index| {
2752 self.build_context
2753 .locations()
2754 .artifact_cache_control_for(index)
2755 }) =>
2756 {
2757 CacheControl::Override(header)
2758 }
2759 Connectivity::Online => CacheControl::from(
2760 self.build_context
2761 .cache()
2762 .freshness(&cache_entry, source.name(), source.source_tree())
2763 .map_err(Error::CacheRead)?,
2764 ),
2765 Connectivity::Offline => CacheControl::AllowStale,
2766 };
2767
2768 let download = |response| {
2769 async {
2770 let algorithms = {
2772 let mut algorithms = http_hash_algorithms(hashes);
2773 for digest in revision.hashes() {
2774 algorithms.push(digest.algorithm());
2775 }
2776 algorithms.sort();
2777 algorithms.dedup();
2778 algorithms
2779 };
2780
2781 let (hashes, size) = self
2782 .download_archive(response, source, ext, entry.path(), &algorithms)
2783 .await?;
2784 for existing in revision.hashes() {
2785 if !hashes.contains(existing) {
2786 return Err(Error::CacheHeal(source.to_string(), existing.algorithm()));
2787 }
2788 }
2789 Ok(revision
2790 .clone()
2791 .with_hashes(HashDigests::from(hashes))
2792 .with_size(size))
2793 }
2794 .boxed_local()
2795 .instrument(info_span!("download", source_dist = %source))
2796 };
2797 client
2798 .managed(async |client| {
2799 client
2800 .cached_client()
2801 .skip_cache_with_retry(
2802 Self::request(url.clone(), client)?,
2803 &cache_entry,
2804 cache_control.clone(),
2805 download,
2806 )
2807 .await
2808 .map_err(|err| match err {
2809 CachedClientError::Callback { err, .. } => err,
2810 CachedClientError::Client(err) => Error::Client(err),
2811 })
2812 })
2813 .await
2814 }
2815
2816 async fn download_archive(
2818 &self,
2819 response: Response,
2820 source: &BuildableSource<'_>,
2821 ext: SourceDistExtension,
2822 target: &Path,
2823 algorithms: &[HashAlgorithm],
2824 ) -> Result<(Vec<HashDigest>, u64), Error> {
2825 let temp_dir = tempfile::tempdir_in(
2826 self.build_context
2827 .cache()
2828 .bucket(CacheBucket::SourceDistributions),
2829 )
2830 .map_err(Error::CacheWrite)?;
2831
2832 let reader = response
2833 .bytes_stream()
2834 .map_err(std::io::Error::other)
2835 .into_async_read();
2836
2837 let mut hashers = algorithms
2839 .iter()
2840 .copied()
2841 .map(Hasher::from)
2842 .collect::<Vec<_>>();
2843 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
2844
2845 let span = info_span!("download_source_dist", source_dist = %source);
2847 uv_extract::stream::archive(&mut hasher, ext, temp_dir.path())
2848 .await
2849 .map_err(|err| Error::Extract(source.to_string(), err))?;
2850 drop(span);
2851
2852 let expected_size = match source {
2853 BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => {
2854 dist.size()
2855 }
2856 BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(),
2857 _ => None,
2858 };
2859
2860 if !algorithms.is_empty() || expected_size.is_some() {
2862 hasher.finish().await.map_err(Error::HashExhaustion)?;
2863 }
2864 if let Some(expected) = expected_size
2865 && hasher.bytes_read() != expected
2866 {
2867 return Err(Error::MismatchedSize {
2868 distribution: source.to_string(),
2869 expected,
2870 actual: hasher.bytes_read(),
2871 });
2872 }
2873
2874 let size = hasher.bytes_read();
2875 let hashes = hashers.into_iter().map(HashDigest::from).collect();
2876
2877 let extracted = match uv_extract::strip_component(temp_dir.path()) {
2879 Ok(top_level) => top_level,
2880 Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.keep(),
2881 Err(err) => {
2882 return Err(Error::Extract(
2883 temp_dir.path().to_string_lossy().into_owned(),
2884 err,
2885 ));
2886 }
2887 };
2888
2889 fs_err::tokio::create_dir_all(target.parent().expect("Cache entry to have parent"))
2891 .await
2892 .map_err(Error::CacheWrite)?;
2893 if let Err(err) = rename_with_retry(extracted, target).await {
2894 if err.kind() == std::io::ErrorKind::AlreadyExists {
2896 warn!("Directory already exists: {}", target.display());
2897 } else {
2898 return Err(Error::CacheWrite(err));
2899 }
2900 }
2901
2902 Ok((hashes, size))
2903 }
2904
2905 async fn persist_archive(
2907 &self,
2908 path: &Path,
2909 ext: SourceDistExtension,
2910 target: &Path,
2911 algorithms: &[HashAlgorithm],
2912 ) -> Result<Vec<HashDigest>, Error> {
2913 debug!("Unpacking for build: {}", path.display());
2914
2915 let temp_dir = tempfile::tempdir_in(
2916 self.build_context
2917 .cache()
2918 .bucket(CacheBucket::SourceDistributions),
2919 )
2920 .map_err(Error::CacheWrite)?;
2921 let reader = fs_err::tokio::File::open(&path)
2922 .await
2923 .map_err(Error::CacheRead)?;
2924
2925 let mut hashers = algorithms
2927 .iter()
2928 .copied()
2929 .map(Hasher::from)
2930 .collect::<Vec<_>>();
2931 let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
2932
2933 uv_extract::stream::archive(&mut hasher, ext, &temp_dir.path())
2935 .await
2936 .map_err(|err| Error::Extract(temp_dir.path().to_string_lossy().into_owned(), err))?;
2937
2938 if !algorithms.is_empty() {
2940 hasher.finish().await.map_err(Error::HashExhaustion)?;
2941 }
2942
2943 let hashes = hashers.into_iter().map(HashDigest::from).collect();
2944
2945 let extracted = match uv_extract::strip_component(temp_dir.path()) {
2947 Ok(top_level) => top_level,
2948 Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(),
2949 Err(err) => {
2950 return Err(Error::Extract(
2951 temp_dir.path().to_string_lossy().into_owned(),
2952 err,
2953 ));
2954 }
2955 };
2956
2957 fs_err::tokio::create_dir_all(target.parent().expect("Cache entry to have parent"))
2959 .await
2960 .map_err(Error::CacheWrite)?;
2961 if let Err(err) = rename_with_retry(extracted, target).await {
2962 if err.kind() == std::io::ErrorKind::DirectoryNotEmpty {
2964 warn!("Directory already exists: {}", target.display());
2965 } else {
2966 return Err(Error::CacheWrite(err));
2967 }
2968 }
2969
2970 Ok(hashes)
2971 }
2972
2973 fn stop_discovery_at<'path>(
2976 source: &BuildableSource<'_>,
2977 source_root: &'path Path,
2978 ) -> Option<&'path Path> {
2979 if matches!(
2980 source,
2981 BuildableSource::Dist(SourceDist::GitDirectory(_))
2982 | BuildableSource::Url(SourceUrl::GitDirectory(_))
2983 ) {
2984 Some(source_root)
2985 } else {
2986 None
2987 }
2988 }
2989
2990 #[instrument(skip_all, fields(dist = %source))]
2994 async fn build_distribution(
2995 &self,
2996 source: &BuildableSource<'_>,
2997 source_root: &Path,
2998 subdirectory: Option<&Path>,
2999 cache_shard: &CacheShard,
3000 no_sources: NoSources,
3001 ) -> Result<(String, WheelFilename, ResolutionMetadata), Error> {
3002 debug!("Building: {source}");
3003
3004 if self
3006 .build_context
3007 .build_options()
3008 .no_build_requirement(source.name())
3009 {
3010 if source.is_editable() {
3011 debug!("Allowing build for editable source distribution: {source}");
3012 } else {
3013 return Err(Error::NoBuild);
3014 }
3015 }
3016
3017 let temp_dir = self
3019 .build_context
3020 .cache()
3021 .build_dir()
3022 .map_err(Error::CacheWrite)?;
3023
3024 fs::create_dir_all(&cache_shard)
3026 .await
3027 .map_err(Error::CacheWrite)?;
3028
3029 let disk_filename = if let Some(name) = self
3031 .build_context
3032 .direct_build(
3033 source_root,
3034 subdirectory,
3035 temp_dir.path(),
3036 no_sources.clone(),
3037 if source.is_editable() {
3038 BuildKind::Editable
3039 } else {
3040 BuildKind::Wheel
3041 },
3042 Some(&source.to_string()),
3043 )
3044 .await
3045 .map_err(|err| Error::Build(err.into()))?
3046 {
3047 name.to_string()
3049 } else {
3050 let base_python = if cfg!(unix) {
3052 self.build_context
3053 .interpreter()
3054 .await
3055 .find_base_python()
3056 .map_err(Error::BaseInterpreter)?
3057 } else {
3058 self.build_context
3059 .interpreter()
3060 .await
3061 .to_base_python()
3062 .map_err(Error::BaseInterpreter)?
3063 };
3064
3065 let build_kind = if source.is_editable() {
3066 BuildKind::Editable
3067 } else {
3068 BuildKind::Wheel
3069 };
3070
3071 let install_path = if let Some(subdirectory) = subdirectory {
3072 source_root.join(subdirectory)
3073 } else {
3074 source_root.to_path_buf()
3075 };
3076
3077 let stop_discovery_at = Self::stop_discovery_at(source, source_root);
3078
3079 let build_key = BuildKey {
3080 base_python: base_python.into_boxed_path(),
3081 source_root: source_root.to_path_buf().into_boxed_path(),
3082 subdirectory: subdirectory
3083 .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()),
3084 no_sources: no_sources.clone(),
3085 build_kind,
3086 };
3087
3088 if let Some(builder) = self.build_context.build_arena().remove(&build_key) {
3089 debug!("Reusing existing build environment for: {source}");
3090 let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
3091
3092 self.build_context.build_arena().insert(build_key, builder);
3094
3095 wheel
3096 } else {
3097 debug!("Creating build environment for: {source}");
3098
3099 let builder = self
3100 .build_context
3101 .setup_build(
3102 source_root,
3103 subdirectory,
3104 &install_path,
3105 stop_discovery_at,
3106 Some(&source.to_string()),
3107 source.as_dist(),
3108 &no_sources,
3109 if source.is_editable() {
3110 BuildKind::Editable
3111 } else {
3112 BuildKind::Wheel
3113 },
3114 if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
3115 BuildOutput::Quiet
3116 } else {
3117 BuildOutput::Debug
3118 },
3119 self.build_stack.cloned().unwrap_or_default(),
3120 )
3121 .await
3122 .map_err(|err| Error::Build(err.into()))?;
3123
3124 let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
3126
3127 self.build_context.build_arena().insert(build_key, builder);
3129
3130 wheel
3131 }
3132 };
3133
3134 let filename = WheelFilename::from_str(&disk_filename)?;
3136 let metadata = read_wheel_metadata(&filename, &temp_dir.path().join(&disk_filename))?;
3137
3138 validate_metadata(source, &metadata)?;
3140 validate_filename(&filename, &metadata)?;
3141
3142 rename_with_retry(
3144 temp_dir.path().join(&disk_filename),
3145 cache_shard.join(&disk_filename),
3146 )
3147 .await
3148 .map_err(Error::CacheWrite)?;
3149
3150 debug!("Built `{source}` into `{disk_filename}`");
3151 Ok((disk_filename, filename, metadata))
3152 }
3153
3154 #[instrument(skip_all, fields(dist = %source))]
3156 async fn build_metadata(
3157 &self,
3158 source: &BuildableSource<'_>,
3159 source_root: &Path,
3160 subdirectory: Option<&Path>,
3161 no_sources: NoSources,
3162 ) -> Result<Option<ResolutionMetadata>, Error> {
3163 debug!("Preparing metadata for: {source}");
3164
3165 let source_name = source.name();
3166 if self
3167 .build_context
3168 .build_options()
3169 .no_build_requirement(source_name)
3170 && !(source_name.is_none() && source.is_editable())
3173 {
3174 return if let Some(name) = source_name {
3175 Err(Error::NoBuildPackage(name.clone()))
3176 } else {
3177 Err(Error::NoBuild)
3178 };
3179 }
3180
3181 if let Some(requires_python) = source.requires_python() {
3184 let installed = self.build_context.interpreter().await.python_version();
3185 let target = release_specifiers_to_ranges(requires_python.clone())
3186 .bounding_range()
3187 .map(|bounding_range| bounding_range.0.cloned())
3188 .unwrap_or(Bound::Unbounded);
3189 let is_compatible = match target {
3190 Bound::Included(target) => *installed >= target,
3191 Bound::Excluded(target) => *installed > target,
3192 Bound::Unbounded => true,
3193 };
3194 if !is_compatible {
3195 return Err(Error::RequiresPython(
3196 requires_python.clone(),
3197 installed.clone(),
3198 ));
3199 }
3200 }
3201
3202 let base_python = if cfg!(unix) {
3204 self.build_context
3205 .interpreter()
3206 .await
3207 .find_base_python()
3208 .map_err(Error::BaseInterpreter)?
3209 } else {
3210 self.build_context
3211 .interpreter()
3212 .await
3213 .to_base_python()
3214 .map_err(Error::BaseInterpreter)?
3215 };
3216
3217 let build_kind = if source.is_editable() {
3219 BuildKind::Editable
3220 } else {
3221 BuildKind::Wheel
3222 };
3223
3224 let install_path = if let Some(subdirectory) = subdirectory {
3225 source_root.join(subdirectory)
3226 } else {
3227 source_root.to_path_buf()
3228 };
3229
3230 let stop_discovery_at = Self::stop_discovery_at(source, source_root);
3231
3232 let mut builder = self
3234 .build_context
3235 .setup_build(
3236 source_root,
3237 subdirectory,
3238 &install_path,
3239 stop_discovery_at,
3240 Some(&source.to_string()),
3241 source.as_dist(),
3242 &no_sources,
3243 build_kind,
3244 if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
3245 BuildOutput::Quiet
3246 } else {
3247 BuildOutput::Debug
3248 },
3249 self.build_stack.cloned().unwrap_or_default(),
3250 )
3251 .await
3252 .map_err(|err| Error::Build(err.into()))?;
3253
3254 let dist_info = builder.metadata().await.map_err(Error::Build)?;
3256
3257 self.build_context.build_arena().insert(
3259 BuildKey {
3260 base_python: base_python.into_boxed_path(),
3261 source_root: source_root.to_path_buf().into_boxed_path(),
3262 subdirectory: subdirectory
3263 .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()),
3264 no_sources,
3265 build_kind,
3266 },
3267 builder,
3268 );
3269
3270 let Some(dist_info) = dist_info else {
3272 return Ok(None);
3273 };
3274
3275 debug!("Prepared metadata for: {source}");
3277 let content = fs::read(dist_info.join("METADATA"))
3278 .await
3279 .map_err(Error::CacheRead)?;
3280 let metadata = ResolutionMetadata::parse_metadata(&content)?;
3281
3282 validate_metadata(source, &metadata)?;
3284
3285 Ok(Some(metadata))
3286 }
3287
3288 fn request(
3290 url: DisplaySafeUrl,
3291 client: &RegistryClient,
3292 ) -> Result<reqwest::Request, reqwest::Error> {
3293 client
3294 .uncached_client(&url)
3295 .get(Url::from(url))
3296 .header(
3297 "accept-encoding",
3301 reqwest::header::HeaderValue::from_static("identity"),
3302 )
3303 .build()
3304 }
3305}
3306
3307pub fn prune(cache: &Cache) -> Result<Removal, Error> {
3309 let mut removal = cache.removal();
3310
3311 let bucket = cache.bucket(CacheBucket::SourceDistributions);
3312 if bucket.is_dir() {
3313 for entry in walkdir::WalkDir::new(bucket) {
3314 let entry = entry.map_err(Error::CacheWalk)?;
3315
3316 if !entry.file_type().is_dir() {
3317 continue;
3318 }
3319
3320 let revision = entry.path().join("revision.http");
3323 if revision.is_file() {
3324 if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision) {
3325 for sibling in entry.path().read_dir().map_err(Error::CacheRead)? {
3327 let sibling = sibling.map_err(Error::CacheRead)?;
3328 if sibling.file_type().map_err(Error::CacheRead)?.is_dir() {
3329 let sibling_name = sibling.file_name();
3330 if sibling_name != pointer.revision.id().as_str() {
3331 debug!(
3332 "Removing dangling source revision: {}",
3333 sibling.path().display()
3334 );
3335 removal += cache
3336 .remove_path(sibling.path())
3337 .map_err(Error::CacheWrite)?;
3338 }
3339 }
3340 }
3341 }
3342 }
3343
3344 let revision = entry.path().join("revision.rev");
3347 if revision.is_file() {
3348 if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision) {
3349 for sibling in entry.path().read_dir().map_err(Error::CacheRead)? {
3351 let sibling = sibling.map_err(Error::CacheRead)?;
3352 if sibling.file_type().map_err(Error::CacheRead)?.is_dir() {
3353 let sibling_name = sibling.file_name();
3354 if sibling_name != pointer.revision.id().as_str() {
3355 debug!(
3356 "Removing dangling source revision: {}",
3357 sibling.path().display()
3358 );
3359 removal += cache
3360 .remove_path(sibling.path())
3361 .map_err(Error::CacheWrite)?;
3362 }
3363 }
3364 }
3365 }
3366 }
3367 }
3368 }
3369
3370 Ok(removal)
3371}
3372
3373#[derive(Debug)]
3375enum StaticMetadata {
3376 Some(ResolutionMetadata),
3378 Dynamic,
3380 None,
3382}
3383
3384impl StaticMetadata {
3385 async fn read(
3387 source: &BuildableSource<'_>,
3388 source_root: &Path,
3389 subdirectory: Option<&Path>,
3390 ) -> Result<Self, Error> {
3391 let pyproject_toml = match read_pyproject_toml(source_root, subdirectory).await {
3393 Ok(pyproject_toml) => Some(pyproject_toml),
3394 Err(Error::MissingPyprojectToml) => {
3395 debug!("No `pyproject.toml` available for: {source}");
3396 None
3397 }
3398 Err(err) => return Err(err),
3399 };
3400
3401 let dynamic = pyproject_toml.as_ref().is_some_and(|pyproject_toml| {
3403 pyproject_toml.project.as_ref().is_some_and(|project| {
3404 project
3405 .dynamic
3406 .as_ref()
3407 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
3408 })
3409 });
3410
3411 if let Some(pyproject_toml) = pyproject_toml {
3413 match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) {
3414 Ok(metadata) => {
3415 debug!("Found static `pyproject.toml` for: {source}");
3416
3417 match validate_metadata(source, &metadata) {
3419 Ok(()) => {
3420 return Ok(Self::Some(metadata));
3421 }
3422 Err(err) => {
3423 debug!("Ignoring `pyproject.toml` for {source}: {err}");
3424 }
3425 }
3426 }
3427 Err(
3428 err @ (uv_pypi_types::MetadataError::Pep508Error(_)
3429 | uv_pypi_types::MetadataError::DynamicField(_)
3430 | uv_pypi_types::MetadataError::FieldNotFound(_)
3431 | uv_pypi_types::MetadataError::PoetrySyntax),
3432 ) => {
3433 debug!("No static `pyproject.toml` available for: {source} ({err:?})");
3434 }
3435 Err(err) => return Err(Error::PyprojectToml(err)),
3436 }
3437 }
3438
3439 if source.is_source_tree() {
3442 return Ok(if dynamic { Self::Dynamic } else { Self::None });
3443 }
3444
3445 match read_pkg_info(source_root, subdirectory).await {
3447 Ok(metadata) => {
3448 debug!("Found static `PKG-INFO` for: {source}");
3449
3450 match validate_metadata(source, &metadata) {
3452 Ok(()) => {
3453 let metadata = if dynamic {
3455 ResolutionMetadata {
3456 dynamic: true,
3457 ..metadata
3458 }
3459 } else {
3460 metadata
3461 };
3462 return Ok(Self::Some(metadata));
3463 }
3464 Err(err) => {
3465 debug!("Ignoring `PKG-INFO` for {source}: {err}");
3466 }
3467 }
3468 }
3469 Err(
3470 err @ (Error::MissingPkgInfo
3471 | Error::PkgInfo(
3472 uv_pypi_types::MetadataError::Pep508Error(_)
3473 | uv_pypi_types::MetadataError::DynamicField(_)
3474 | uv_pypi_types::MetadataError::FieldNotFound(_)
3475 | uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
3476 )),
3477 ) => {
3478 debug!("No static `PKG-INFO` available for: {source} ({err:?})");
3479 }
3480 Err(err) => return Err(err),
3481 }
3482
3483 Ok(Self::None)
3484 }
3485}
3486
3487fn has_sources(content: &str) -> Result<bool, toml::de::Error> {
3489 #[derive(serde::Deserialize)]
3490 struct PyProjectToml {
3491 tool: Option<Tool>,
3492 }
3493
3494 #[derive(serde::Deserialize)]
3495 struct Tool {
3496 uv: Option<ToolUv>,
3497 }
3498
3499 #[derive(serde::Deserialize)]
3500 struct ToolUv {
3501 sources: Option<ToolUvSources>,
3502 }
3503
3504 let pyproject_toml =
3505 info_span!("toml::from_str has sources").in_scope(|| toml::from_str(content))?;
3506 if let PyProjectToml { tool: Some(tool) } = pyproject_toml {
3507 if let Some(uv) = tool.uv {
3508 if let Some(sources) = uv.sources {
3509 if !sources.inner().is_empty() {
3510 return Ok(true);
3511 }
3512 }
3513 }
3514 }
3515
3516 Ok(false)
3517}
3518
3519fn validate_metadata(
3521 source: &BuildableSource<'_>,
3522 metadata: &ResolutionMetadata,
3523) -> Result<(), Error> {
3524 if let Some(name) = source.name() {
3525 if metadata.name != *name {
3526 return Err(Error::WheelMetadataNameMismatch {
3527 metadata: metadata.name.clone(),
3528 given: name.clone(),
3529 });
3530 }
3531 }
3532
3533 if let Some(version) = source.version() {
3534 if *version != metadata.version && *version != metadata.version.clone().without_local() {
3535 return Err(Error::WheelMetadataVersionMismatch {
3536 metadata: metadata.version.clone(),
3537 given: version.clone(),
3538 });
3539 }
3540 }
3541
3542 Ok(())
3543}
3544
3545fn validate_filename(filename: &WheelFilename, metadata: &ResolutionMetadata) -> Result<(), Error> {
3547 if metadata.name != filename.name {
3548 return Err(Error::WheelFilenameNameMismatch {
3549 metadata: metadata.name.clone(),
3550 filename: filename.name.clone(),
3551 });
3552 }
3553
3554 if metadata.version != filename.version {
3555 return Err(Error::WheelFilenameVersionMismatch {
3556 metadata: metadata.version.clone(),
3557 filename: filename.version.clone(),
3558 });
3559 }
3560
3561 Ok(())
3562}
3563
3564#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3568pub(crate) struct HttpRevisionPointer {
3569 revision: Revision,
3570}
3571
3572impl HttpRevisionPointer {
3573 pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3575 match fs_err::File::open(path.as_ref()) {
3576 Ok(file) => {
3577 let data = DataWithCachePolicy::from_reader(file)?.data;
3578 let revision = rmp_serde::from_slice::<Revision>(&data)?;
3579 Ok(Some(Self { revision }))
3580 }
3581 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3582 Err(err) => Err(Error::CacheRead(err)),
3583 }
3584 }
3585
3586 pub(crate) fn into_revision(self) -> Revision {
3588 self.revision
3589 }
3590}
3591
3592#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3596pub(crate) struct LocalRevisionPointer {
3597 cache_info: CacheInfo,
3598 revision: Revision,
3599}
3600
3601impl LocalRevisionPointer {
3602 pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3604 match fs_err::read(path) {
3605 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
3606 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3607 Err(err) => Err(Error::CacheRead(err)),
3608 }
3609 }
3610
3611 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
3613 fs::create_dir_all(&entry.dir())
3614 .await
3615 .map_err(Error::CacheWrite)?;
3616 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
3617 .await
3618 .map_err(Error::CacheWrite)
3619 }
3620
3621 pub(crate) fn cache_info(&self) -> &CacheInfo {
3623 &self.cache_info
3624 }
3625
3626 fn revision(&self) -> &Revision {
3628 &self.revision
3629 }
3630
3631 pub(crate) fn into_revision(self) -> Revision {
3633 self.revision
3634 }
3635}
3636
3637#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3641pub(crate) struct RevisionHashes {
3642 hashes: Vec<HashDigest>,
3643}
3644
3645impl RevisionHashes {
3646 pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3648 match fs_err::read(path) {
3649 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
3650 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3651 Err(err) => Err(Error::CacheRead(err)),
3652 }
3653 }
3654
3655 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
3657 fs::create_dir_all(&entry.dir())
3658 .await
3659 .map_err(Error::CacheWrite)?;
3660 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
3661 .await
3662 .map_err(Error::CacheWrite)
3663 }
3664
3665 pub(crate) fn into_hashes(self) -> HashDigests {
3667 HashDigests::from(self.hashes)
3668 }
3669}
3670
3671impl Hashed for RevisionHashes {
3672 fn hashes(&self) -> &[HashDigest] {
3673 &self.hashes
3674 }
3675}
3676
3677async fn read_pkg_info(
3681 source_tree: &Path,
3682 subdirectory: Option<&Path>,
3683) -> Result<ResolutionMetadata, Error> {
3684 let pkg_info = match subdirectory {
3686 Some(subdirectory) => source_tree.join(subdirectory).join("PKG-INFO"),
3687 None => source_tree.join("PKG-INFO"),
3688 };
3689 let content = match fs::read(pkg_info).await {
3690 Ok(content) => content,
3691 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3692 return Err(Error::MissingPkgInfo);
3693 }
3694 Err(err) => return Err(Error::CacheRead(err)),
3695 };
3696
3697 let metadata = ResolutionMetadata::parse_pkg_info(&content).map_err(Error::PkgInfo)?;
3699
3700 Ok(metadata)
3701}
3702
3703async fn read_pyproject_toml(
3706 source_tree: &Path,
3707 subdirectory: Option<&Path>,
3708) -> Result<PyProjectToml, Error> {
3709 let pyproject_toml = match subdirectory {
3711 Some(subdirectory) => source_tree.join(subdirectory).join("pyproject.toml"),
3712 None => source_tree.join("pyproject.toml"),
3713 };
3714 let content = match fs::read_to_string(&pyproject_toml).await {
3715 Ok(content) => content,
3716 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3717 return Err(Error::MissingPyprojectToml);
3718 }
3719 Err(err) => return Err(Error::CacheRead(err)),
3720 };
3721
3722 let pyproject_toml = PyProjectToml::from_toml(&content, pyproject_toml.simplified_display())?;
3723
3724 Ok(pyproject_toml)
3725}
3726
3727#[derive(Debug, Clone)]
3729struct CachedMetadata(ResolutionMetadata);
3730
3731impl CachedMetadata {
3732 async fn read(cache_entry: &CacheEntry) -> Result<Option<Self>, Error> {
3734 match fs::read(&cache_entry.path()).await {
3735 Ok(cached) => Ok(Some(Self(rmp_serde::from_slice(&cached)?))),
3736 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3737 Err(err) => Err(Error::CacheRead(err)),
3738 }
3739 }
3740
3741 fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool {
3743 name.is_none_or(|name| self.0.name == *name)
3744 && version.is_none_or(|version| self.0.version == *version)
3745 }
3746}
3747
3748impl From<CachedMetadata> for ResolutionMetadata {
3749 fn from(value: CachedMetadata) -> Self {
3750 value.0
3751 }
3752}
3753
3754fn read_wheel_metadata(
3756 filename: &WheelFilename,
3757 wheel: &Path,
3758) -> Result<ResolutionMetadata, Error> {
3759 let file = fs_err::File::open(wheel).map_err(Error::CacheRead)?;
3760 let reader = std::io::BufReader::new(file);
3761 let dist_info = read_archive_metadata(filename, reader)
3762 .map_err(|err| Error::WheelMetadata(wheel.to_path_buf(), Box::new(err)))?;
3763 Ok(ResolutionMetadata::parse_metadata(&dist_info)?)
3764}