1use std::cmp::Reverse;
2use std::future::Future;
3use std::io;
4use std::path::Path;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context, Poll};
8
9use futures::{FutureExt, TryStreamExt};
10use rayon::in_place_scope;
11use rayon::prelude::*;
12use rustc_hash::FxHashMap;
13use tokio::io::{AsyncRead, AsyncSeekExt, ReadBuf};
14use tokio::sync::Semaphore;
15use tokio_util::compat::FuturesAsyncReadCompatExt;
16use tracing::{Instrument, info_span, instrument, warn};
17use url::Url;
18
19use uv_cache::{ArchiveFileId, ArchiveId, Cache, CacheBucket, CacheEntry, WheelCache};
20use uv_cache_info::{CacheInfo, Timestamp};
21use uv_client::{
22 CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
23};
24use uv_configuration::initialize_rayon_once;
25use uv_distribution_filename::WheelFilename;
26use uv_distribution_types::{
27 BuildInfo, BuildableSource, BuiltDist, Dist, DistRef, HashPolicy, Hashed, IndexUrl,
28 InstalledDist, Name, SourceDist,
29};
30use uv_extract::dirhash::{DirectoryDigest, HashedFile};
31use uv_extract::hash::Hasher;
32use uv_fs::{LockedFile, write_atomic};
33use uv_git::{GIT_LFS, GitError};
34use uv_platform_tags::Tags;
35use uv_preview::PreviewFeature;
36use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml};
37use uv_python::PythonVariant;
38use uv_redacted::DisplaySafeUrl;
39use uv_types::{BuildContext, BuildStack};
40
41use crate::archive::Archive;
42use crate::error::PythonVersion;
43use crate::extracted_wheel::{ExtractedWheel, HashedWheel, WheelExtractor};
44use crate::hash::http_hash_algorithms;
45use crate::metadata::{ArchiveMetadata, Metadata};
46use crate::source::SourceDistributionBuilder;
47use crate::{Error, LocalWheel, Reporter, RequiresDist};
48
49pub struct DistributionDatabase<'a, Context: BuildContext> {
62 build_context: &'a Context,
63 builder: SourceDistributionBuilder<'a, Context>,
64 client: ManagedClient<'a>,
65 reporter: Option<Arc<dyn Reporter>>,
66 content_addressed_cache: bool,
67}
68
69impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
70 pub fn new(
71 client: &'a RegistryClient,
72 build_context: &'a Context,
73 downloads_semaphore: Arc<Semaphore>,
74 ) -> Self {
75 let content_addressed_cache = uv_preview::is_enabled(PreviewFeature::ContentAddressedCache)
79 && !uv_extract::insecure_no_validate();
80 Self {
81 build_context,
82 builder: SourceDistributionBuilder::new(build_context),
83 client: ManagedClient::new(client, downloads_semaphore),
84 reporter: None,
85 content_addressed_cache,
86 }
87 }
88
89 #[must_use]
91 pub fn with_build_stack(self, build_stack: &'a BuildStack) -> Self {
92 Self {
93 builder: self.builder.with_build_stack(build_stack),
94 ..self
95 }
96 }
97
98 #[must_use]
100 pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
101 Self {
102 builder: self.builder.with_reporter(reporter.clone()),
103 reporter: Some(reporter),
104 ..self
105 }
106 }
107
108 fn handle_response_errors(&self, err: reqwest::Error) -> io::Error {
110 if err.is_timeout() {
111 io::Error::new(
113 io::ErrorKind::TimedOut,
114 format!(
115 "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).",
116 self.client.unmanaged.read_timeout().as_secs()
117 ),
118 )
119 } else {
120 io::Error::other(err)
121 }
122 }
123
124 async fn lock_wheel(
133 wheel_entry: &CacheEntry,
134 filename: &WheelFilename,
135 ) -> Result<LockedFile, Error> {
136 #[cfg(windows)]
140 let lock_key = filename.stem();
141 #[cfg(not(windows))]
143 let lock_key = filename.cache_key();
144
145 let lock_entry = wheel_entry.with_file(format!("{lock_key}.lock"));
146 lock_entry.lock().await.map_err(Error::CacheLock)
147 }
148
149 #[instrument(skip_all, fields(%dist))]
156 pub async fn get_or_build_wheel(
157 &self,
158 dist: &Dist,
159 tags: &Tags,
160 hashes: HashPolicy<'_>,
161 ) -> Result<LocalWheel, Error> {
162 match dist {
163 Dist::Built(built) => self.get_wheel(built, hashes).await,
164 Dist::Source(source) => self.build_wheel(source, tags, hashes).await,
165 }
166 }
167
168 #[instrument(skip_all, fields(%dist))]
174 pub async fn get_installed_metadata(
175 &self,
176 dist: &InstalledDist,
177 ) -> Result<ArchiveMetadata, Error> {
178 if let Some(metadata) = self
180 .build_context
181 .dependency_metadata()
182 .get(dist.name(), Some(dist.version()))
183 {
184 return Ok(ArchiveMetadata::from_metadata23(metadata));
185 }
186
187 let metadata = dist
188 .read_metadata()
189 .map_err(|err| Error::ReadInstalled(Box::new(dist.clone()), err))?;
190
191 Ok(ArchiveMetadata::from_metadata23(metadata.clone()))
192 }
193
194 #[instrument(skip_all, fields(%dist))]
200 pub async fn get_or_build_wheel_metadata(
201 &self,
202 dist: &Dist,
203 hashes: HashPolicy<'_>,
204 ) -> Result<ArchiveMetadata, Error> {
205 match dist {
206 Dist::Built(built) => self.get_wheel_metadata(built, hashes).await,
207 Dist::Source(source) => {
208 self.build_wheel_metadata(&BuildableSource::Dist(source), hashes)
209 .await
210 }
211 }
212 }
213
214 async fn get_wheel(
219 &self,
220 dist: &BuiltDist,
221 hashes: HashPolicy<'_>,
222 ) -> Result<LocalWheel, Error> {
223 match dist {
224 BuiltDist::Registry(wheels) => {
225 let wheel = wheels.best_wheel();
226 let url = wheel.file.url.to_url()?;
227 let size = wheel.file.size;
228
229 let wheel_entry = self.build_context.cache().entry(
231 CacheBucket::Wheels,
232 WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()),
233 wheel.filename.cache_key(),
234 );
235
236 if url.scheme() == "file" {
238 let path = url
239 .to_file_path()
240 .map_err(|()| Error::NonFileUrl(url.clone()))?;
241 return self
242 .load_wheel(&path, &wheel.filename, wheel_entry, dist, hashes)
243 .await;
244 }
245
246 match self
248 .stream_wheel(
249 url.clone(),
250 dist.index(),
251 &wheel.filename,
252 size,
253 &wheel_entry,
254 dist,
255 hashes,
256 )
257 .await
258 {
259 Ok(archive) => Ok(LocalWheel {
260 dist: Dist::Built(dist.clone()),
261 archive: self
262 .build_context
263 .cache()
264 .archive(&archive.id)
265 .into_boxed_path(),
266 hashes: archive.hashes,
267 filename: wheel.filename.clone(),
268 cache: CacheInfo::default(),
269 build: None,
270 }),
271 Err(Error::Extract(name, err)) => {
272 if err.is_http_streaming_unsupported() {
273 warn!(
274 "Streaming unsupported for {dist}; downloading wheel to disk ({err})"
275 );
276 } else if err.is_http_streaming_failed() {
277 warn!("Streaming failed for {dist}; downloading wheel to disk ({err})");
278 } else {
279 return Err(Error::Extract(name, err));
280 }
281
282 let archive = self
285 .download_wheel(
286 url,
287 dist.index(),
288 &wheel.filename,
289 size,
290 &wheel_entry,
291 dist,
292 hashes,
293 )
294 .await?;
295
296 Ok(LocalWheel {
297 dist: Dist::Built(dist.clone()),
298 archive: self
299 .build_context
300 .cache()
301 .archive(&archive.id)
302 .into_boxed_path(),
303 hashes: archive.hashes,
304 filename: wheel.filename.clone(),
305 cache: CacheInfo::default(),
306 build: None,
307 })
308 }
309 Err(err) => Err(err),
310 }
311 }
312
313 BuiltDist::DirectUrl(wheel) => {
314 let wheel_entry = self.build_context.cache().entry(
316 CacheBucket::Wheels,
317 WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
318 wheel.filename.cache_key(),
319 );
320
321 match self
323 .stream_wheel(
324 wheel.url.raw().clone(),
325 None,
326 &wheel.filename,
327 wheel.size,
328 &wheel_entry,
329 dist,
330 hashes,
331 )
332 .await
333 {
334 Ok(archive) => Ok(LocalWheel {
335 dist: Dist::Built(dist.clone()),
336 archive: self
337 .build_context
338 .cache()
339 .archive(&archive.id)
340 .into_boxed_path(),
341 hashes: archive.hashes,
342 filename: wheel.filename.clone(),
343 cache: CacheInfo::default(),
344 build: None,
345 }),
346 Err(Error::Extract(name, err)) => {
347 if err.is_http_streaming_unsupported() {
348 warn!(
349 "Streaming unsupported for {dist}; downloading wheel to disk ({err})"
350 );
351 } else if err.is_http_streaming_failed() {
352 warn!("Streaming failed for {dist}; downloading wheel to disk ({err})");
353 } else {
354 return Err(Error::Extract(name, err));
355 }
356
357 let archive = self
360 .download_wheel(
361 wheel.url.raw().clone(),
362 None,
363 &wheel.filename,
364 wheel.size,
365 &wheel_entry,
366 dist,
367 hashes,
368 )
369 .await?;
370 Ok(LocalWheel {
371 dist: Dist::Built(dist.clone()),
372 archive: self
373 .build_context
374 .cache()
375 .archive(&archive.id)
376 .into_boxed_path(),
377 hashes: archive.hashes,
378 filename: wheel.filename.clone(),
379 cache: CacheInfo::default(),
380 build: None,
381 })
382 }
383 Err(err) => Err(err),
384 }
385 }
386
387 BuiltDist::GitPath(wheel) => {
388 let fetch = self
390 .build_context
391 .git()
392 .fetch(
393 &wheel.git,
394 self.client.unmanaged.git_http_settings(wheel.git.url()),
395 self.build_context.cache().bucket(CacheBucket::Git),
396 self.reporter.clone().map(<dyn Reporter>::into_git_reporter),
397 )
398 .await?;
399
400 if wheel.git.lfs().enabled() && !fetch.lfs_ready() {
401 if GIT_LFS.is_err() {
402 return Err(Error::MissingWheelGitLfsArtifacts(
403 wheel.url.to_url(),
404 GitError::GitLfsNotFound,
405 ));
406 }
407 return Err(Error::MissingWheelGitLfsArtifacts(
408 wheel.url.to_url(),
409 GitError::GitLfsNotConfigured,
410 ));
411 }
412
413 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
414 let cache_entry = self.build_context.cache().entry(
415 CacheBucket::Wheels,
416 WheelCache::Git(&wheel.url, git_sha.as_short_str()).root(),
417 wheel.filename.stem(),
418 );
419
420 let install_path = fetch.path().join(&wheel.install_path);
421
422 self.load_wheel(&install_path, &wheel.filename, cache_entry, dist, hashes)
423 .await
424 }
425
426 BuiltDist::Path(wheel) => {
427 let cache_entry = self.build_context.cache().entry(
428 CacheBucket::Wheels,
429 WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
430 wheel.filename.cache_key(),
431 );
432
433 self.load_wheel(
434 &wheel.install_path,
435 &wheel.filename,
436 cache_entry,
437 dist,
438 hashes,
439 )
440 .await
441 }
442 }
443 }
444
445 async fn build_wheel(
451 &self,
452 dist: &SourceDist,
453 tags: &Tags,
454 hashes: HashPolicy<'_>,
455 ) -> Result<LocalWheel, Error> {
456 let built_wheel = self
457 .builder
458 .download_and_build(&BuildableSource::Dist(dist), tags, hashes, &self.client)
459 .boxed_local()
460 .await?;
461
462 if !built_wheel.filename.is_compatible(tags) {
468 return if tags.is_cross() {
469 Err(Error::BuiltWheelIncompatibleTargetPlatform {
470 filename: built_wheel.filename,
471 python_platform: tags.python_platform().clone(),
472 python_version: PythonVersion {
473 version: tags.python_version(),
474 variant: if tags.is_freethreaded() {
475 PythonVariant::Freethreaded
476 } else {
477 PythonVariant::Default
478 },
479 },
480 })
481 } else {
482 Err(Error::BuiltWheelIncompatibleHostPlatform {
483 filename: built_wheel.filename,
484 python_platform: tags.python_platform().clone(),
485 python_version: PythonVersion {
486 version: tags.python_version(),
487 variant: if tags.is_freethreaded() {
488 PythonVariant::Freethreaded
489 } else {
490 PythonVariant::Default
491 },
492 },
493 })
494 };
495 }
496
497 let wheel_entry = CacheEntry::from_path(built_wheel.target.as_ref());
499 let _lock = Self::lock_wheel(&wheel_entry, &built_wheel.filename).await?;
500
501 match self.build_context.cache().resolve_link(&built_wheel.target) {
504 Ok(archive) => {
505 return Ok(LocalWheel {
506 dist: Dist::Source(dist.clone()),
507 archive: archive.into_boxed_path(),
508 filename: built_wheel.filename,
509 hashes: built_wheel.hashes,
510 cache: built_wheel.cache_info,
511 build: Some(built_wheel.build_info),
512 });
513 }
514 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
515 Err(err) => return Err(Error::CacheRead(err)),
516 }
517
518 let id = self
520 .unzip_wheel(
521 &built_wheel.path,
522 &built_wheel.target,
523 DistRef::Source(dist),
524 )
525 .await?;
526
527 Ok(LocalWheel {
528 dist: Dist::Source(dist.clone()),
529 archive: self.build_context.cache().archive(&id).into_boxed_path(),
530 hashes: built_wheel.hashes,
531 filename: built_wheel.filename,
532 cache: built_wheel.cache_info,
533 build: Some(built_wheel.build_info),
534 })
535 }
536
537 async fn get_wheel_metadata(
542 &self,
543 dist: &BuiltDist,
544 hashes: HashPolicy<'_>,
545 ) -> Result<ArchiveMetadata, Error> {
546 if hashes.is_generate(dist) {
561 let wheel = self.get_wheel(dist, hashes).await?;
562 let metadata = if let Some(metadata) = self
564 .build_context
565 .dependency_metadata()
566 .get(dist.name(), Some(dist.version()))
567 {
568 metadata
569 } else {
570 wheel.metadata()?
571 };
572 let hashes = wheel.hashes;
573 return Ok(ArchiveMetadata {
574 metadata: Metadata::from_metadata23(metadata),
575 hashes,
576 });
577 }
578
579 if let Some(metadata) = self
581 .build_context
582 .dependency_metadata()
583 .get(dist.name(), Some(dist.version()))
584 {
585 return Ok(ArchiveMetadata::from_metadata23(metadata));
586 }
587
588 let result = self
589 .client
590 .managed(|client| {
591 client
592 .wheel_metadata(
593 dist,
594 self.build_context.git(),
595 self.build_context.capabilities(),
596 self.reporter.clone().map(<dyn Reporter>::into_git_reporter),
597 )
598 .boxed_local()
599 })
600 .await;
601
602 match result {
603 Ok(metadata) => {
604 Ok(ArchiveMetadata::from_metadata23(metadata))
606 }
607 Err(err) if err.is_http_streaming_unsupported() => {
608 warn!(
609 "Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})"
610 );
611
612 let wheel = self.get_wheel(dist, hashes).await?;
615 let metadata = wheel.metadata()?;
616 let hashes = wheel.hashes;
617 Ok(ArchiveMetadata {
618 metadata: Metadata::from_metadata23(metadata),
619 hashes,
620 })
621 }
622 Err(err) => Err(err.into()),
623 }
624 }
625
626 pub async fn build_wheel_metadata(
631 &self,
632 source: &BuildableSource<'_>,
633 hashes: HashPolicy<'_>,
634 ) -> Result<ArchiveMetadata, Error> {
635 if let Some(dist) = source.as_dist() {
637 if let Some(metadata) = self
638 .build_context
639 .dependency_metadata()
640 .get(dist.name(), dist.version())
641 {
642 self.builder.resolve_revision(source, &self.client).await?;
645
646 return Ok(ArchiveMetadata::from_metadata23(metadata));
647 }
648 }
649
650 let metadata = self
651 .builder
652 .download_and_build_metadata(source, hashes, &self.client)
653 .boxed_local()
654 .await?;
655
656 Ok(metadata)
657 }
658
659 pub async fn requires_dist(
661 &self,
662 path: &Path,
663 pyproject_toml: &PyProjectToml,
664 ) -> Result<Option<RequiresDist>, Error> {
665 self.builder
666 .source_tree_requires_dist(
667 path,
668 pyproject_toml,
669 self.client.unmanaged.credentials_cache(),
670 )
671 .await
672 }
673
674 async fn stream_wheel(
676 &self,
677 url: DisplaySafeUrl,
678 index: Option<&IndexUrl>,
679 filename: &WheelFilename,
680 size: Option<u64>,
681 wheel_entry: &CacheEntry,
682 dist: &BuiltDist,
683 hashes: HashPolicy<'_>,
684 ) -> Result<Archive, Error> {
685 let expected_size = match dist {
686 BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => size,
687 BuiltDist::DirectUrl(_) => size,
688 _ => None,
689 };
690
691 let _lock = Self::lock_wheel(wheel_entry, filename).await?;
693
694 let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key()));
696
697 let download = |response: reqwest::Response| {
698 async {
699 let progress_size = size.or_else(|| content_length(&response));
700
701 let progress = self.reporter.as_ref().map(|reporter| {
702 (
703 reporter,
704 reporter.on_download_start(dist.name(), progress_size),
705 )
706 });
707
708 let reader = response
709 .bytes_stream()
710 .map_err(|err| self.handle_response_errors(err))
711 .into_async_read();
712
713 let algorithms = http_hash_algorithms(hashes);
715 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
716 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
717
718 let extractor = WheelExtractor::new(
720 self.build_context.cache().root(),
721 self.content_addressed_cache,
722 )
723 .map_err(Error::CacheWrite)?;
724
725 let mut extracted = match progress {
726 Some((reporter, progress)) => {
727 let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter);
728 extractor
729 .extract_streaming(&mut reader)
730 .await
731 .map_err(|err| Error::Extract(filename.to_string(), err))?
732 }
733 None => extractor
734 .extract_streaming(&mut hasher)
735 .await
736 .map_err(|err| Error::Extract(filename.to_string(), err))?,
737 };
738 hasher.finish().await.map_err(Error::HashExhaustion)?;
740 let actual_size = hasher.bytes_read();
741 if let Some(expected) = expected_size
742 && actual_size != expected
743 {
744 return Err(Error::MismatchedSize {
745 distribution: dist.to_string(),
746 expected,
747 actual: actual_size,
748 });
749 }
750
751 extracted.validate_and_heal_record(dist)?;
754
755 let id = self
757 .persist_extracted_wheel(extracted, wheel_entry.path())
758 .await?;
759
760 if let Some((reporter, progress)) = progress {
761 reporter.on_download_complete(dist.name(), progress);
762 }
763
764 Ok(Archive::new(
765 id,
766 hashers.into_iter().map(HashDigest::from).collect(),
767 filename.clone(),
768 Some(actual_size),
769 ))
770 }
771 .instrument(info_span!("wheel", wheel = %dist))
772 };
773
774 let req = self.request(url.clone())?;
776
777 let cache_control = match self.client.unmanaged.connectivity() {
779 Connectivity::Online
780 if let Some(header) = index.and_then(|index| {
781 self.build_context
782 .locations()
783 .artifact_cache_control_for(index)
784 }) =>
785 {
786 CacheControl::Override(header)
787 }
788 Connectivity::Online => CacheControl::from(
789 self.build_context
790 .cache()
791 .freshness(&http_entry, Some(&filename.name), None)
792 .map_err(Error::CacheRead)?,
793 ),
794 Connectivity::Offline => CacheControl::AllowStale,
795 };
796
797 let archive = self
798 .client
799 .managed(|client| {
800 client.cached_client().get_serde_with_retry(
801 req,
802 &http_entry,
803 cache_control.clone(),
804 download,
805 )
806 })
807 .await
808 .map_err(|err| match err {
809 CachedClientError::Callback { err, .. } => err,
810 CachedClientError::Client(err) => Error::Client(err),
811 })?;
812
813 if let (Some(expected), Some(actual)) = (expected_size, archive.size)
814 && expected != actual
815 {
816 return Err(Error::MismatchedSize {
817 distribution: dist.to_string(),
818 expected,
819 actual,
820 });
821 }
822
823 let archive = Some(archive)
825 .filter(|archive| archive.has_digests(hashes))
826 .filter(|archive| archive.exists(self.build_context.cache()))
827 .filter(|archive| expected_size.is_none() || archive.size.is_some());
828
829 let archive = if let Some(archive) = archive {
830 archive
831 } else {
832 self.client
833 .managed(async |client| {
834 client
835 .cached_client()
836 .skip_cache_with_retry(
837 self.request(url)?,
838 &http_entry,
839 cache_control,
840 download,
841 )
842 .await
843 .map_err(|err| match err {
844 CachedClientError::Callback { err, .. } => err,
845 CachedClientError::Client(err) => Error::Client(err),
846 })
847 })
848 .await?
849 };
850
851 Ok(archive)
852 }
853
854 async fn download_wheel(
856 &self,
857 url: DisplaySafeUrl,
858 index: Option<&IndexUrl>,
859 filename: &WheelFilename,
860 size: Option<u64>,
861 wheel_entry: &CacheEntry,
862 dist: &BuiltDist,
863 hashes: HashPolicy<'_>,
864 ) -> Result<Archive, Error> {
865 let expected_size = match dist {
866 BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => size,
867 BuiltDist::DirectUrl(_) => size,
868 _ => None,
869 };
870
871 let content_addressed_cache = self.content_addressed_cache;
872
873 let _lock = Self::lock_wheel(wheel_entry, filename).await?;
875
876 let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key()));
878
879 let download = |response: reqwest::Response| {
880 async {
881 let progress_size = size.or_else(|| content_length(&response));
882
883 let progress = self.reporter.as_ref().map(|reporter| {
884 (
885 reporter,
886 reporter.on_download_start(dist.name(), progress_size),
887 )
888 });
889
890 let reader = response
891 .bytes_stream()
892 .map_err(|err| self.handle_response_errors(err))
893 .into_async_read();
894 let algorithms = http_hash_algorithms(hashes);
895 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
896 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
897
898 let temp_file = tempfile::tempfile_in(self.build_context.cache().root())
900 .map_err(Error::CacheWrite)?;
901 let mut writer = tokio::io::BufWriter::new(fs_err::tokio::File::from_std(
902 fs_err::File::from_parts(temp_file, self.build_context.cache().root()),
904 ));
905
906 match progress {
907 Some((reporter, progress)) => {
908 let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter);
911
912 tokio::io::copy(&mut reader, &mut writer)
913 .await
914 .map_err(Error::CacheWrite)?;
915 }
916 None => {
917 tokio::io::copy(&mut hasher, &mut writer)
918 .await
919 .map_err(Error::CacheWrite)?;
920 }
921 }
922
923 if let Some(expected) = expected_size
924 && hasher.bytes_read() != expected
925 {
926 return Err(Error::MismatchedSize {
927 distribution: dist.to_string(),
928 expected,
929 actual: hasher.bytes_read(),
930 });
931 }
932
933 let actual_size = hasher.bytes_read();
934
935 let extractor =
937 WheelExtractor::new(self.build_context.cache().root(), content_addressed_cache)
938 .map_err(Error::CacheWrite)?;
939 let mut file = writer.into_inner();
940 file.seek(io::SeekFrom::Start(0))
941 .await
942 .map_err(Error::CacheWrite)?;
943
944 let file = file.into_std().await;
945 let mut extracted =
946 tokio::task::spawn_blocking(move || extractor.extract_seekable(file))
947 .await?
948 .map_err(|err| Error::Extract(filename.to_string(), err))?;
949 let hashes = hashers.into_iter().map(HashDigest::from).collect();
950
951 extracted.validate_and_heal_record(dist)?;
954
955 let id = self
957 .persist_extracted_wheel(extracted, wheel_entry.path())
958 .await?;
959
960 if let Some((reporter, progress)) = progress {
961 reporter.on_download_complete(dist.name(), progress);
962 }
963
964 Ok(Archive::new(
965 id,
966 hashes,
967 filename.clone(),
968 Some(actual_size),
969 ))
970 }
971 .instrument(info_span!("wheel", wheel = %dist))
972 };
973
974 let req = self.request(url.clone())?;
976
977 let cache_control = match self.client.unmanaged.connectivity() {
979 Connectivity::Online
980 if let Some(header) = index.and_then(|index| {
981 self.build_context
982 .locations()
983 .artifact_cache_control_for(index)
984 }) =>
985 {
986 CacheControl::Override(header)
987 }
988 Connectivity::Online => CacheControl::from(
989 self.build_context
990 .cache()
991 .freshness(&http_entry, Some(&filename.name), None)
992 .map_err(Error::CacheRead)?,
993 ),
994 Connectivity::Offline => CacheControl::AllowStale,
995 };
996
997 let archive = self
998 .client
999 .managed(|client| {
1000 client.cached_client().get_serde_with_retry(
1001 req,
1002 &http_entry,
1003 cache_control.clone(),
1004 download,
1005 )
1006 })
1007 .await
1008 .map_err(|err| match err {
1009 CachedClientError::Callback { err, .. } => err,
1010 CachedClientError::Client(err) => Error::Client(err),
1011 })?;
1012
1013 if let (Some(expected), Some(actual)) = (expected_size, archive.size)
1014 && expected != actual
1015 {
1016 return Err(Error::MismatchedSize {
1017 distribution: dist.to_string(),
1018 expected,
1019 actual,
1020 });
1021 }
1022
1023 let archive = Some(archive)
1025 .filter(|archive| archive.has_digests(hashes))
1026 .filter(|archive| archive.exists(self.build_context.cache()))
1027 .filter(|archive| expected_size.is_none() || archive.size.is_some());
1028
1029 let archive = if let Some(archive) = archive {
1030 archive
1031 } else {
1032 self.client
1033 .managed(async |client| {
1034 client
1035 .cached_client()
1036 .skip_cache_with_retry(
1037 self.request(url)?,
1038 &http_entry,
1039 cache_control,
1040 download,
1041 )
1042 .await
1043 .map_err(|err| match err {
1044 CachedClientError::Callback { err, .. } => err,
1045 CachedClientError::Client(err) => Error::Client(err),
1046 })
1047 })
1048 .await?
1049 };
1050
1051 Ok(archive)
1052 }
1053
1054 async fn load_wheel(
1056 &self,
1057 path: &Path,
1058 filename: &WheelFilename,
1059 wheel_entry: CacheEntry,
1060 dist: &BuiltDist,
1061 hashes: HashPolicy<'_>,
1062 ) -> Result<LocalWheel, Error> {
1063 let _lock = Self::lock_wheel(&wheel_entry, filename).await?;
1065
1066 let modified = Timestamp::from_path(path).map_err(Error::CacheRead)?;
1068
1069 let pointer_entry = wheel_entry.with_file(format!("{}.rev", filename.cache_key()));
1071 let pointer = PathArchivePointer::read_from(&pointer_entry)?;
1072
1073 let archive = pointer
1075 .filter(|pointer| pointer.is_up_to_date(modified))
1076 .map(PathArchivePointer::into_archive)
1077 .filter(|archive| archive.has_digests(hashes));
1078
1079 if let Some(archive) = archive {
1081 Ok(LocalWheel {
1082 dist: Dist::Built(dist.clone()),
1083 archive: self
1084 .build_context
1085 .cache()
1086 .archive(&archive.id)
1087 .into_boxed_path(),
1088 hashes: archive.hashes,
1089 filename: filename.clone(),
1090 cache: CacheInfo::from_timestamp(modified),
1091 build: None,
1092 })
1093 } else if hashes.is_none() {
1094 let archive = Archive::new(
1096 self.unzip_wheel(path, wheel_entry.path(), DistRef::Built(dist))
1097 .await?,
1098 HashDigests::empty(),
1099 filename.clone(),
1100 None,
1101 );
1102
1103 let pointer = PathArchivePointer {
1105 timestamp: modified,
1106 archive: archive.clone(),
1107 };
1108 pointer.write_to(&pointer_entry).await?;
1109
1110 Ok(LocalWheel {
1111 dist: Dist::Built(dist.clone()),
1112 archive: self
1113 .build_context
1114 .cache()
1115 .archive(&archive.id)
1116 .into_boxed_path(),
1117 hashes: archive.hashes,
1118 filename: filename.clone(),
1119 cache: CacheInfo::from_timestamp(modified),
1120 build: None,
1121 })
1122 } else {
1123 let file = fs_err::tokio::File::open(path)
1125 .await
1126 .map_err(Error::CacheRead)?;
1127 let extractor = WheelExtractor::new(
1128 self.build_context.cache().root(),
1129 self.content_addressed_cache,
1130 )
1131 .map_err(Error::CacheWrite)?;
1132
1133 let algorithms = hashes.algorithms();
1135 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
1136 let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers);
1137
1138 let mut extracted = extractor
1140 .extract_streaming(&mut hasher)
1141 .await
1142 .map_err(|err| Error::Extract(filename.to_string(), err))?;
1143
1144 hasher.finish().await.map_err(Error::HashExhaustion)?;
1146
1147 let hashes = hashers.into_iter().map(HashDigest::from).collect();
1148
1149 extracted.validate_and_heal_record(dist)?;
1152
1153 let id = self
1155 .persist_extracted_wheel(extracted, wheel_entry.path())
1156 .await?;
1157
1158 let archive = Archive::new(id, hashes, filename.clone(), None);
1160
1161 let pointer = PathArchivePointer {
1163 timestamp: modified,
1164 archive: archive.clone(),
1165 };
1166 pointer.write_to(&pointer_entry).await?;
1167
1168 Ok(LocalWheel {
1169 dist: Dist::Built(dist.clone()),
1170 archive: self
1171 .build_context
1172 .cache()
1173 .archive(&archive.id)
1174 .into_boxed_path(),
1175 hashes: archive.hashes,
1176 filename: filename.clone(),
1177 cache: CacheInfo::from_timestamp(modified),
1178 build: None,
1179 })
1180 }
1181 }
1182
1183 async fn unzip_wheel(
1185 &self,
1186 path: &Path,
1187 target: &Path,
1188 dist: DistRef<'_>,
1189 ) -> Result<ArchiveId, Error> {
1190 let content_addressed_cache = self.content_addressed_cache;
1191
1192 let mut extracted = tokio::task::spawn_blocking({
1193 let path = path.to_owned();
1194 let root = self.build_context.cache().root().to_path_buf();
1195 move || -> Result<_, Error> {
1196 let extractor = WheelExtractor::new(&root, content_addressed_cache)
1198 .map_err(Error::CacheWrite)?;
1199 let reader = fs_err::File::open(&path).map_err(Error::CacheWrite)?;
1200 extractor
1201 .extract_seekable(reader)
1202 .map_err(|err| Error::Extract(path.to_string_lossy().into_owned(), err))
1203 }
1204 })
1205 .await??;
1206
1207 extracted.validate_and_heal_record(dist)?;
1209
1210 let id = self.persist_extracted_wheel(extracted, target).await?;
1212
1213 Ok(id)
1214 }
1215
1216 async fn persist_extracted_wheel(
1221 &self,
1222 extracted: ExtractedWheel,
1223 target: &Path,
1224 ) -> Result<ArchiveId, Error> {
1225 let (temp_dir, hashed_wheel) = extracted.into_parts();
1226 let cache = self.build_context.cache();
1227 let (temp_dir, id) = if let Some(HashedWheel { files, tree }) = hashed_wheel {
1228 let digest = DirectoryDigest::from(tree.hash());
1229 let id = ArchiveId::from_digest(digest.into());
1230 let cache = cache.clone();
1231 let temp_dir = tokio::task::spawn_blocking(move || {
1232 persist_archive_files(&cache, temp_dir.path(), &files)
1233 .map_err(Error::CacheWrite)?;
1234 Ok::<_, Error>(temp_dir)
1235 })
1236 .await??;
1237 (temp_dir, id)
1238 } else {
1239 (temp_dir, ArchiveId::default())
1240 };
1241
1242 cache
1243 .persist_with_id(temp_dir, target, id)
1244 .await
1245 .map_err(Error::CacheWrite)
1246 }
1247
1248 fn request(&self, url: DisplaySafeUrl) -> Result<reqwest::Request, reqwest::Error> {
1250 self.client
1251 .unmanaged
1252 .uncached_client(&url)
1253 .get(Url::from(url))
1254 .header(
1255 "accept-encoding",
1259 reqwest::header::HeaderValue::from_static("identity"),
1260 )
1261 .build()
1262 }
1263
1264 pub fn client(&self) -> &ManagedClient<'a> {
1266 &self.client
1267 }
1268}
1269
1270fn persist_archive_files(cache: &Cache, archive: &Path, files: &[HashedFile]) -> io::Result<()> {
1272 initialize_rayon_once();
1273 let targets = files
1274 .par_iter()
1275 .filter(|file| !file.path().ends_with("RECORD"))
1277 .map(|file| {
1278 let id = ArchiveFileId::from_digest(&file.object_digest_hex());
1279 (archive.join(file.path()), cache.archive_file(&id))
1280 })
1281 .collect::<Vec<_>>();
1282
1283 let mut shards: FxHashMap<&Path, Vec<_>> = FxHashMap::default();
1286 for (source, target) in &targets {
1287 let Some(parent) = target.parent() else {
1288 return Err(io::Error::new(
1289 io::ErrorKind::InvalidInput,
1290 "archive file path must have a parent directory",
1291 ));
1292 };
1293 shards.entry(parent).or_default().push((source, target));
1294 }
1295
1296 let mut shards = shards
1297 .into_iter()
1298 .map(|(parent, files)| (parent, files, Ok(())))
1299 .collect::<Vec<_>>();
1300 shards.sort_unstable_by_key(|(_, files, _)| Reverse(files.len()));
1302
1303 in_place_scope(|scope| -> io::Result<()> {
1306 for (parent, files, result) in &mut shards {
1307 fs_err::create_dir_all(parent)?;
1308 scope.spawn(move |_| {
1309 *result = files
1310 .iter()
1311 .try_for_each(|(source, target)| persist_archive_file(source, target));
1312 });
1313 }
1314 Ok(())
1315 })?;
1316
1317 shards.into_iter().try_for_each(|(_, _, result)| result)
1318}
1319
1320fn persist_archive_file(src: &Path, dst: &Path) -> io::Result<()> {
1322 match fs_err::hard_link(src, dst) {
1325 Ok(()) => return Ok(()),
1326 Err(_) if dst.try_exists()? => {}
1327 Err(_) => return uv_fs::copy_atomic_sync(src, dst),
1328 }
1329
1330 if let Err(err) = fs_err::remove_file(src)
1332 && err.kind() != io::ErrorKind::NotFound
1333 {
1334 return Err(err);
1335 }
1336
1337 fs_err::hard_link(dst, src).or_else(|_| uv_fs::copy_atomic_sync(dst, src))
1338}
1339
1340pub struct ManagedClient<'a> {
1342 pub unmanaged: &'a RegistryClient,
1343 control: Arc<Semaphore>,
1344}
1345
1346impl<'a> ManagedClient<'a> {
1347 fn new(client: &'a RegistryClient, control: Arc<Semaphore>) -> Self {
1349 ManagedClient {
1350 unmanaged: client,
1351 control,
1352 }
1353 }
1354
1355 pub async fn managed<F, T>(&self, f: impl FnOnce(&'a RegistryClient) -> F) -> T
1360 where
1361 F: Future<Output = T>,
1362 {
1363 let _permit = self.control.acquire().await.unwrap();
1364 f(self.unmanaged).await
1365 }
1366
1367 pub async fn manual<F, T>(&'a self, f: impl FnOnce(&'a RegistryClient, &'a Semaphore) -> F) -> T
1375 where
1376 F: Future<Output = T>,
1377 {
1378 f(self.unmanaged, &self.control).await
1379 }
1380}
1381
1382fn content_length(response: &reqwest::Response) -> Option<u64> {
1384 response
1385 .headers()
1386 .get(reqwest::header::CONTENT_LENGTH)
1387 .and_then(|val| val.to_str().ok())
1388 .and_then(|val| val.parse::<u64>().ok())
1389}
1390
1391struct ProgressReader<'a, R> {
1393 reader: R,
1394 index: usize,
1395 reporter: &'a dyn Reporter,
1396}
1397
1398impl<'a, R> ProgressReader<'a, R> {
1399 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1401 Self {
1402 reader,
1403 index,
1404 reporter,
1405 }
1406 }
1407}
1408
1409impl<R> AsyncRead for ProgressReader<'_, R>
1410where
1411 R: AsyncRead + Unpin,
1412{
1413 fn poll_read(
1414 mut self: Pin<&mut Self>,
1415 cx: &mut Context<'_>,
1416 buf: &mut ReadBuf<'_>,
1417 ) -> Poll<io::Result<()>> {
1418 Pin::new(&mut self.as_mut().reader)
1419 .poll_read(cx, buf)
1420 .map_ok(|()| {
1421 self.reporter
1422 .on_download_progress(self.index, buf.filled().len() as u64);
1423 })
1424 }
1425}
1426
1427#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1431pub struct HttpArchivePointer {
1432 archive: Archive,
1433}
1434
1435impl HttpArchivePointer {
1436 pub fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
1438 match fs_err::File::open(path.as_ref()) {
1439 Ok(file) => {
1440 let data = DataWithCachePolicy::from_reader(file)?.data;
1441 let archive = rmp_serde::from_slice::<Archive>(&data)?;
1442 Ok(Some(Self { archive }))
1443 }
1444 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
1445 Err(err) => Err(Error::CacheRead(err)),
1446 }
1447 }
1448
1449 pub fn into_archive(self) -> Archive {
1451 self.archive
1452 }
1453
1454 pub fn to_cache_info(&self) -> CacheInfo {
1456 CacheInfo::default()
1457 }
1458
1459 pub fn to_build_info(&self) -> Option<BuildInfo> {
1461 None
1462 }
1463}
1464
1465#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1469pub struct PathArchivePointer {
1470 timestamp: Timestamp,
1471 archive: Archive,
1472}
1473
1474impl PathArchivePointer {
1475 pub fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
1477 match fs_err::read(path) {
1478 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
1479 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
1480 Err(err) => Err(Error::CacheRead(err)),
1481 }
1482 }
1483
1484 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
1486 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
1487 .await
1488 .map_err(Error::CacheWrite)
1489 }
1490
1491 pub fn is_up_to_date(&self, modified: Timestamp) -> bool {
1493 self.timestamp == modified
1494 }
1495
1496 pub fn into_archive(self) -> Archive {
1498 self.archive
1499 }
1500
1501 pub fn to_cache_info(&self) -> CacheInfo {
1503 CacheInfo::from_timestamp(self.timestamp)
1504 }
1505
1506 pub fn to_build_info(&self) -> Option<BuildInfo> {
1508 None
1509 }
1510}