Skip to main content

uv_distribution/source/
mod.rs

1//! Fetch and build source distributions from remote sources.
2
3// This is to squash warnings about `|r| r.into_git_reporter()`. Clippy wants
4// me to eta-reduce that and write it as
5// `<(dyn reporter::Reporter + 'static)>::into_git_reporter`
6// instead. But that's a monster. On the other hand, applying this suppression
7// instruction more granularly is annoying. So we just slap it on the module
8// for now. ---AG
9#![expect(clippy::redundant_closure_for_method_calls)]
10
11use std::borrow::Cow;
12use std::ops::Bound;
13use std::path::Path;
14use std::str::FromStr;
15use std::sync::Arc;
16
17use fs_err::tokio as fs;
18use futures::{FutureExt, TryStreamExt};
19use reqwest::{Response, StatusCode};
20use tokio_util::compat::FuturesAsyncReadCompatExt;
21use tracing::{Instrument, debug, info_span, instrument, warn};
22use url::Url;
23
24use uv_auth::CredentialsCache;
25use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache};
26use uv_cache_info::CacheInfo;
27use uv_client::{
28    BaseClientBuilder, CacheControl, CachedClientError, Connectivity, DataWithCachePolicy,
29    RegistryClient,
30};
31use uv_configuration::{BuildKind, BuildOutput, NoSources};
32use uv_distribution_filename::{SourceDistExtension, WheelFilename};
33use uv_distribution_types::{
34    BuildInfo, BuildVariables, BuildableSource, ConfigSettings, DirectorySourceUrl,
35    ExtraBuildRequirement, GitDirectorySourceUrl, GitPathSourceUrl, HashPolicy, Hashed, IndexUrl,
36    PathSourceUrl, RemoteSource, RequirementSource, RequiresPython, SourceDist, SourceUrl,
37};
38use uv_fs::{Simplified, rename_with_retry, write_atomic};
39use uv_git::{Fetch, GIT_LFS, GitError, GitHttpSettings, GitResolver};
40use uv_git_types::{GitHubRepository, GitOid, GitUrl};
41use uv_metadata::read_archive_metadata;
42use uv_normalize::PackageName;
43use uv_pep440::{Version, release_specifiers_to_ranges};
44use uv_platform_tags::Tags;
45use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, PyProjectToml, ResolutionMetadata};
46use uv_redacted::DisplaySafeUrl;
47use uv_types::{BuildContext, BuildKey, BuildStack, SourceBuildTrait};
48use uv_workspace::pyproject::ToolUvSources;
49
50use crate::distribution_database::ManagedClient;
51use crate::error::Error;
52use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata};
53use crate::source::built_wheel_metadata::{BuiltWheelFile, BuiltWheelMetadata};
54use crate::source::revision::Revision;
55use crate::source::validated_archive::{ArchiveValidation, ValidatedSourceArchive};
56use crate::{Reporter, RequiresDist};
57
58mod built_wheel_metadata;
59mod revision;
60mod validated_archive;
61
62/// Access distribution metadata without requiring a build interpreter.
63///
64/// This is intended for metadata operations that occur before selecting an interpreter, such as
65/// choosing a tool Python from `requires-python`.
66pub struct StaticMetadataDatabase<'a, 'client> {
67    client_builder: &'a BaseClientBuilder<'client>,
68    git: &'a GitResolver,
69    cache: &'a Cache,
70}
71
72/// A direct source tree materialized on disk for static metadata inspection.
73#[derive(Debug)]
74struct MaterializedSourceTree(Box<Path>);
75
76impl MaterializedSourceTree {
77    /// Return the on-disk path for this source tree.
78    fn path(&self) -> &Path {
79        &self.0
80    }
81}
82
83impl<'a, 'client> StaticMetadataDatabase<'a, 'client> {
84    /// Create a [`StaticMetadataDatabase`] for an invocation.
85    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    /// Materialize a direct source tree, if the requirement identifies one.
98    ///
99    /// Directory requirements are already materialized. Git source trees are fetched into the
100    /// Git cache and returned at the requested subdirectory.
101    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    /// Read static [`RequiresPython`] from an already materialized source tree.
140    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    /// Read static [`RequiresPython`] from a direct source-tree requirement.
162    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
173/// Fetch and validate a Git source tree.
174async 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
209/// Fetch and build a source distribution from a remote source, or from a local cache.
210pub(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
216/// The name of the file that contains the revision ID for a remote distribution, encoded via `MsgPack`.
217pub(crate) const HTTP_REVISION: &str = "revision.http";
218
219/// The name of the file that contains the revision ID for a local distribution, encoded via `MsgPack`.
220pub(crate) const LOCAL_REVISION: &str = "revision.rev";
221
222/// The name of the file that contains the cached distribution hashes, encoded via `MsgPack`.
223pub(crate) const HASHES: &str = "hashes.msgpack";
224
225/// The name of the file that contains the cached distribution metadata, encoded via `MsgPack`.
226const METADATA: &str = "metadata.msgpack";
227
228/// The directory within each entry under which to store the unpacked source distribution.
229const SOURCE: &str = "src";
230
231impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
232    /// Initialize a [`SourceDistributionBuilder`] from a [`BuildContext`].
233    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    /// Set the [`BuildStack`] to use for the [`SourceDistributionBuilder`].
242    #[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    /// Set the [`Reporter`] to use for the [`SourceDistributionBuilder`].
251    #[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    /// Download and build a [`SourceDist`].
260    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                // For registry source distributions, shard by package, then version, for
270                // convenience in debugging.
271                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 the URL is a file URL, use the local path directly.
281                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                // For direct URLs, cache directly under the hash of the URL itself.
317                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                // For direct URLs, cache directly under the hash of the URL itself.
374                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    /// Download a [`SourceDist`] and determine its metadata. This typically involves building the
423    /// source distribution into a wheel; however, some build backends support determining the
424    /// metadata without building the source distribution.
425    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                // For registry source distributions, shard by package, then version.
434                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 the URL is a file URL, use the local path directly.
444                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                // For direct URLs, cache directly under the hash of the URL itself.
478                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                // For direct URLs, cache directly under the hash of the URL itself.
533                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    /// Determine the [`ConfigSettings`] for the given package name.
592    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    /// Determine the extra build dependencies for the given package name.
609    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    /// Determine the extra build variables for the given package name.
620    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    /// Build a source distribution from a remote URL.
625    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        // Fetch the revision for the source distribution.
640        let revision = self
641            .url_revision(source, ext, url, index, cache_shard, hashes, client)
642            .await?;
643
644        // Before running the build, check that the hashes match.
645        if !revision.satisfies(hashes) {
646            return Err(Error::hash_mismatch(
647                source.to_string(),
648                hashes.digests(),
649                revision.hashes(),
650            ));
651        }
652
653        // Scope all operations to the revision. Within the revision, there's no need to check for
654        // freshness, since entries have to be fresher than the revision itself.
655        let cache_shard = cache_shard.shard(revision.id());
656        let source_dist_entry = cache_shard.entry(SOURCE);
657
658        // We don't track any cache information for URL-based source distributions; they're assumed
659        // to be immutable.
660        let cache_info = CacheInfo::default();
661
662        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
663        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 the cache contains a compatible wheel, return it.
677        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        // Otherwise, we need to build a wheel. Before building, ensure that the source is present.
691        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        // Validate that the subdirectory exists.
708        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        // Build the source distribution.
723        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        // Store the metadata.
740        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    /// Build the source distribution's metadata from a local path.
756    ///
757    /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid
758    /// building the wheel.
759    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        // Fetch the revision for the source distribution.
773        let revision = self
774            .url_revision(source, ext, url, index, cache_shard, hashes, client)
775            .await?;
776
777        // Before running the build, check that the hashes match.
778        if !revision.satisfies(hashes) {
779            return Err(Error::hash_mismatch(
780                source.to_string(),
781                hashes.digests(),
782                revision.hashes(),
783            ));
784        }
785
786        // Scope all operations to the revision. Within the revision, there's no need to check for
787        // freshness, since entries have to be fresher than the revision itself.
788        let cache_shard = cache_shard.shard(revision.id());
789        let source_dist_entry = cache_shard.entry(SOURCE);
790
791        // If the metadata is static, return it.
792        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        // If the cache contains compatible metadata, return it.
805        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        // Otherwise, we need a wheel.
824        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        // Validate that the subdirectory exists.
841        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        // Otherwise, we either need to build the metadata.
851        // If the backend supports `prepare_metadata_for_build_wheel`, use it.
852        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            // If necessary, mark the metadata as dynamic.
863            let metadata = if dynamic {
864                ResolutionMetadata {
865                    dynamic: true,
866                    ..metadata
867                }
868            } else {
869                metadata
870            };
871
872            // Store the metadata.
873            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        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
887        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        // Build the source distribution.
906        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        // If necessary, mark the metadata as dynamic.
923        let metadata = if dynamic {
924            ResolutionMetadata {
925                dynamic: true,
926                ..metadata
927            }
928        } else {
929            metadata
930        };
931
932        // Store the metadata.
933        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    /// Return the [`Revision`] for a remote URL, refreshing it if necessary.
944    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        // Determine the cache control policy for the request.
957        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                // At this point, we're seeing a new or updated source distribution. Initialize a
979                // new revision, to collect the source and built artifacts.
980                let revision = Revision::new();
981
982                // Download the source distribution.
983                debug!("Downloading source distribution: {source}");
984                let entry = cache_shard.shard(revision.id()).entry(SOURCE);
985                let (hashes, size) = self
986                    .download_archive(response, source, ext, entry.path(), hashes, &[])
987                    .await?;
988
989                Ok(revision
990                    .with_hashes(HashDigests::from(hashes))
991                    .with_size(size))
992            }
993            .boxed_local()
994            .instrument(info_span!("download", source_dist = %source))
995        };
996        let req = Self::request(url.clone(), client.unmanaged)?;
997        let revision = client
998            .managed(|client| {
999                client.cached_client().get_serde_with_retry(
1000                    req,
1001                    &cache_entry,
1002                    cache_control.clone(),
1003                    download,
1004                )
1005            })
1006            .await
1007            .map_err(|err| match err {
1008                CachedClientError::Callback { err, .. } => err,
1009                CachedClientError::Client(err) => Error::Client(err),
1010            })?;
1011
1012        let expected_size = match source {
1013            BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => {
1014                dist.size()
1015            }
1016            BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(),
1017            _ => None,
1018        };
1019        if let (Some(expected), Some(actual)) = (expected_size, revision.size())
1020            && expected != actual
1021        {
1022            return Err(Error::MismatchedSize {
1023                distribution: source.to_string(),
1024                expected,
1025                actual,
1026            });
1027        }
1028
1029        // If the archive is missing the required hashes or size, force a refresh.
1030        if revision.has_digests(hashes) && (expected_size.is_none() || revision.size().is_some()) {
1031            Ok(revision)
1032        } else {
1033            client
1034                .managed(async |client| {
1035                    client
1036                        .cached_client()
1037                        .skip_cache_with_retry(
1038                            Self::request(url.clone(), client)?,
1039                            &cache_entry,
1040                            cache_control,
1041                            download,
1042                        )
1043                        .await
1044                        .map_err(|err| match err {
1045                            CachedClientError::Callback { err, .. } => err,
1046                            CachedClientError::Client(err) => Error::Client(err),
1047                        })
1048                })
1049                .await
1050        }
1051    }
1052
1053    /// Build a source distribution from a local archive (e.g., `.tar.gz` or `.zip`).
1054    async fn archive(
1055        &self,
1056        source: &BuildableSource<'_>,
1057        resource: &PathSourceUrl<'_>,
1058        cache_shard: &CacheShard,
1059        tags: &Tags,
1060        hashes: HashPolicy<'_>,
1061    ) -> Result<BuiltWheelMetadata, Error> {
1062        let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1063
1064        // Fetch the revision for the source distribution.
1065        let LocalRevisionPointer {
1066            cache_info,
1067            revision,
1068        } = self
1069            .archive_revision(source, resource, cache_shard, hashes)
1070            .await?;
1071
1072        // Before running the build, check that the hashes match.
1073        if !revision.satisfies(hashes) {
1074            return Err(Error::hash_mismatch(
1075                source.to_string(),
1076                hashes.digests(),
1077                revision.hashes(),
1078            ));
1079        }
1080
1081        // Scope all operations to the revision. Within the revision, there's no need to check for
1082        // freshness, since entries have to be fresher than the revision itself.
1083        let cache_shard = cache_shard.shard(revision.id());
1084        let source_entry = cache_shard.entry(SOURCE);
1085
1086        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
1087        let config_settings = self.config_settings_for(source.name());
1088        let extra_build_deps = self.extra_build_dependencies_for(source.name());
1089        let extra_build_variables = self.extra_build_variables_for(source.name());
1090        let build_info = BuildInfo::from_settings(
1091            config_settings.into_owned(),
1092            extra_build_deps.to_vec(),
1093            extra_build_variables.cloned(),
1094        );
1095        let cache_shard = build_info
1096            .cache_shard()
1097            .map(|digest| cache_shard.shard(digest))
1098            .unwrap_or(cache_shard);
1099
1100        // If the cache contains a compatible wheel, return it.
1101        if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1102            .ok()
1103            .flatten()
1104            .filter(|file| file.matches(source.name(), source.version()))
1105        {
1106            return Ok(BuiltWheelMetadata::from_file(
1107                file,
1108                revision.into_hashes(),
1109                cache_info,
1110                build_info,
1111            ));
1112        }
1113
1114        // Otherwise, we need to build a wheel, which requires a source distribution.
1115        let revision = if source_entry.path().is_dir() {
1116            revision
1117        } else {
1118            self.heal_archive_revision(source, resource, &source_entry, revision, hashes)
1119                .await?
1120        };
1121
1122        let task = self
1123            .reporter
1124            .as_ref()
1125            .map(|reporter| reporter.on_build_start(source));
1126
1127        let (disk_filename, filename, metadata) = self
1128            .build_distribution(
1129                source,
1130                source_entry.path(),
1131                None,
1132                &cache_shard,
1133                NoSources::None,
1134            )
1135            .await?;
1136
1137        if let Some(task) = task {
1138            if let Some(reporter) = self.reporter.as_ref() {
1139                reporter.on_build_complete(source, task);
1140            }
1141        }
1142
1143        // Store the metadata.
1144        let metadata_entry = cache_shard.entry(METADATA);
1145        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1146            .await
1147            .map_err(Error::CacheWrite)?;
1148
1149        Ok(BuiltWheelMetadata {
1150            path: cache_shard.join(&disk_filename).into_boxed_path(),
1151            target: cache_shard.join(filename.stem()).into_boxed_path(),
1152            filename,
1153            hashes: revision.into_hashes(),
1154            cache_info,
1155            build_info,
1156        })
1157    }
1158
1159    /// Build the source distribution's metadata from a local archive (e.g., `.tar.gz` or `.zip`).
1160    ///
1161    /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid
1162    /// building the wheel.
1163    async fn archive_metadata(
1164        &self,
1165        source: &BuildableSource<'_>,
1166        resource: &PathSourceUrl<'_>,
1167        cache_shard: &CacheShard,
1168        hashes: HashPolicy<'_>,
1169    ) -> Result<ArchiveMetadata, Error> {
1170        let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1171
1172        // Fetch the revision for the source distribution.
1173        let LocalRevisionPointer { revision, .. } = self
1174            .archive_revision(source, resource, cache_shard, hashes)
1175            .await?;
1176
1177        // Before running the build, check that the hashes match.
1178        if !revision.satisfies(hashes) {
1179            return Err(Error::hash_mismatch(
1180                source.to_string(),
1181                hashes.digests(),
1182                revision.hashes(),
1183            ));
1184        }
1185
1186        // Scope all operations to the revision. Within the revision, there's no need to check for
1187        // freshness, since entries have to be fresher than the revision itself.
1188        let cache_shard = cache_shard.shard(revision.id());
1189        let source_entry = cache_shard.entry(SOURCE);
1190
1191        // If the metadata is static, return it.
1192        let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
1193            StaticMetadata::Some(metadata) => {
1194                return Ok(ArchiveMetadata {
1195                    metadata: Metadata::from_metadata23(metadata),
1196                    hashes: revision.into_hashes(),
1197                });
1198            }
1199            StaticMetadata::Dynamic => true,
1200            StaticMetadata::None => false,
1201        };
1202
1203        // If the cache contains compatible metadata, return it.
1204        let metadata_entry = cache_shard.entry(METADATA);
1205        match CachedMetadata::read(&metadata_entry).await {
1206            Ok(Some(metadata)) => {
1207                if metadata.matches(source.name(), source.version()) {
1208                    debug!("Using cached metadata for: {source}");
1209                    return Ok(ArchiveMetadata {
1210                        metadata: Metadata::from_metadata23(metadata.into()),
1211                        hashes: revision.into_hashes(),
1212                    });
1213                }
1214                debug!("Cached metadata does not match expected name and version for: {source}");
1215            }
1216            Ok(None) => {}
1217            Err(err) => {
1218                debug!("Failed to deserialize cached metadata for: {source} ({err})");
1219            }
1220        }
1221
1222        // Otherwise, we need a source distribution.
1223        let revision = if source_entry.path().is_dir() {
1224            revision
1225        } else {
1226            self.heal_archive_revision(source, resource, &source_entry, revision, hashes)
1227                .await?
1228        };
1229
1230        // If the backend supports `prepare_metadata_for_build_wheel`, use it.
1231        if let Some(metadata) = self
1232            .build_metadata(source, source_entry.path(), None, NoSources::None)
1233            .boxed_local()
1234            .await?
1235        {
1236            // If necessary, mark the metadata as dynamic.
1237            let metadata = if dynamic {
1238                ResolutionMetadata {
1239                    dynamic: true,
1240                    ..metadata
1241                }
1242            } else {
1243                metadata
1244            };
1245
1246            // Store the metadata.
1247            fs::create_dir_all(metadata_entry.dir())
1248                .await
1249                .map_err(Error::CacheWrite)?;
1250            write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1251                .await
1252                .map_err(Error::CacheWrite)?;
1253
1254            return Ok(ArchiveMetadata {
1255                metadata: Metadata::from_metadata23(metadata),
1256                hashes: revision.into_hashes(),
1257            });
1258        }
1259
1260        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
1261        let config_settings = self.config_settings_for(source.name());
1262        let extra_build_deps = self.extra_build_dependencies_for(source.name());
1263        let extra_build_variables = self.extra_build_variables_for(source.name());
1264        let build_info = BuildInfo::from_settings(
1265            config_settings.into_owned(),
1266            extra_build_deps.to_vec(),
1267            extra_build_variables.cloned(),
1268        );
1269        let cache_shard = build_info
1270            .cache_shard()
1271            .map(|digest| cache_shard.shard(digest))
1272            .unwrap_or(cache_shard);
1273
1274        // Otherwise, we need to build a wheel.
1275        let task = self
1276            .reporter
1277            .as_ref()
1278            .map(|reporter| reporter.on_build_start(source));
1279
1280        let (_disk_filename, _filename, metadata) = self
1281            .build_distribution(
1282                source,
1283                source_entry.path(),
1284                None,
1285                &cache_shard,
1286                NoSources::None,
1287            )
1288            .await?;
1289
1290        if let Some(task) = task {
1291            if let Some(reporter) = self.reporter.as_ref() {
1292                reporter.on_build_complete(source, task);
1293            }
1294        }
1295
1296        // If necessary, mark the metadata as dynamic.
1297        let metadata = if dynamic {
1298            ResolutionMetadata {
1299                dynamic: true,
1300                ..metadata
1301            }
1302        } else {
1303            metadata
1304        };
1305
1306        // Store the metadata.
1307        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1308            .await
1309            .map_err(Error::CacheWrite)?;
1310
1311        Ok(ArchiveMetadata {
1312            metadata: Metadata::from_metadata23(metadata),
1313            hashes: revision.into_hashes(),
1314        })
1315    }
1316
1317    /// Return the [`Revision`] for a local archive, refreshing it if necessary.
1318    async fn archive_revision(
1319        &self,
1320        source: &BuildableSource<'_>,
1321        resource: &PathSourceUrl<'_>,
1322        cache_shard: &CacheShard,
1323        hashes: HashPolicy<'_>,
1324    ) -> Result<LocalRevisionPointer, Error> {
1325        // Verify that the archive exists.
1326        if !resource.path.is_file() {
1327            return Err(Error::NotFound(resource.url.clone()));
1328        }
1329
1330        // Determine the last-modified time of the source distribution.
1331        let cache_info = CacheInfo::from_file(&resource.path).map_err(Error::CacheRead)?;
1332
1333        // Read the existing metadata from the cache.
1334        let revision_entry = cache_shard.entry(LOCAL_REVISION);
1335
1336        // If the revision already exists, return it. There's no need to check for freshness, since
1337        // we use an exact timestamp.
1338        if let Some(pointer) = LocalRevisionPointer::read_from(&revision_entry)? {
1339            if *pointer.cache_info() == cache_info {
1340                if pointer.revision().has_digests(hashes) {
1341                    return Ok(pointer);
1342                }
1343            }
1344        }
1345
1346        // Otherwise, we need to create a new revision.
1347        let revision = Revision::new();
1348
1349        // Unzip the archive to a temporary directory.
1350        debug!("Unpacking source distribution: {source}");
1351        let entry = cache_shard.shard(revision.id()).entry(SOURCE);
1352        let hashes = self
1353            .persist_archive(
1354                source,
1355                &resource.path,
1356                resource.ext,
1357                entry.path(),
1358                hashes,
1359                &[],
1360            )
1361            .await?;
1362
1363        // Include the hashes and cache info in the revision.
1364        let revision = revision.with_hashes(HashDigests::from(hashes));
1365
1366        // Persist the revision.
1367        let pointer = LocalRevisionPointer {
1368            cache_info,
1369            revision,
1370        };
1371        pointer.write_to(&revision_entry).await?;
1372
1373        Ok(pointer)
1374    }
1375
1376    /// Build a source distribution from a local source tree (i.e., directory), either editable or
1377    /// non-editable.
1378    async fn source_tree(
1379        &self,
1380        source: &BuildableSource<'_>,
1381        resource: &DirectorySourceUrl<'_>,
1382        tags: &Tags,
1383        hashes: HashPolicy<'_>,
1384    ) -> Result<BuiltWheelMetadata, Error> {
1385        // Before running the build, check that the hashes match.
1386        if hashes.requires_validation() {
1387            return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
1388        }
1389
1390        let cache_shard = self.build_context.cache().shard(
1391            CacheBucket::SourceDistributions,
1392            if resource.editable.unwrap_or(false) {
1393                WheelCache::Editable(resource.url).root()
1394            } else {
1395                WheelCache::Path(resource.url).root()
1396            },
1397        );
1398
1399        // Acquire the advisory lock.
1400        let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1401
1402        // Fetch the revision for the source distribution.
1403        let LocalRevisionPointer {
1404            cache_info,
1405            revision,
1406        } = self
1407            .source_tree_revision(source, resource, &cache_shard)
1408            .await?;
1409
1410        // Scope all operations to the revision. Within the revision, there's no need to check for
1411        // freshness, since entries have to be fresher than the revision itself.
1412        let cache_shard = cache_shard.shard(revision.id());
1413
1414        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
1415        let config_settings = self.config_settings_for(source.name());
1416        let extra_build_deps = self.extra_build_dependencies_for(source.name());
1417        let extra_build_variables = self.extra_build_variables_for(source.name());
1418        let build_info = BuildInfo::from_settings(
1419            config_settings.into_owned(),
1420            extra_build_deps.to_vec(),
1421            extra_build_variables.cloned(),
1422        );
1423        let cache_shard = build_info
1424            .cache_shard()
1425            .map(|digest| cache_shard.shard(digest))
1426            .unwrap_or(cache_shard);
1427
1428        // If the cache contains a compatible wheel, return it.
1429        if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1430            .ok()
1431            .flatten()
1432            .filter(|file| file.matches(source.name(), source.version()))
1433        {
1434            return Ok(BuiltWheelMetadata::from_file(
1435                file,
1436                revision.into_hashes(),
1437                cache_info,
1438                build_info,
1439            ));
1440        }
1441
1442        // Otherwise, we need to build a wheel.
1443        let task = self
1444            .reporter
1445            .as_ref()
1446            .map(|reporter| reporter.on_build_start(source));
1447
1448        let (disk_filename, filename, metadata) = self
1449            .build_distribution(
1450                source,
1451                resource.install_path,
1452                None,
1453                &cache_shard,
1454                self.build_context.sources().clone(),
1455            )
1456            .await?;
1457
1458        if let Some(task) = task {
1459            if let Some(reporter) = self.reporter.as_ref() {
1460                reporter.on_build_complete(source, task);
1461            }
1462        }
1463
1464        // Store the metadata.
1465        let metadata_entry = cache_shard.entry(METADATA);
1466        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1467            .await
1468            .map_err(Error::CacheWrite)?;
1469
1470        Ok(BuiltWheelMetadata {
1471            path: cache_shard.join(&disk_filename).into_boxed_path(),
1472            target: cache_shard.join(filename.stem()).into_boxed_path(),
1473            filename,
1474            hashes: revision.into_hashes(),
1475            cache_info,
1476            build_info,
1477        })
1478    }
1479
1480    /// Build the source distribution's metadata from a local source tree (i.e., a directory),
1481    /// either editable or non-editable.
1482    ///
1483    /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid
1484    /// building the wheel.
1485    async fn source_tree_metadata(
1486        &self,
1487        source: &BuildableSource<'_>,
1488        resource: &DirectorySourceUrl<'_>,
1489        hashes: HashPolicy<'_>,
1490        credentials_cache: &CredentialsCache,
1491    ) -> Result<ArchiveMetadata, Error> {
1492        // Before running the build, check that the hashes match.
1493        if hashes.requires_validation() {
1494            return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
1495        }
1496
1497        // Project-style resolution always lowers workspace members as editable. Tool-style
1498        // resolution preserves an explicit local requirement choice instead, defaulting implicit
1499        // workspace siblings to non-editable.
1500        let editable = self
1501            .build_context
1502            .source_tree_editable_policy()
1503            .workspace_member_editable(resource.editable);
1504
1505        // If the metadata is static, return it.
1506        let dynamic = match StaticMetadata::read(source, resource.install_path, None).await? {
1507            StaticMetadata::Some(metadata) => {
1508                return Ok(ArchiveMetadata::from(
1509                    Metadata::from_workspace(
1510                        metadata,
1511                        resource.install_path,
1512                        None,
1513                        self.build_context.locations(),
1514                        self.build_context.sources().clone(),
1515                        editable,
1516                        self.build_context.cache(),
1517                        self.build_context.workspace_cache(),
1518                        credentials_cache,
1519                    )
1520                    .await?,
1521                ));
1522            }
1523            StaticMetadata::Dynamic => true,
1524            StaticMetadata::None => false,
1525        };
1526
1527        let cache_shard = self.build_context.cache().shard(
1528            CacheBucket::SourceDistributions,
1529            if resource.editable.unwrap_or(false) {
1530                WheelCache::Editable(resource.url).root()
1531            } else {
1532                WheelCache::Path(resource.url).root()
1533            },
1534        );
1535
1536        // Acquire the advisory lock.
1537        let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
1538
1539        // Fetch the revision for the source distribution.
1540        let LocalRevisionPointer { revision, .. } = self
1541            .source_tree_revision(source, resource, &cache_shard)
1542            .await?;
1543
1544        // Scope all operations to the revision. Within the revision, there's no need to check for
1545        // freshness, since entries have to be fresher than the revision itself.
1546        let cache_shard = cache_shard.shard(revision.id());
1547
1548        // If the cache contains compatible metadata, return it.
1549        let metadata_entry = cache_shard.entry(METADATA);
1550        match CachedMetadata::read(&metadata_entry).await {
1551            Ok(Some(metadata)) => {
1552                if metadata.matches(source.name(), source.version()) {
1553                    debug!("Using cached metadata for: {source}");
1554
1555                    // If necessary, mark the metadata as dynamic.
1556                    let metadata = if dynamic {
1557                        ResolutionMetadata {
1558                            dynamic: true,
1559                            ..metadata.into()
1560                        }
1561                    } else {
1562                        metadata.into()
1563                    };
1564                    return Ok(ArchiveMetadata::from(
1565                        Metadata::from_workspace(
1566                            metadata,
1567                            resource.install_path,
1568                            None,
1569                            self.build_context.locations(),
1570                            self.build_context.sources().clone(),
1571                            editable,
1572                            self.build_context.cache(),
1573                            self.build_context.workspace_cache(),
1574                            credentials_cache,
1575                        )
1576                        .await?,
1577                    ));
1578                }
1579                debug!("Cached metadata does not match expected name and version for: {source}");
1580            }
1581            Ok(None) => {}
1582            Err(err) => {
1583                debug!("Failed to deserialize cached metadata for: {source} ({err})");
1584            }
1585        }
1586
1587        // If the backend supports `prepare_metadata_for_build_wheel`, use it.
1588        if let Some(metadata) = self
1589            .build_metadata(
1590                source,
1591                resource.install_path,
1592                None,
1593                self.build_context.sources().clone(),
1594            )
1595            .boxed_local()
1596            .await?
1597        {
1598            // Store the metadata.
1599            fs::create_dir_all(metadata_entry.dir())
1600                .await
1601                .map_err(Error::CacheWrite)?;
1602            write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1603                .await
1604                .map_err(Error::CacheWrite)?;
1605
1606            // If necessary, mark the metadata as dynamic.
1607            let metadata = if dynamic {
1608                ResolutionMetadata {
1609                    dynamic: true,
1610                    ..metadata
1611                }
1612            } else {
1613                metadata
1614            };
1615
1616            return Ok(ArchiveMetadata::from(
1617                Metadata::from_workspace(
1618                    metadata,
1619                    resource.install_path,
1620                    None,
1621                    self.build_context.locations(),
1622                    self.build_context.sources().clone(),
1623                    editable,
1624                    self.build_context.cache(),
1625                    self.build_context.workspace_cache(),
1626                    credentials_cache,
1627                )
1628                .await?,
1629            ));
1630        }
1631
1632        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
1633        let config_settings = self.config_settings_for(source.name());
1634        let extra_build_deps = self.extra_build_dependencies_for(source.name());
1635        let extra_build_variables = self.extra_build_variables_for(source.name());
1636        let build_info = BuildInfo::from_settings(
1637            config_settings.into_owned(),
1638            extra_build_deps.to_vec(),
1639            extra_build_variables.cloned(),
1640        );
1641        let cache_shard = build_info
1642            .cache_shard()
1643            .map(|digest| cache_shard.shard(digest))
1644            .unwrap_or(cache_shard);
1645
1646        // Otherwise, we need to build a wheel.
1647        let task = self
1648            .reporter
1649            .as_ref()
1650            .map(|reporter| reporter.on_build_start(source));
1651
1652        let (_disk_filename, _filename, metadata) = self
1653            .build_distribution(
1654                source,
1655                resource.install_path,
1656                None,
1657                &cache_shard,
1658                self.build_context.sources().clone(),
1659            )
1660            .await?;
1661
1662        if let Some(task) = task {
1663            if let Some(reporter) = self.reporter.as_ref() {
1664                reporter.on_build_complete(source, task);
1665            }
1666        }
1667
1668        // Store the metadata.
1669        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1670            .await
1671            .map_err(Error::CacheWrite)?;
1672
1673        // If necessary, mark the metadata as dynamic.
1674        let metadata = if dynamic {
1675            ResolutionMetadata {
1676                dynamic: true,
1677                ..metadata
1678            }
1679        } else {
1680            metadata
1681        };
1682
1683        Ok(ArchiveMetadata::from(
1684            Metadata::from_workspace(
1685                metadata,
1686                resource.install_path,
1687                None,
1688                self.build_context.locations(),
1689                self.build_context.sources().clone(),
1690                editable,
1691                self.build_context.cache(),
1692                self.build_context.workspace_cache(),
1693                credentials_cache,
1694            )
1695            .await?,
1696        ))
1697    }
1698
1699    /// Return the [`Revision`] for a local source tree, refreshing it if necessary.
1700    async fn source_tree_revision(
1701        &self,
1702        source: &BuildableSource<'_>,
1703        resource: &DirectorySourceUrl<'_>,
1704        cache_shard: &CacheShard,
1705    ) -> Result<LocalRevisionPointer, Error> {
1706        // Verify that the source tree exists.
1707        if !resource.install_path.is_dir() {
1708            return Err(Error::NotFound(resource.url.clone()));
1709        }
1710
1711        // Determine the last-modified time of the source distribution.
1712        let cache_info = CacheInfo::from_directory(resource.install_path)?;
1713
1714        // Read the existing metadata from the cache.
1715        let entry = cache_shard.entry(LOCAL_REVISION);
1716
1717        // If the revision is fresh, return it.
1718        if self
1719            .build_context
1720            .cache()
1721            .freshness(&entry, source.name(), source.source_tree())
1722            .map_err(Error::CacheRead)?
1723            .is_fresh()
1724        {
1725            match LocalRevisionPointer::read_from(&entry) {
1726                Ok(Some(pointer)) => {
1727                    if *pointer.cache_info() == cache_info {
1728                        return Ok(pointer);
1729                    }
1730
1731                    debug!("Cached revision does not match expected cache info for: {source}");
1732                }
1733                Ok(None) => {}
1734                Err(err) => {
1735                    debug!("Failed to deserialize cached revision for: {source} ({err})");
1736                }
1737            }
1738        }
1739
1740        // Otherwise, we need to create a new revision.
1741        let revision = Revision::new();
1742        let pointer = LocalRevisionPointer {
1743            cache_info,
1744            revision,
1745        };
1746        pointer.write_to(&entry).await?;
1747
1748        Ok(pointer)
1749    }
1750
1751    /// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
1752    pub(crate) async fn source_tree_requires_dist(
1753        &self,
1754        path: &Path,
1755        pyproject_toml: &PyProjectToml,
1756        credentials_cache: &CredentialsCache,
1757    ) -> Result<Option<RequiresDist>, Error> {
1758        // Attempt to read static metadata from the `pyproject.toml`.
1759        match uv_pypi_types::RequiresDist::from_pyproject_toml(pyproject_toml.clone()) {
1760            Ok(requires_dist) => {
1761                debug!("Found static `requires-dist` for: {}", path.display());
1762                let requires_dist = RequiresDist::from_project_maybe_workspace(
1763                    requires_dist,
1764                    path,
1765                    None,
1766                    self.build_context.locations(),
1767                    self.build_context.sources().clone(),
1768                    self.build_context
1769                        .source_tree_editable_policy()
1770                        .workspace_member_editable(None),
1771                    self.build_context.cache(),
1772                    self.build_context.workspace_cache(),
1773                    credentials_cache,
1774                )
1775                .await?;
1776                Ok(Some(requires_dist))
1777            }
1778            Err(
1779                err @ (uv_pypi_types::MetadataError::Pep508Error(_)
1780                | uv_pypi_types::MetadataError::DynamicField(_)
1781                | uv_pypi_types::MetadataError::FieldNotFound(_)
1782                | uv_pypi_types::MetadataError::PoetrySyntax),
1783            ) => {
1784                debug!(
1785                    "No static `requires-dist` available for: {} ({err:?})",
1786                    path.display()
1787                );
1788                Ok(None)
1789            }
1790            Err(err) => Err(Error::PyprojectToml(err)),
1791        }
1792    }
1793
1794    /// Return the [`RevisionHashes`] for an archive stored in a Git repository.
1795    async fn git_archive_revision(
1796        &self,
1797        source: &BuildableSource<'_>,
1798        resource: &GitPathSourceUrl<'_>,
1799        fetch: &Fetch,
1800        cache_shard: &CacheShard,
1801        hashes: HashPolicy<'_>,
1802    ) -> Result<RevisionHashes, Error> {
1803        // Validate that LFS artifacts were fully initialized.
1804        if resource.git.lfs().enabled() && !fetch.lfs_ready() {
1805            if GIT_LFS.is_err() {
1806                return Err(Error::MissingSourceDistGitLfsArtifacts(
1807                    resource.url.to_url(),
1808                    GitError::GitLfsNotFound,
1809                ));
1810            }
1811            return Err(Error::MissingSourceDistGitLfsArtifacts(
1812                resource.url.to_url(),
1813                GitError::GitLfsNotConfigured,
1814            ));
1815        }
1816
1817        // Verify that the archive exists.
1818        let install_path = fetch.path().join(&resource.path);
1819        if !install_path.is_file() {
1820            return Err(Error::NotFound(resource.url.to_url()));
1821        }
1822
1823        // Read the existing metadata from the cache.
1824        let revision_entry = cache_shard.entry(HASHES);
1825
1826        // If the revision already exists, return it. There's no need to check for freshness, since
1827        // everything is scoped to a Git commit.
1828        if let Some(revision) = RevisionHashes::read_from(&revision_entry)? {
1829            if revision.has_digests(hashes) {
1830                return Ok(revision);
1831            }
1832        }
1833
1834        // Otherwise, we need to unzip the archive, or at least compute the hashes.
1835        debug!("Unpacking source distribution: {source}");
1836        let entry = cache_shard.entry(SOURCE);
1837        let hashes = self
1838            .persist_archive(
1839                source,
1840                &install_path,
1841                resource.ext,
1842                entry.path(),
1843                hashes,
1844                &[],
1845            )
1846            .await?;
1847
1848        // Persist the revision.
1849        let revision = RevisionHashes { hashes };
1850        revision.write_to(&revision_entry).await?;
1851
1852        Ok(revision)
1853    }
1854
1855    /// Build a source distribution from a Git repository.
1856    async fn git_archive(
1857        &self,
1858        source: &BuildableSource<'_>,
1859        resource: &GitPathSourceUrl<'_>,
1860        tags: &Tags,
1861        hashes: HashPolicy<'_>,
1862        client: &ManagedClient<'_>,
1863    ) -> Result<BuiltWheelMetadata, Error> {
1864        // Fetch the Git repository.
1865        let fetch = self
1866            .build_context
1867            .git()
1868            .fetch(
1869                resource.git,
1870                client.unmanaged.git_http_settings(resource.git.url()),
1871                self.build_context.cache().bucket(CacheBucket::Git),
1872                self.reporter
1873                    .clone()
1874                    .map(|reporter| reporter.into_git_reporter()),
1875            )
1876            .await?;
1877
1878        let git_sha = fetch.git().precise().expect("Exact commit after checkout");
1879        let cache_shard = self.build_context.cache().shard(
1880            CacheBucket::SourceDistributions,
1881            WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
1882        );
1883
1884        // Fetch the revision for the source distribution.
1885        let revision = self
1886            .git_archive_revision(source, resource, &fetch, &cache_shard, hashes)
1887            .await?;
1888
1889        // Before running the build, check that the hashes match.
1890        if !revision.satisfies(hashes) {
1891            return Err(Error::hash_mismatch(
1892                source.to_string(),
1893                hashes.digests(),
1894                revision.hashes(),
1895            ));
1896        }
1897
1898        let source_entry = cache_shard.entry(SOURCE);
1899
1900        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
1901        let config_settings = self.config_settings_for(source.name());
1902        let extra_build_deps = self.extra_build_dependencies_for(source.name());
1903        let extra_build_variables = self.extra_build_variables_for(source.name());
1904        let build_info = BuildInfo::from_settings(
1905            config_settings.into_owned(),
1906            extra_build_deps.to_vec(),
1907            extra_build_variables.cloned(),
1908        );
1909        let cache_shard = build_info
1910            .cache_shard()
1911            .map(|digest| cache_shard.shard(digest))
1912            .unwrap_or(cache_shard);
1913
1914        // If the cache contains a compatible wheel, return it.
1915        if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
1916            .ok()
1917            .flatten()
1918            .filter(|file| file.matches(source.name(), source.version()))
1919        {
1920            return Ok(BuiltWheelMetadata::from_file(
1921                file,
1922                revision.into_hashes(),
1923                CacheInfo::default(),
1924                build_info,
1925            ));
1926        }
1927
1928        // Otherwise, we need to build a wheel.
1929        let task = self
1930            .reporter
1931            .as_ref()
1932            .map(|reporter| reporter.on_build_start(source));
1933
1934        let (disk_filename, filename, metadata) = self
1935            .build_distribution(
1936                source,
1937                source_entry.path(),
1938                None,
1939                &cache_shard,
1940                NoSources::None,
1941            )
1942            .await?;
1943
1944        if let Some(task) = task {
1945            if let Some(reporter) = self.reporter.as_ref() {
1946                reporter.on_build_complete(source, task);
1947            }
1948        }
1949
1950        // Store the metadata.
1951        let metadata_entry = cache_shard.entry(METADATA);
1952        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
1953            .await
1954            .map_err(Error::CacheWrite)?;
1955
1956        Ok(BuiltWheelMetadata {
1957            path: cache_shard.join(&disk_filename).into_boxed_path(),
1958            target: cache_shard.join(filename.stem()).into_boxed_path(),
1959            filename,
1960            hashes: revision.into_hashes(),
1961            cache_info: CacheInfo::default(),
1962            build_info,
1963        })
1964    }
1965
1966    /// Build a source distribution from a Git repository.
1967    async fn git_archive_metadata(
1968        &self,
1969        source: &BuildableSource<'_>,
1970        resource: &GitPathSourceUrl<'_>,
1971        hashes: HashPolicy<'_>,
1972        client: &ManagedClient<'_>,
1973    ) -> Result<ArchiveMetadata, Error> {
1974        // Fetch the Git repository.
1975        let fetch = self
1976            .build_context
1977            .git()
1978            .fetch(
1979                resource.git,
1980                client.unmanaged.git_http_settings(resource.git.url()),
1981                self.build_context.cache().bucket(CacheBucket::Git),
1982                self.reporter
1983                    .clone()
1984                    .map(|reporter| reporter.into_git_reporter()),
1985            )
1986            .await?;
1987
1988        let git_sha = fetch.git().precise().expect("Exact commit after checkout");
1989        let cache_shard = self.build_context.cache().shard(
1990            CacheBucket::SourceDistributions,
1991            WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
1992        );
1993
1994        // Fetch the revision for the source distribution.
1995        let revision = self
1996            .git_archive_revision(source, resource, &fetch, &cache_shard, hashes)
1997            .await?;
1998
1999        // Before running the build, check that the hashes match.
2000        if !revision.satisfies(hashes) {
2001            return Err(Error::hash_mismatch(
2002                source.to_string(),
2003                hashes.digests(),
2004                revision.hashes(),
2005            ));
2006        }
2007
2008        let source_entry = cache_shard.entry(SOURCE);
2009
2010        // If the metadata is static, return it.
2011        let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
2012            StaticMetadata::Some(metadata) => {
2013                return Ok(ArchiveMetadata {
2014                    metadata: Metadata::from_metadata23(metadata),
2015                    hashes: revision.into_hashes(),
2016                });
2017            }
2018            StaticMetadata::Dynamic => true,
2019            StaticMetadata::None => false,
2020        };
2021
2022        // If the cache contains compatible metadata, return it.
2023        let metadata_entry = cache_shard.entry(METADATA);
2024        match CachedMetadata::read(&metadata_entry).await {
2025            Ok(Some(metadata)) => {
2026                if metadata.matches(source.name(), source.version()) {
2027                    debug!("Using cached metadata for: {source}");
2028                    return Ok(ArchiveMetadata {
2029                        metadata: Metadata::from_metadata23(metadata.into()),
2030                        hashes: revision.into_hashes(),
2031                    });
2032                }
2033                debug!("Cached metadata does not match expected name and version for: {source}");
2034            }
2035            Ok(None) => {}
2036            Err(err) => {
2037                debug!("Failed to deserialize cached metadata for: {source} ({err})");
2038            }
2039        }
2040
2041        // If the backend supports `prepare_metadata_for_build_wheel`, use it.
2042        if let Some(metadata) = self
2043            .build_metadata(source, source_entry.path(), None, NoSources::None)
2044            .boxed_local()
2045            .await?
2046        {
2047            // If necessary, mark the metadata as dynamic.
2048            let metadata = if dynamic {
2049                ResolutionMetadata {
2050                    dynamic: true,
2051                    ..metadata
2052                }
2053            } else {
2054                metadata
2055            };
2056
2057            // Store the metadata.
2058            fs::create_dir_all(metadata_entry.dir())
2059                .await
2060                .map_err(Error::CacheWrite)?;
2061            write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2062                .await
2063                .map_err(Error::CacheWrite)?;
2064
2065            return Ok(ArchiveMetadata {
2066                metadata: Metadata::from_metadata23(metadata),
2067                hashes: revision.into_hashes(),
2068            });
2069        }
2070
2071        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
2072        let config_settings = self.config_settings_for(source.name());
2073        let extra_build_deps = self.extra_build_dependencies_for(source.name());
2074        let extra_build_variables = self.extra_build_variables_for(source.name());
2075        let build_info = BuildInfo::from_settings(
2076            config_settings.into_owned(),
2077            extra_build_deps.to_vec(),
2078            extra_build_variables.cloned(),
2079        );
2080        let cache_shard = build_info
2081            .cache_shard()
2082            .map(|digest| cache_shard.shard(digest))
2083            .unwrap_or(cache_shard);
2084
2085        // Otherwise, we need to build a wheel.
2086        let task = self
2087            .reporter
2088            .as_ref()
2089            .map(|reporter| reporter.on_build_start(source));
2090
2091        let (_disk_filename, _filename, metadata) = self
2092            .build_distribution(
2093                source,
2094                source_entry.path(),
2095                None,
2096                &cache_shard,
2097                NoSources::None,
2098            )
2099            .await?;
2100
2101        if let Some(task) = task {
2102            if let Some(reporter) = self.reporter.as_ref() {
2103                reporter.on_build_complete(source, task);
2104            }
2105        }
2106
2107        // If necessary, mark the metadata as dynamic.
2108        let metadata = if dynamic {
2109            ResolutionMetadata {
2110                dynamic: true,
2111                ..metadata
2112            }
2113        } else {
2114            metadata
2115        };
2116
2117        // Store the metadata.
2118        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2119            .await
2120            .map_err(Error::CacheWrite)?;
2121
2122        Ok(ArchiveMetadata {
2123            metadata: Metadata::from_metadata23(metadata),
2124            hashes: revision.into_hashes(),
2125        })
2126    }
2127
2128    /// Build a source distribution from a Git repository.
2129    async fn git_source_tree(
2130        &self,
2131        source: &BuildableSource<'_>,
2132        resource: &GitDirectorySourceUrl<'_>,
2133        tags: &Tags,
2134        hashes: HashPolicy<'_>,
2135        client: &ManagedClient<'_>,
2136    ) -> Result<BuiltWheelMetadata, Error> {
2137        // Before running the build, check that the hashes match.
2138        if hashes.requires_validation() {
2139            return Err(Error::HashesNotSupportedGit(source.to_string()));
2140        }
2141
2142        let fetch = fetch_git_source_tree(
2143            self.build_context.git(),
2144            resource.git,
2145            resource.url.to_url(),
2146            resource.subdirectory,
2147            client.unmanaged.git_http_settings(resource.git.url()),
2148            self.build_context.cache(),
2149            self.reporter
2150                .clone()
2151                .map(|reporter| reporter.into_git_reporter()),
2152        )
2153        .await?;
2154
2155        let git_sha = fetch.git().precise().expect("Exact commit after checkout");
2156        let cache_shard = self.build_context.cache().shard(
2157            CacheBucket::SourceDistributions,
2158            WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
2159        );
2160        let metadata_entry = cache_shard.entry(METADATA);
2161
2162        // Acquire the advisory lock.
2163        let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
2164
2165        // We don't track any cache information for Git-based source distributions; they're assumed
2166        // to be immutable.
2167        let cache_info = CacheInfo::default();
2168
2169        // We don't compute hashes for Git-based source distributions, since the Git commit SHA is
2170        // used as the identifier.
2171        let hashes = HashDigests::empty();
2172
2173        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
2174        let config_settings = self.config_settings_for(source.name());
2175        let extra_build_deps = self.extra_build_dependencies_for(source.name());
2176        let extra_build_variables = self.extra_build_variables_for(source.name());
2177        let build_info = BuildInfo::from_settings(
2178            config_settings.into_owned(),
2179            extra_build_deps.to_vec(),
2180            extra_build_variables.cloned(),
2181        );
2182        let cache_shard = build_info
2183            .cache_shard()
2184            .map(|digest| cache_shard.shard(digest))
2185            .unwrap_or(cache_shard);
2186
2187        // If the cache contains a compatible wheel, return it.
2188        if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard)
2189            .ok()
2190            .flatten()
2191            .filter(|file| file.matches(source.name(), source.version()))
2192        {
2193            return Ok(BuiltWheelMetadata::from_file(
2194                file, hashes, cache_info, build_info,
2195            ));
2196        }
2197
2198        let task = self
2199            .reporter
2200            .as_ref()
2201            .map(|reporter| reporter.on_build_start(source));
2202
2203        let (disk_filename, filename, metadata) = self
2204            .build_distribution(
2205                source,
2206                fetch.path(),
2207                resource.subdirectory,
2208                &cache_shard,
2209                self.build_context.sources().clone(),
2210            )
2211            .await?;
2212
2213        if let Some(task) = task {
2214            if let Some(reporter) = self.reporter.as_ref() {
2215                reporter.on_build_complete(source, task);
2216            }
2217        }
2218
2219        // Store the metadata.
2220        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2221            .await
2222            .map_err(Error::CacheWrite)?;
2223
2224        Ok(BuiltWheelMetadata {
2225            path: cache_shard.join(&disk_filename).into_boxed_path(),
2226            target: cache_shard.join(filename.stem()).into_boxed_path(),
2227            filename,
2228            hashes,
2229            cache_info,
2230            build_info,
2231        })
2232    }
2233
2234    /// Build the source distribution's metadata from a Git repository.
2235    ///
2236    /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid
2237    /// building the wheel.
2238    async fn git_source_tree_metadata(
2239        &self,
2240        source: &BuildableSource<'_>,
2241        resource: &GitDirectorySourceUrl<'_>,
2242        hashes: HashPolicy<'_>,
2243        client: &ManagedClient<'_>,
2244        credentials_cache: &CredentialsCache,
2245    ) -> Result<ArchiveMetadata, Error> {
2246        // Before running the build, check that the hashes match.
2247        if hashes.requires_validation() {
2248            return Err(Error::HashesNotSupportedGit(source.to_string()));
2249        }
2250
2251        // If the reference appears to be a commit, and we've already checked it out, avoid taking
2252        // the GitHub fast path.
2253        let cache_shard = resource
2254            .git
2255            .reference()
2256            .as_str()
2257            .and_then(|reference| GitOid::from_str(reference).ok())
2258            .map(|oid| {
2259                self.build_context.cache().shard(
2260                    CacheBucket::SourceDistributions,
2261                    WheelCache::Git(resource.url, oid.as_short_str()).root(),
2262                )
2263            });
2264        if cache_shard
2265            .as_ref()
2266            .is_some_and(|cache_shard| cache_shard.is_dir())
2267        {
2268            debug!("Skipping GitHub fast path for: {source} (shard exists)");
2269        } else {
2270            debug!("Attempting GitHub fast path for: {source}");
2271
2272            // If this is GitHub URL, attempt to resolve to a precise commit using the GitHub API.
2273            match self
2274                .build_context
2275                .git()
2276                .github_fast_path(
2277                    resource.git,
2278                    client
2279                        .unmanaged
2280                        .uncached_client(resource.git.url())
2281                        .raw_client(),
2282                )
2283                .await
2284            {
2285                Ok(Some(precise)) => {
2286                    // There's no need to check the cache, since we can't use cached metadata if there are
2287                    // sources, and we can't know if there are sources without fetching the
2288                    // `pyproject.toml`.
2289                    //
2290                    // For the same reason, there's no need to write to the cache, since we won't be able to
2291                    // use it on subsequent runs.
2292                    match self
2293                        .github_metadata(precise, source, resource, client)
2294                        .await
2295                    {
2296                        Ok(Some(metadata)) => {
2297                            // Validate the metadata, but ignore it if the metadata doesn't match.
2298                            match validate_metadata(source, &metadata) {
2299                                Ok(()) => {
2300                                    debug!(
2301                                        "Found static metadata via GitHub fast path for: {source}"
2302                                    );
2303                                    return Ok(ArchiveMetadata {
2304                                        metadata: Metadata::from_metadata23(metadata),
2305                                        hashes: HashDigests::empty(),
2306                                    });
2307                                }
2308                                Err(err) => {
2309                                    debug!(
2310                                        "Ignoring `pyproject.toml` from GitHub for {source}: {err}"
2311                                    );
2312                                }
2313                            }
2314                        }
2315                        Ok(None) => {
2316                            // Nothing to do.
2317                        }
2318                        Err(err) => {
2319                            debug!(
2320                                "Failed to fetch `pyproject.toml` via GitHub fast path for: {source} ({err})"
2321                            );
2322                        }
2323                    }
2324                }
2325                Ok(None) => {
2326                    // Nothing to do.
2327                }
2328                Err(err) => {
2329                    debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})");
2330                }
2331            }
2332        }
2333
2334        let fetch = fetch_git_source_tree(
2335            self.build_context.git(),
2336            resource.git,
2337            resource.url.to_url(),
2338            resource.subdirectory,
2339            client.unmanaged.git_http_settings(resource.git.url()),
2340            self.build_context.cache(),
2341            self.reporter
2342                .clone()
2343                .map(|reporter| reporter.into_git_reporter()),
2344        )
2345        .await?;
2346
2347        let git_sha = fetch.git().precise().expect("Exact commit after checkout");
2348        let cache_shard = self.build_context.cache().shard(
2349            CacheBucket::SourceDistributions,
2350            WheelCache::Git(resource.url, git_sha.as_short_str()).root(),
2351        );
2352        let metadata_entry = cache_shard.entry(METADATA);
2353
2354        // Acquire the advisory lock.
2355        let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?;
2356
2357        let path = if let Some(subdirectory) = resource.subdirectory {
2358            Cow::Owned(fetch.path().join(subdirectory))
2359        } else {
2360            Cow::Borrowed(fetch.path())
2361        };
2362
2363        let git_member = GitWorkspaceMember {
2364            fetch_root: fetch.path(),
2365            git_source: resource,
2366        };
2367
2368        // If the metadata is static, return it.
2369        let dynamic =
2370            match StaticMetadata::read(source, fetch.path(), resource.subdirectory).await? {
2371                StaticMetadata::Some(metadata) => {
2372                    return Ok(ArchiveMetadata::from(
2373                        Metadata::from_workspace(
2374                            metadata,
2375                            &path,
2376                            Some(&git_member),
2377                            self.build_context.locations(),
2378                            self.build_context.sources().clone(),
2379                            self.build_context
2380                                .source_tree_editable_policy()
2381                                .workspace_member_editable(None),
2382                            self.build_context.cache(),
2383                            self.build_context.workspace_cache(),
2384                            credentials_cache,
2385                        )
2386                        .await?,
2387                    ));
2388                }
2389                StaticMetadata::Dynamic => true,
2390                StaticMetadata::None => false,
2391            };
2392
2393        // If the cache contains compatible metadata, return it.
2394        if self
2395            .build_context
2396            .cache()
2397            .freshness(&metadata_entry, source.name(), source.source_tree())
2398            .map_err(Error::CacheRead)?
2399            .is_fresh()
2400        {
2401            match CachedMetadata::read(&metadata_entry).await {
2402                Ok(Some(metadata)) => {
2403                    if metadata.matches(source.name(), source.version()) {
2404                        debug!("Using cached metadata for: {source}");
2405
2406                        let git_member = GitWorkspaceMember {
2407                            fetch_root: fetch.path(),
2408                            git_source: resource,
2409                        };
2410                        return Ok(ArchiveMetadata::from(
2411                            Metadata::from_workspace(
2412                                metadata.into(),
2413                                &path,
2414                                Some(&git_member),
2415                                self.build_context.locations(),
2416                                self.build_context.sources().clone(),
2417                                self.build_context
2418                                    .source_tree_editable_policy()
2419                                    .workspace_member_editable(None),
2420                                self.build_context.cache(),
2421                                self.build_context.workspace_cache(),
2422                                credentials_cache,
2423                            )
2424                            .await?,
2425                        ));
2426                    }
2427                    debug!(
2428                        "Cached metadata does not match expected name and version for: {source}"
2429                    );
2430                }
2431                Ok(None) => {}
2432                Err(err) => {
2433                    debug!("Failed to deserialize cached metadata for: {source} ({err})");
2434                }
2435            }
2436        }
2437
2438        // If the backend supports `prepare_metadata_for_build_wheel`, use it.
2439        if let Some(metadata) = self
2440            .build_metadata(
2441                source,
2442                fetch.path(),
2443                resource.subdirectory,
2444                self.build_context.sources().clone(),
2445            )
2446            .boxed_local()
2447            .await?
2448        {
2449            // If necessary, mark the metadata as dynamic.
2450            let metadata = if dynamic {
2451                ResolutionMetadata {
2452                    dynamic: true,
2453                    ..metadata
2454                }
2455            } else {
2456                metadata
2457            };
2458
2459            // Store the metadata.
2460            fs::create_dir_all(metadata_entry.dir())
2461                .await
2462                .map_err(Error::CacheWrite)?;
2463            write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2464                .await
2465                .map_err(Error::CacheWrite)?;
2466
2467            return Ok(ArchiveMetadata::from(
2468                Metadata::from_workspace(
2469                    metadata,
2470                    &path,
2471                    Some(&git_member),
2472                    self.build_context.locations(),
2473                    self.build_context.sources().clone(),
2474                    self.build_context
2475                        .source_tree_editable_policy()
2476                        .workspace_member_editable(None),
2477                    self.build_context.cache(),
2478                    self.build_context.workspace_cache(),
2479                    credentials_cache,
2480                )
2481                .await?,
2482            ));
2483        }
2484
2485        // If there are build settings or extra build dependencies, we need to scope to a cache shard.
2486        let config_settings = self.config_settings_for(source.name());
2487        let extra_build_deps = self.extra_build_dependencies_for(source.name());
2488        let extra_build_variables = self.extra_build_variables_for(source.name());
2489        let build_info = BuildInfo::from_settings(
2490            config_settings.into_owned(),
2491            extra_build_deps.to_vec(),
2492            extra_build_variables.cloned(),
2493        );
2494        let cache_shard = build_info
2495            .cache_shard()
2496            .map(|digest| cache_shard.shard(digest))
2497            .unwrap_or(cache_shard);
2498
2499        // Otherwise, we need to build a wheel.
2500        let task = self
2501            .reporter
2502            .as_ref()
2503            .map(|reporter| reporter.on_build_start(source));
2504
2505        let (_disk_filename, _filename, metadata) = self
2506            .build_distribution(
2507                source,
2508                fetch.path(),
2509                resource.subdirectory,
2510                &cache_shard,
2511                self.build_context.sources().clone(),
2512            )
2513            .await?;
2514
2515        if let Some(task) = task {
2516            if let Some(reporter) = self.reporter.as_ref() {
2517                reporter.on_build_complete(source, task);
2518            }
2519        }
2520
2521        // If necessary, mark the metadata as dynamic.
2522        let metadata = if dynamic {
2523            ResolutionMetadata {
2524                dynamic: true,
2525                ..metadata
2526            }
2527        } else {
2528            metadata
2529        };
2530
2531        // Store the metadata.
2532        write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
2533            .await
2534            .map_err(Error::CacheWrite)?;
2535
2536        Ok(ArchiveMetadata::from(
2537            Metadata::from_workspace(
2538                metadata,
2539                fetch.path(),
2540                Some(&git_member),
2541                self.build_context.locations(),
2542                self.build_context.sources().clone(),
2543                self.build_context
2544                    .source_tree_editable_policy()
2545                    .workspace_member_editable(None),
2546                self.build_context.cache(),
2547                self.build_context.workspace_cache(),
2548                credentials_cache,
2549            )
2550            .await?,
2551        ))
2552    }
2553
2554    /// Resolve a source to a specific revision.
2555    pub(crate) async fn resolve_revision(
2556        &self,
2557        source: &BuildableSource<'_>,
2558        client: &ManagedClient<'_>,
2559    ) -> Result<Option<GitOid>, Error> {
2560        let git = match source {
2561            BuildableSource::Dist(SourceDist::GitDirectory(source)) => &*source.git,
2562            BuildableSource::Dist(SourceDist::GitPath(source)) => &*source.git,
2563            BuildableSource::Url(SourceUrl::GitDirectory(source)) => source.git,
2564            BuildableSource::Url(SourceUrl::GitPath(source)) => source.git,
2565            _ => {
2566                return Ok(None);
2567            }
2568        };
2569
2570        // If the URL is already precise, return it.
2571        if let Some(precise) = self.build_context.git().get_precise(git) {
2572            debug!("Precise commit already known: {source}");
2573            return Ok(Some(precise));
2574        }
2575
2576        // If this is GitHub URL, attempt to resolve to a precise commit using the GitHub API.
2577        if let Some(precise) = self
2578            .build_context
2579            .git()
2580            .github_fast_path(
2581                git,
2582                client.unmanaged.uncached_client(git.url()).raw_client(),
2583            )
2584            .await?
2585        {
2586            debug!("Resolved to precise commit via GitHub fast path: {source}");
2587            return Ok(Some(precise));
2588        }
2589
2590        // Otherwise, fetch the Git repository.
2591        let fetch = self
2592            .build_context
2593            .git()
2594            .fetch(
2595                git,
2596                client.unmanaged.git_http_settings(git.url()),
2597                self.build_context.cache().bucket(CacheBucket::Git),
2598                self.reporter
2599                    .clone()
2600                    .map(|reporter| reporter.into_git_reporter()),
2601            )
2602            .await?;
2603
2604        Ok(fetch.git().precise())
2605    }
2606
2607    /// Fetch static [`ResolutionMetadata`] from a GitHub repository, if possible.
2608    ///
2609    /// Attempts to fetch the `pyproject.toml` from the resolved commit using the GitHub API.
2610    async fn github_metadata(
2611        &self,
2612        commit: GitOid,
2613        source: &BuildableSource<'_>,
2614        resource: &GitDirectorySourceUrl<'_>,
2615        client: &ManagedClient<'_>,
2616    ) -> Result<Option<ResolutionMetadata>, Error> {
2617        let GitDirectorySourceUrl {
2618            git, subdirectory, ..
2619        } = resource;
2620
2621        // The fast path isn't available for subdirectories. If a `pyproject.toml` is in a
2622        // subdirectory, it could be part of a workspace; and if it's part of a workspace, it could
2623        // have `tool.uv.sources` entries that it inherits from the workspace root.
2624        if subdirectory.is_some() {
2625            return Ok(None);
2626        }
2627
2628        let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(git.repository())
2629        else {
2630            return Ok(None);
2631        };
2632
2633        // Fetch the `pyproject.toml` from the resolved commit.
2634        let url =
2635            format!("https://raw.githubusercontent.com/{owner}/{repo}/{commit}/pyproject.toml");
2636
2637        debug!("Attempting to fetch `pyproject.toml` from: {url}");
2638
2639        let content = client
2640            .managed(async |client| {
2641                let response = client.uncached_client(git.url()).get(&url).send().await?;
2642
2643                // If the `pyproject.toml` does not exist, the GitHub API will return a 404.
2644                if response.status() == StatusCode::NOT_FOUND {
2645                    return Ok::<Option<String>, Error>(None);
2646                }
2647                response.error_for_status_ref()?;
2648
2649                let content = response.text().await?;
2650                Ok::<Option<String>, Error>(Some(content))
2651            })
2652            .await?;
2653
2654        let Some(content) = content else {
2655            debug!("GitHub API returned a 404 for: {url}");
2656            return Ok(None);
2657        };
2658
2659        // Parse the `pyproject.toml`.
2660        let pyproject_toml = match PyProjectToml::from_toml(&content, source) {
2661            Ok(metadata) => metadata,
2662            Err(
2663                uv_pypi_types::MetadataError::InvalidPyprojectTomlSyntax(..)
2664                | uv_pypi_types::MetadataError::InvalidPyprojectTomlSchema(..),
2665            ) => {
2666                debug!("Failed to read `pyproject.toml` from GitHub API for: {url}");
2667                return Ok(None);
2668            }
2669            Err(err) => return Err(err.into()),
2670        };
2671
2672        // Parse the metadata.
2673        let metadata =
2674            match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) {
2675                Ok(metadata) => metadata,
2676                Err(
2677                    uv_pypi_types::MetadataError::Pep508Error(..)
2678                    | uv_pypi_types::MetadataError::DynamicField(..)
2679                    | uv_pypi_types::MetadataError::FieldNotFound(..)
2680                    | uv_pypi_types::MetadataError::PoetrySyntax,
2681                ) => {
2682                    debug!("Failed to extract static metadata from GitHub API for: {url}");
2683                    return Ok(None);
2684                }
2685                Err(err) => return Err(err.into()),
2686            };
2687
2688        // Determine whether the project has `tool.uv.sources`. If the project has sources, it must
2689        // be lowered, which requires access to the workspace. For example, it could have workspace
2690        // members that need to be translated to concrete paths on disk.
2691        //
2692        // TODO(charlie): We could still use the `pyproject.toml` if the sources are all `git` or
2693        // `url` sources; this is only applicable to `workspace` and `path` sources. It's awkward,
2694        // though, because we'd need to pass a path into the lowering routine, and that path would
2695        // be incorrect (we'd just be relying on it not being used).
2696        match has_sources(&content) {
2697            Ok(false) => {}
2698            Ok(true) => {
2699                debug!("Skipping GitHub fast path; `pyproject.toml` has sources: {url}");
2700                return Ok(None);
2701            }
2702            Err(err) => {
2703                debug!("Failed to parse `tool.uv.sources` from GitHub API for: {url} ({err})");
2704                return Ok(None);
2705            }
2706        }
2707
2708        Ok(Some(metadata))
2709    }
2710
2711    /// Heal a [`Revision`] for a local archive.
2712    async fn heal_archive_revision(
2713        &self,
2714        source: &BuildableSource<'_>,
2715        resource: &PathSourceUrl<'_>,
2716        entry: &CacheEntry,
2717        revision: Revision,
2718        hashes: HashPolicy<'_>,
2719    ) -> Result<Revision, Error> {
2720        warn!("Re-extracting missing source distribution: {source}");
2721
2722        let hashes = self
2723            .persist_archive(
2724                source,
2725                &resource.path,
2726                resource.ext,
2727                entry.path(),
2728                hashes,
2729                revision.hashes(),
2730            )
2731            .await?;
2732        Ok(revision.with_hashes(HashDigests::from(hashes)))
2733    }
2734
2735    /// Heal a [`Revision`] for a remote archive.
2736    async fn heal_url_revision(
2737        &self,
2738        source: &BuildableSource<'_>,
2739        ext: SourceDistExtension,
2740        url: &DisplaySafeUrl,
2741        index: Option<&IndexUrl>,
2742        entry: &CacheEntry,
2743        revision: Revision,
2744        hashes: HashPolicy<'_>,
2745        client: &ManagedClient<'_>,
2746    ) -> Result<Revision, Error> {
2747        warn!("Re-downloading missing source distribution: {source}");
2748        let cache_entry = entry.shard().entry(HTTP_REVISION);
2749
2750        // Determine the cache control policy for the request.
2751        let cache_control = match client.unmanaged.connectivity() {
2752            Connectivity::Online
2753                if let Some(header) = index.and_then(|index| {
2754                    self.build_context
2755                        .locations()
2756                        .artifact_cache_control_for(index)
2757                }) =>
2758            {
2759                CacheControl::Override(header)
2760            }
2761            Connectivity::Online => CacheControl::from(
2762                self.build_context
2763                    .cache()
2764                    .freshness(&cache_entry, source.name(), source.source_tree())
2765                    .map_err(Error::CacheRead)?,
2766            ),
2767            Connectivity::Offline => CacheControl::AllowStale,
2768        };
2769
2770        let download = |response| {
2771            async {
2772                let (hashes, size) = self
2773                    .download_archive(
2774                        response,
2775                        source,
2776                        ext,
2777                        entry.path(),
2778                        hashes,
2779                        revision.hashes(),
2780                    )
2781                    .await?;
2782                Ok(revision
2783                    .clone()
2784                    .with_hashes(HashDigests::from(hashes))
2785                    .with_size(size))
2786            }
2787            .boxed_local()
2788            .instrument(info_span!("download", source_dist = %source))
2789        };
2790        client
2791            .managed(async |client| {
2792                client
2793                    .cached_client()
2794                    .skip_cache_with_retry(
2795                        Self::request(url.clone(), client)?,
2796                        &cache_entry,
2797                        cache_control.clone(),
2798                        download,
2799                    )
2800                    .await
2801                    .map_err(|err| match err {
2802                        CachedClientError::Callback { err, .. } => err,
2803                        CachedClientError::Client(err) => Error::Client(err),
2804                    })
2805            })
2806            .await
2807    }
2808
2809    /// Download, extract, validate, and persist a source distribution into the cache.
2810    async fn download_archive(
2811        &self,
2812        response: Response,
2813        source: &BuildableSource<'_>,
2814        ext: SourceDistExtension,
2815        target: &Path,
2816        hash_policy: HashPolicy<'_>,
2817        existing_hashes: &[HashDigest],
2818    ) -> Result<(Vec<HashDigest>, u64), Error> {
2819        let reader = response
2820            .bytes_stream()
2821            .map_err(std::io::Error::other)
2822            .into_async_read();
2823        let expected_size = match source {
2824            BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => {
2825                dist.size()
2826            }
2827            BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(),
2828            _ => None,
2829        };
2830
2831        let archive = ValidatedSourceArchive::extract(
2832            reader.compat(),
2833            source,
2834            ext,
2835            self.build_context.cache(),
2836            ArchiveValidation {
2837                extra_algorithms: &[HashAlgorithm::Sha256],
2838                hash_policy,
2839                existing_hashes,
2840                expected_size,
2841            },
2842        )
2843        .instrument(info_span!("download_source_dist", source_dist = %source))
2844        .await?;
2845        let metadata = archive.persist(target).await?;
2846        Ok((metadata.hashes, metadata.size))
2847    }
2848
2849    /// Extract, validate, and persist a local source archive into the cache.
2850    async fn persist_archive(
2851        &self,
2852        source: &BuildableSource<'_>,
2853        path: &Path,
2854        ext: SourceDistExtension,
2855        target: &Path,
2856        hash_policy: HashPolicy<'_>,
2857        existing_hashes: &[HashDigest],
2858    ) -> Result<Vec<HashDigest>, Error> {
2859        debug!("Unpacking for build: {}", path.display());
2860        let reader = fs_err::tokio::File::open(path)
2861            .await
2862            .map_err(Error::CacheRead)?;
2863        let archive = ValidatedSourceArchive::extract(
2864            reader,
2865            source,
2866            ext,
2867            self.build_context.cache(),
2868            ArchiveValidation {
2869                extra_algorithms: &[],
2870                hash_policy,
2871                existing_hashes,
2872                expected_size: None,
2873            },
2874        )
2875        .await?;
2876        Ok(archive.persist(target).await?.hashes)
2877    }
2878
2879    /// For Git directories, we check them out into the cache, so we need to avoid workspace
2880    /// discovery that goes outside the cache.
2881    fn stop_discovery_at<'path>(
2882        source: &BuildableSource<'_>,
2883        source_root: &'path Path,
2884    ) -> Option<&'path Path> {
2885        if matches!(
2886            source,
2887            BuildableSource::Dist(SourceDist::GitDirectory(_))
2888                | BuildableSource::Url(SourceUrl::GitDirectory(_))
2889        ) {
2890            Some(source_root)
2891        } else {
2892            None
2893        }
2894    }
2895
2896    /// Build a source distribution, storing the built wheel in the cache.
2897    ///
2898    /// Returns the un-normalized disk filename, the parsed, normalized filename and the metadata
2899    #[instrument(skip_all, fields(dist = %source))]
2900    async fn build_distribution(
2901        &self,
2902        source: &BuildableSource<'_>,
2903        source_root: &Path,
2904        subdirectory: Option<&Path>,
2905        cache_shard: &CacheShard,
2906        no_sources: NoSources,
2907    ) -> Result<(String, WheelFilename, ResolutionMetadata), Error> {
2908        debug!("Building: {source}");
2909
2910        // Guard against build of source distributions when disabled.
2911        if self
2912            .build_context
2913            .build_options()
2914            .no_build_requirement(source.name())
2915        {
2916            if source.is_editable() || source.is_first_party() {
2917                debug!("Allowing build for first-party or editable source distribution: {source}");
2918            } else {
2919                return Err(Error::NoBuild);
2920            }
2921        }
2922
2923        // Build into a temporary directory, to prevent partial builds.
2924        let temp_dir = self
2925            .build_context
2926            .cache()
2927            .build_dir()
2928            .map_err(Error::CacheWrite)?;
2929
2930        // Build the wheel.
2931        fs::create_dir_all(&cache_shard)
2932            .await
2933            .map_err(Error::CacheWrite)?;
2934
2935        // Try a direct build if that isn't disabled and the uv build backend is used.
2936        let disk_filename = if let Some(name) = self
2937            .build_context
2938            .direct_build(
2939                source_root,
2940                subdirectory,
2941                temp_dir.path(),
2942                no_sources.clone(),
2943                if source.is_editable() {
2944                    BuildKind::Editable
2945                } else {
2946                    BuildKind::Wheel
2947                },
2948                Some(&source.to_string()),
2949            )
2950            .await
2951            .map_err(|err| Error::Build(err.into()))?
2952        {
2953            // In the uv build backend, the normalized filename and the disk filename are the same.
2954            name.to_string()
2955        } else {
2956            // Identify the base Python interpreter to use in the cache key.
2957            let base_python = if cfg!(unix) {
2958                self.build_context
2959                    .interpreter()
2960                    .await
2961                    .find_base_python()
2962                    .map_err(Error::BaseInterpreter)?
2963            } else {
2964                self.build_context
2965                    .interpreter()
2966                    .await
2967                    .to_base_python()
2968                    .map_err(Error::BaseInterpreter)?
2969            };
2970
2971            let build_kind = if source.is_editable() {
2972                BuildKind::Editable
2973            } else {
2974                BuildKind::Wheel
2975            };
2976
2977            let install_path = if let Some(subdirectory) = subdirectory {
2978                source_root.join(subdirectory)
2979            } else {
2980                source_root.to_path_buf()
2981            };
2982
2983            let stop_discovery_at = Self::stop_discovery_at(source, source_root);
2984
2985            let build_key = BuildKey {
2986                base_python: base_python.into_boxed_path(),
2987                source_root: source_root.to_path_buf().into_boxed_path(),
2988                subdirectory: subdirectory
2989                    .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()),
2990                no_sources: no_sources.clone(),
2991                build_kind,
2992            };
2993
2994            if let Some(builder) = self.build_context.build_arena().remove(&build_key) {
2995                debug!("Reusing existing build environment for: {source}");
2996                let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
2997
2998                // Store the build context.
2999                self.build_context.build_arena().insert(build_key, builder);
3000
3001                wheel
3002            } else {
3003                debug!("Creating build environment for: {source}");
3004
3005                let builder = self
3006                    .build_context
3007                    .setup_build(
3008                        source_root,
3009                        subdirectory,
3010                        &install_path,
3011                        stop_discovery_at,
3012                        Some(&source.to_string()),
3013                        source.as_dist(),
3014                        &no_sources,
3015                        if source.is_editable() {
3016                            BuildKind::Editable
3017                        } else {
3018                            BuildKind::Wheel
3019                        },
3020                        if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
3021                            BuildOutput::Quiet
3022                        } else {
3023                            BuildOutput::Debug
3024                        },
3025                        self.build_stack.cloned().unwrap_or_default(),
3026                    )
3027                    .await
3028                    .map_err(|err| Error::Build(err.into()))?;
3029
3030                // Build the wheel.
3031                let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
3032
3033                // Store the build context.
3034                self.build_context.build_arena().insert(build_key, builder);
3035
3036                wheel
3037            }
3038        };
3039
3040        // Read the metadata from the wheel.
3041        let filename = WheelFilename::from_str(&disk_filename)?;
3042        let metadata = read_wheel_metadata(&filename, &temp_dir.path().join(&disk_filename))?;
3043
3044        // Validate the metadata.
3045        validate_metadata(source, &metadata)?;
3046        validate_filename(&filename, &metadata)?;
3047
3048        // Move the wheel to the cache.
3049        rename_with_retry(
3050            temp_dir.path().join(&disk_filename),
3051            cache_shard.join(&disk_filename),
3052        )
3053        .await
3054        .map_err(Error::CacheWrite)?;
3055
3056        debug!("Built `{source}` into `{disk_filename}`");
3057        Ok((disk_filename, filename, metadata))
3058    }
3059
3060    /// Build the metadata for a source distribution.
3061    #[instrument(skip_all, fields(dist = %source))]
3062    async fn build_metadata(
3063        &self,
3064        source: &BuildableSource<'_>,
3065        source_root: &Path,
3066        subdirectory: Option<&Path>,
3067        no_sources: NoSources,
3068    ) -> Result<Option<ResolutionMetadata>, Error> {
3069        debug!("Preparing metadata for: {source}");
3070
3071        let source_name = source.name();
3072        if self
3073            .build_context
3074            .build_options()
3075            .no_build_requirement(source_name)
3076            // Editable requirements without a known name need metadata to apply
3077            // package-specific build settings; named editables must respect `--no-build`.
3078            && !(source_name.is_none() && source.is_editable())
3079        {
3080            return if let Some(name) = source_name {
3081                Err(Error::NoBuildPackage(name.clone()))
3082            } else {
3083                Err(Error::NoBuild)
3084            };
3085        }
3086
3087        // Ensure that the _installed_ Python version is compatible with the `requires-python`
3088        // specifier.
3089        if let Some(requires_python) = source.requires_python() {
3090            let installed = self.build_context.interpreter().await.python_version();
3091            let target = release_specifiers_to_ranges(requires_python.clone())
3092                .bounding_range()
3093                .map(|bounding_range| bounding_range.0.cloned())
3094                .unwrap_or(Bound::Unbounded);
3095            let is_compatible = match target {
3096                Bound::Included(target) => *installed >= target,
3097                Bound::Excluded(target) => *installed > target,
3098                Bound::Unbounded => true,
3099            };
3100            if !is_compatible {
3101                return Err(Error::RequiresPython(
3102                    requires_python.clone(),
3103                    installed.clone(),
3104                ));
3105            }
3106        }
3107
3108        // Identify the base Python interpreter to use in the cache key.
3109        let base_python = if cfg!(unix) {
3110            self.build_context
3111                .interpreter()
3112                .await
3113                .find_base_python()
3114                .map_err(Error::BaseInterpreter)?
3115        } else {
3116            self.build_context
3117                .interpreter()
3118                .await
3119                .to_base_python()
3120                .map_err(Error::BaseInterpreter)?
3121        };
3122
3123        // Determine whether this is an editable or non-editable build.
3124        let build_kind = if source.is_editable() {
3125            BuildKind::Editable
3126        } else {
3127            BuildKind::Wheel
3128        };
3129
3130        let install_path = if let Some(subdirectory) = subdirectory {
3131            source_root.join(subdirectory)
3132        } else {
3133            source_root.to_path_buf()
3134        };
3135
3136        let stop_discovery_at = Self::stop_discovery_at(source, source_root);
3137
3138        // Set up the builder.
3139        let mut builder = self
3140            .build_context
3141            .setup_build(
3142                source_root,
3143                subdirectory,
3144                &install_path,
3145                stop_discovery_at,
3146                Some(&source.to_string()),
3147                source.as_dist(),
3148                &no_sources,
3149                build_kind,
3150                if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
3151                    BuildOutput::Quiet
3152                } else {
3153                    BuildOutput::Debug
3154                },
3155                self.build_stack.cloned().unwrap_or_default(),
3156            )
3157            .await
3158            .map_err(|err| Error::Build(err.into()))?;
3159
3160        // Build the metadata.
3161        let dist_info = builder.metadata().await.map_err(Error::Build)?;
3162
3163        // Store the build context.
3164        self.build_context.build_arena().insert(
3165            BuildKey {
3166                base_python: base_python.into_boxed_path(),
3167                source_root: source_root.to_path_buf().into_boxed_path(),
3168                subdirectory: subdirectory
3169                    .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()),
3170                no_sources,
3171                build_kind,
3172            },
3173            builder,
3174        );
3175
3176        // Return the `.dist-info` directory, if it exists.
3177        let Some(dist_info) = dist_info else {
3178            return Ok(None);
3179        };
3180
3181        // Read the metadata from disk.
3182        debug!("Prepared metadata for: {source}");
3183        let content = fs::read(dist_info.join("METADATA"))
3184            .await
3185            .map_err(Error::CacheRead)?;
3186        let metadata = ResolutionMetadata::parse_metadata(&content)?;
3187
3188        // Validate the metadata.
3189        validate_metadata(source, &metadata)?;
3190
3191        Ok(Some(metadata))
3192    }
3193
3194    /// Returns a GET [`reqwest::Request`] for the given URL.
3195    fn request(
3196        url: DisplaySafeUrl,
3197        client: &RegistryClient,
3198    ) -> Result<reqwest::Request, reqwest::Error> {
3199        client
3200            .uncached_client(&url)
3201            .get(Url::from(url))
3202            .header(
3203                // `reqwest` defaults to accepting compressed responses.
3204                // Specify identity encoding to get consistent .whl downloading
3205                // behavior from servers. ref: https://github.com/pypa/pip/pull/1688
3206                "accept-encoding",
3207                reqwest::header::HeaderValue::from_static("identity"),
3208            )
3209            .build()
3210    }
3211}
3212
3213/// Prune any unused source distributions from the cache.
3214pub fn prune(cache: &Cache) -> Result<Removal, Error> {
3215    let mut removal = cache.removal();
3216
3217    let bucket = cache.bucket(CacheBucket::SourceDistributions);
3218    if bucket.is_dir() {
3219        for entry in walkdir::WalkDir::new(bucket) {
3220            let entry = entry.map_err(Error::CacheWalk)?;
3221
3222            if !entry.file_type().is_dir() {
3223                continue;
3224            }
3225
3226            // If we find a `revision.http` file, read the pointer, and remove any extraneous
3227            // directories.
3228            let revision = entry.path().join("revision.http");
3229            if revision.is_file() {
3230                if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision) {
3231                    // Remove all sibling directories that are not referenced by the pointer.
3232                    for sibling in entry.path().read_dir().map_err(Error::CacheRead)? {
3233                        let sibling = sibling.map_err(Error::CacheRead)?;
3234                        if sibling.file_type().map_err(Error::CacheRead)?.is_dir() {
3235                            let sibling_name = sibling.file_name();
3236                            if sibling_name != pointer.revision.id().as_str() {
3237                                debug!(
3238                                    "Removing dangling source revision: {}",
3239                                    sibling.path().display()
3240                                );
3241                                removal += cache
3242                                    .remove_path(sibling.path())
3243                                    .map_err(Error::CacheWrite)?;
3244                            }
3245                        }
3246                    }
3247                }
3248            }
3249
3250            // If we find a `revision.rev` file, read the pointer, and remove any extraneous
3251            // directories.
3252            let revision = entry.path().join("revision.rev");
3253            if revision.is_file() {
3254                if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision) {
3255                    // Remove all sibling directories that are not referenced by the pointer.
3256                    for sibling in entry.path().read_dir().map_err(Error::CacheRead)? {
3257                        let sibling = sibling.map_err(Error::CacheRead)?;
3258                        if sibling.file_type().map_err(Error::CacheRead)?.is_dir() {
3259                            let sibling_name = sibling.file_name();
3260                            if sibling_name != pointer.revision.id().as_str() {
3261                                debug!(
3262                                    "Removing dangling source revision: {}",
3263                                    sibling.path().display()
3264                                );
3265                                removal += cache
3266                                    .remove_path(sibling.path())
3267                                    .map_err(Error::CacheWrite)?;
3268                            }
3269                        }
3270                    }
3271                }
3272            }
3273        }
3274    }
3275
3276    Ok(removal)
3277}
3278
3279/// The result of extracting statically available metadata from a source distribution.
3280#[derive(Debug)]
3281enum StaticMetadata {
3282    /// The metadata was found and successfully read.
3283    Some(ResolutionMetadata),
3284    /// The metadata was found, but it was ignored due to a dynamic version.
3285    Dynamic,
3286    /// The metadata was not found.
3287    None,
3288}
3289
3290impl StaticMetadata {
3291    /// Read the [`ResolutionMetadata`] from a source distribution.
3292    async fn read(
3293        source: &BuildableSource<'_>,
3294        source_root: &Path,
3295        subdirectory: Option<&Path>,
3296    ) -> Result<Self, Error> {
3297        // Attempt to read the `pyproject.toml`.
3298        let pyproject_toml = match read_pyproject_toml(source_root, subdirectory).await {
3299            Ok(pyproject_toml) => Some(pyproject_toml),
3300            Err(Error::MissingPyprojectToml) => {
3301                debug!("No `pyproject.toml` available for: {source}");
3302                None
3303            }
3304            Err(err) => return Err(err),
3305        };
3306
3307        // Determine whether the version is static or dynamic.
3308        let dynamic = pyproject_toml.as_ref().is_some_and(|pyproject_toml| {
3309            pyproject_toml.project.as_ref().is_some_and(|project| {
3310                project
3311                    .dynamic
3312                    .as_ref()
3313                    .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
3314            })
3315        });
3316
3317        // Attempt to read static metadata from the `pyproject.toml`.
3318        if let Some(pyproject_toml) = pyproject_toml {
3319            match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) {
3320                Ok(metadata) => {
3321                    debug!("Found static `pyproject.toml` for: {source}");
3322
3323                    // Validate the metadata, but ignore it if the metadata doesn't match.
3324                    match validate_metadata(source, &metadata) {
3325                        Ok(()) => {
3326                            return Ok(Self::Some(metadata));
3327                        }
3328                        Err(err) => {
3329                            debug!("Ignoring `pyproject.toml` for {source}: {err}");
3330                        }
3331                    }
3332                }
3333                Err(
3334                    err @ (uv_pypi_types::MetadataError::Pep508Error(_)
3335                    | uv_pypi_types::MetadataError::DynamicField(_)
3336                    | uv_pypi_types::MetadataError::FieldNotFound(_)
3337                    | uv_pypi_types::MetadataError::PoetrySyntax),
3338                ) => {
3339                    debug!("No static `pyproject.toml` available for: {source} ({err:?})");
3340                }
3341                Err(err) => return Err(Error::PyprojectToml(err)),
3342            }
3343        }
3344
3345        // If the source distribution is a source tree, avoid reading `PKG-INFO`, since it could be
3346        // out-of-date.
3347        if source.is_source_tree() {
3348            return Ok(if dynamic { Self::Dynamic } else { Self::None });
3349        }
3350
3351        // Attempt to read static metadata from the `PKG-INFO` file.
3352        match read_pkg_info(source_root, subdirectory).await {
3353            Ok(metadata) => {
3354                debug!("Found static `PKG-INFO` for: {source}");
3355
3356                // Validate the metadata, but ignore it if the metadata doesn't match.
3357                match validate_metadata(source, &metadata) {
3358                    Ok(()) => {
3359                        // If necessary, mark the metadata as dynamic.
3360                        let metadata = if dynamic {
3361                            ResolutionMetadata {
3362                                dynamic: true,
3363                                ..metadata
3364                            }
3365                        } else {
3366                            metadata
3367                        };
3368                        return Ok(Self::Some(metadata));
3369                    }
3370                    Err(err) => {
3371                        debug!("Ignoring `PKG-INFO` for {source}: {err}");
3372                    }
3373                }
3374            }
3375            Err(
3376                err @ (Error::MissingPkgInfo
3377                | Error::PkgInfo(
3378                    uv_pypi_types::MetadataError::Pep508Error(_)
3379                    | uv_pypi_types::MetadataError::DynamicField(_)
3380                    | uv_pypi_types::MetadataError::FieldNotFound(_)
3381                    | uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
3382                )),
3383            ) => {
3384                debug!("No static `PKG-INFO` available for: {source} ({err:?})");
3385            }
3386            Err(err) => return Err(err),
3387        }
3388
3389        Ok(Self::None)
3390    }
3391}
3392
3393/// Returns `true` if a `pyproject.toml` has `tool.uv.sources`.
3394fn has_sources(content: &str) -> Result<bool, toml::de::Error> {
3395    #[derive(serde::Deserialize)]
3396    struct PyProjectToml {
3397        tool: Option<Tool>,
3398    }
3399
3400    #[derive(serde::Deserialize)]
3401    struct Tool {
3402        uv: Option<ToolUv>,
3403    }
3404
3405    #[derive(serde::Deserialize)]
3406    struct ToolUv {
3407        sources: Option<ToolUvSources>,
3408    }
3409
3410    let pyproject_toml =
3411        info_span!("toml::from_str has sources").in_scope(|| toml::from_str(content))?;
3412    if let PyProjectToml { tool: Some(tool) } = pyproject_toml {
3413        if let Some(uv) = tool.uv {
3414            if let Some(sources) = uv.sources {
3415                if !sources.inner().is_empty() {
3416                    return Ok(true);
3417                }
3418            }
3419        }
3420    }
3421
3422    Ok(false)
3423}
3424
3425/// Validate that the source distribution matches the built metadata.
3426fn validate_metadata(
3427    source: &BuildableSource<'_>,
3428    metadata: &ResolutionMetadata,
3429) -> Result<(), Error> {
3430    if let Some(name) = source.name() {
3431        if metadata.name != *name {
3432            return Err(Error::WheelMetadataNameMismatch {
3433                metadata: metadata.name.clone(),
3434                given: name.clone(),
3435            });
3436        }
3437    }
3438
3439    if let Some(version) = source.version() {
3440        if *version != metadata.version && *version != metadata.version.clone().without_local() {
3441            return Err(Error::WheelMetadataVersionMismatch {
3442                metadata: metadata.version.clone(),
3443                given: version.clone(),
3444            });
3445        }
3446    }
3447
3448    Ok(())
3449}
3450
3451/// Validate that the source distribution matches the built filename.
3452fn validate_filename(filename: &WheelFilename, metadata: &ResolutionMetadata) -> Result<(), Error> {
3453    if metadata.name != filename.name {
3454        return Err(Error::WheelFilenameNameMismatch {
3455            metadata: metadata.name.clone(),
3456            filename: filename.name.clone(),
3457        });
3458    }
3459
3460    if metadata.version != filename.version {
3461        return Err(Error::WheelFilenameVersionMismatch {
3462            metadata: metadata.version.clone(),
3463            filename: filename.version.clone(),
3464        });
3465    }
3466
3467    Ok(())
3468}
3469
3470/// A pointer to a source distribution revision in the cache, fetched from an HTTP archive.
3471///
3472/// Encoded with `MsgPack`, and represented on disk by a `.http` file.
3473#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3474pub(crate) struct HttpRevisionPointer {
3475    revision: Revision,
3476}
3477
3478impl HttpRevisionPointer {
3479    /// Read an [`HttpRevisionPointer`] from the cache.
3480    pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3481        match fs_err::File::open(path.as_ref()) {
3482            Ok(file) => {
3483                let data = DataWithCachePolicy::from_reader(file)?.data;
3484                let revision = rmp_serde::from_slice::<Revision>(&data)?;
3485                Ok(Some(Self { revision }))
3486            }
3487            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3488            Err(err) => Err(Error::CacheRead(err)),
3489        }
3490    }
3491
3492    /// Return the [`Revision`] from the pointer.
3493    pub(crate) fn into_revision(self) -> Revision {
3494        self.revision
3495    }
3496}
3497
3498/// A pointer to a source distribution revision in the cache, fetched from a local path.
3499///
3500/// Encoded with `MsgPack`, and represented on disk by a `.rev` file.
3501#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3502pub(crate) struct LocalRevisionPointer {
3503    cache_info: CacheInfo,
3504    revision: Revision,
3505}
3506
3507impl LocalRevisionPointer {
3508    /// Read an [`LocalRevisionPointer`] from the cache.
3509    pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3510        match fs_err::read(path) {
3511            Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
3512            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3513            Err(err) => Err(Error::CacheRead(err)),
3514        }
3515    }
3516
3517    /// Write an [`LocalRevisionPointer`] to the cache.
3518    async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
3519        fs::create_dir_all(&entry.dir())
3520            .await
3521            .map_err(Error::CacheWrite)?;
3522        write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
3523            .await
3524            .map_err(Error::CacheWrite)
3525    }
3526
3527    /// Return the [`CacheInfo`] for the pointer.
3528    pub(crate) fn cache_info(&self) -> &CacheInfo {
3529        &self.cache_info
3530    }
3531
3532    /// Return the [`Revision`] for the pointer.
3533    fn revision(&self) -> &Revision {
3534        &self.revision
3535    }
3536
3537    /// Return the [`Revision`] for the pointer.
3538    pub(crate) fn into_revision(self) -> Revision {
3539        self.revision
3540    }
3541}
3542
3543/// A pointer to a source distribution revision in the cache, fetched from a local path.
3544///
3545/// Encoded with `MsgPack`, and represented on disk by a `.rev` file.
3546#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3547pub(crate) struct RevisionHashes {
3548    hashes: Vec<HashDigest>,
3549}
3550
3551impl RevisionHashes {
3552    /// Read an [`RevisionHashes`] from the cache.
3553    pub(crate) fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
3554        match fs_err::read(path) {
3555            Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
3556            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3557            Err(err) => Err(Error::CacheRead(err)),
3558        }
3559    }
3560
3561    /// Write an [`LocalRevisionPointer`] to the cache.
3562    async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
3563        fs::create_dir_all(&entry.dir())
3564            .await
3565            .map_err(Error::CacheWrite)?;
3566        write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
3567            .await
3568            .map_err(Error::CacheWrite)
3569    }
3570
3571    /// Return the computed hashes of the archive.
3572    pub(crate) fn into_hashes(self) -> HashDigests {
3573        HashDigests::from(self.hashes)
3574    }
3575}
3576
3577impl Hashed for RevisionHashes {
3578    fn hashes(&self) -> &[HashDigest] {
3579        &self.hashes
3580    }
3581}
3582
3583/// Read the [`ResolutionMetadata`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2
3584/// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and
3585/// `Provides-Extra`) are marked as dynamic.
3586async fn read_pkg_info(
3587    source_tree: &Path,
3588    subdirectory: Option<&Path>,
3589) -> Result<ResolutionMetadata, Error> {
3590    // Read the `PKG-INFO` file.
3591    let pkg_info = match subdirectory {
3592        Some(subdirectory) => source_tree.join(subdirectory).join("PKG-INFO"),
3593        None => source_tree.join("PKG-INFO"),
3594    };
3595    let content = match fs::read(pkg_info).await {
3596        Ok(content) => content,
3597        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3598            return Err(Error::MissingPkgInfo);
3599        }
3600        Err(err) => return Err(Error::CacheRead(err)),
3601    };
3602
3603    // Parse the metadata.
3604    let metadata = ResolutionMetadata::parse_pkg_info(&content).map_err(Error::PkgInfo)?;
3605
3606    Ok(metadata)
3607}
3608
3609/// Read the [`ResolutionMetadata`] from a source distribution's `pyproject.toml` file, if it defines static
3610/// metadata consistent with PEP 621.
3611async fn read_pyproject_toml(
3612    source_tree: &Path,
3613    subdirectory: Option<&Path>,
3614) -> Result<PyProjectToml, Error> {
3615    // Read the `pyproject.toml` file.
3616    let pyproject_toml = match subdirectory {
3617        Some(subdirectory) => source_tree.join(subdirectory).join("pyproject.toml"),
3618        None => source_tree.join("pyproject.toml"),
3619    };
3620    let content = match fs::read_to_string(&pyproject_toml).await {
3621        Ok(content) => content,
3622        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3623            return Err(Error::MissingPyprojectToml);
3624        }
3625        Err(err) => return Err(Error::CacheRead(err)),
3626    };
3627
3628    let pyproject_toml = PyProjectToml::from_toml(&content, pyproject_toml.simplified_display())?;
3629
3630    Ok(pyproject_toml)
3631}
3632
3633/// Wheel metadata stored in the source distribution cache.
3634#[derive(Debug, Clone)]
3635struct CachedMetadata(ResolutionMetadata);
3636
3637impl CachedMetadata {
3638    /// Read an existing cached [`ResolutionMetadata`], if it exists.
3639    async fn read(cache_entry: &CacheEntry) -> Result<Option<Self>, Error> {
3640        match fs::read(&cache_entry.path()).await {
3641            Ok(cached) => Ok(Some(Self(rmp_serde::from_slice(&cached)?))),
3642            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
3643            Err(err) => Err(Error::CacheRead(err)),
3644        }
3645    }
3646
3647    /// Returns `true` if the metadata matches the given package name and version.
3648    fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool {
3649        name.is_none_or(|name| self.0.name == *name)
3650            && version.is_none_or(|version| self.0.version == *version)
3651    }
3652}
3653
3654impl From<CachedMetadata> for ResolutionMetadata {
3655    fn from(value: CachedMetadata) -> Self {
3656        value.0
3657    }
3658}
3659
3660/// Read the [`ResolutionMetadata`] from a built wheel.
3661fn read_wheel_metadata(
3662    filename: &WheelFilename,
3663    wheel: &Path,
3664) -> Result<ResolutionMetadata, Error> {
3665    let file = fs_err::File::open(wheel).map_err(Error::CacheRead)?;
3666    let reader = std::io::BufReader::new(file);
3667    let dist_info = read_archive_metadata(filename, reader)
3668        .map_err(|err| Error::WheelMetadata(wheel.to_path_buf(), Box::new(err)))?;
3669    Ok(ResolutionMetadata::parse_metadata(&dist_info)?)
3670}