1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt::Display;
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use std::str::FromStr;
7use std::task::{Context, Poll};
8use std::time::{Duration, Instant, SystemTimeError};
9use std::{env, io};
10
11use futures::TryStreamExt;
12use itertools::Itertools;
13use owo_colors::OwoColorize;
14use reqwest_retry::RetryError;
15use reqwest_retry::policies::ExponentialBackoff;
16use serde::Deserialize;
17use thiserror::Error;
18use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt, BufWriter, ReadBuf};
19use tokio_util::compat::FuturesAsyncReadCompatExt;
20use tokio_util::either::Either;
21use tracing::{debug, instrument};
22use url::Url;
23
24use uv_client::{
25 BaseClient, RetriableError, WrappedReqwestError, fetch_with_url_fallback,
26 retryable_on_request_failure,
27};
28use uv_distribution_filename::{ExtensionError, SourceDistExtension};
29use uv_extract::hash::Hasher;
30use uv_fs::{Simplified, rename_with_retry};
31use uv_platform::{self as platform, Arch, Libc, Os, Platform};
32use uv_pypi_types::{HashAlgorithm, HashDigest};
33use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
34use uv_static::{
35 EnvVars, astral_mirror_base_url, astral_mirror_url_from_env, custom_astral_mirror_url,
36};
37
38use crate::PythonVariant;
39use crate::implementation::{
40 Error as ImplementationError, ImplementationName, LenientImplementationName,
41};
42use crate::installation::PythonInstallationKey;
43use crate::managed::ManagedPythonInstallation;
44use crate::python_version::{BuildVersionError, python_build_version_from_env};
45use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
46
47#[derive(Error, Debug)]
48pub enum Error {
49 #[error(transparent)]
50 Io(#[from] io::Error),
51 #[error(transparent)]
52 ImplementationError(#[from] ImplementationError),
53 #[error("Expected download URL (`{0}`) to end in a supported file extension: {1}")]
54 MissingExtension(String, ExtensionError),
55 #[error("Invalid Python version: {0}")]
56 InvalidPythonVersion(String),
57 #[error("Invalid request key (empty request)")]
58 EmptyRequest,
59 #[error("Invalid request key (too many parts): {0}")]
60 TooManyParts(String),
61 #[error("Failed to download {0}")]
62 NetworkError(DisplaySafeUrl, #[source] WrappedReqwestError),
63 #[error(
64 "Request failed after {retries} {subject} in {duration:.1}s",
65 subject = if *retries > 1 { "retries" } else { "retry" },
66 duration = duration.as_secs_f32()
67 )]
68 NetworkErrorWithRetries {
69 #[source]
70 err: Box<Self>,
71 retries: u32,
72 duration: Duration,
73 },
74 #[error("Failed to download {0}")]
75 NetworkMiddlewareError(DisplaySafeUrl, #[source] anyhow::Error),
76 #[error("Failed to extract archive: {0}")]
77 ExtractError(String, #[source] uv_extract::Error),
78 #[error("Failed to hash installation")]
79 HashExhaustion(#[source] io::Error),
80 #[error("Hash mismatch for `{installation}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")]
81 HashMismatch {
82 installation: String,
83 expected: String,
84 actual: String,
85 },
86 #[error("Invalid download URL")]
87 InvalidUrl(#[from] DisplaySafeUrlError),
88 #[error("Invalid download URL: {0}")]
89 InvalidUrlFormat(DisplaySafeUrl),
90 #[error("Invalid path in file URL: `{0}`")]
91 InvalidFileUrl(String),
92 #[error("Failed to create download directory")]
93 DownloadDirError(#[source] io::Error),
94 #[error("Failed to copy to: {0}", to.user_display())]
95 CopyError {
96 to: PathBuf,
97 #[source]
98 err: io::Error,
99 },
100 #[error("Failed to read managed Python installation directory: {0}", dir.user_display())]
101 ReadError {
102 dir: PathBuf,
103 #[source]
104 err: io::Error,
105 },
106 #[error("Failed to parse request part")]
107 InvalidRequestPlatform(#[from] platform::Error),
108 #[error("No download found for request: {}", _0.green())]
109 NoDownloadFound(PythonDownloadRequest),
110 #[error("A mirror was provided via `{0}`, but the URL does not match the expected format: {0}")]
111 Mirror(&'static str, String),
112 #[error("Failed to determine the libc used on the current platform")]
113 LibcDetection(#[from] platform::LibcDetectionError),
114 #[error("Unable to parse the JSON Python download list at {0}")]
115 InvalidPythonDownloadsJSON(String, #[source] serde_json::Error),
116 #[error("This version of uv is too old to support the JSON Python download list at {0}")]
117 UnsupportedPythonDownloadsJSON(String),
118 #[error("Error while fetching remote python downloads json from '{0}'")]
119 FetchingPythonDownloadsJSONError(String, #[source] Box<Self>),
120 #[error("An offline Python installation was requested, but {file} (from {url}) is missing in {}", python_builds_dir.user_display())]
121 OfflinePythonMissing {
122 file: Box<PythonInstallationKey>,
123 url: Box<DisplaySafeUrl>,
124 python_builds_dir: PathBuf,
125 },
126 #[error(transparent)]
127 BuildVersion(#[from] BuildVersionError),
128 #[error("No download URL found for Python")]
129 NoPythonDownloadUrlFound,
130 #[error(transparent)]
131 SystemTime(#[from] SystemTimeError),
132}
133
134impl RetriableError for Error {
135 fn retries(&self) -> u32 {
140 if let Self::NetworkErrorWithRetries { retries, .. } = self {
144 return *retries;
145 }
146 if let Self::NetworkMiddlewareError(_, anyhow_error) = self
147 && let Some(RetryError::WithRetries { retries, .. }) =
148 anyhow_error.downcast_ref::<RetryError>()
149 {
150 return *retries;
151 }
152 0
153 }
154
155 fn should_try_next_url(&self) -> bool {
161 match self {
162 Self::NetworkError(..)
167 | Self::NetworkMiddlewareError(..)
168 | Self::NetworkErrorWithRetries { .. } => true,
169 Self::Io(err) => retryable_on_request_failure(err).is_some(),
173 _ => false,
174 }
175 }
176
177 fn into_retried(self, retries: u32, duration: Duration) -> Self {
178 Self::NetworkErrorWithRetries {
179 err: Box::new(self),
180 retries,
181 duration,
182 }
183 }
184}
185
186const CPYTHON_DOWNLOADS_URL_PREFIX: &str =
188 "https://github.com/astral-sh/python-build-standalone/releases/download/";
189
190const CPYTHON_MIRROR_SUFFIX: &str = "/github/python-build-standalone/releases/download/";
192
193fn effective_cpython_mirror(astral_mirror_url: Option<&str>) -> String {
195 format!(
196 "{}{CPYTHON_MIRROR_SUFFIX}",
197 astral_mirror_base_url(astral_mirror_url)
198 )
199}
200
201#[derive(Debug, PartialEq, Eq, Clone, Hash)]
202pub struct ManagedPythonDownload {
203 key: PythonInstallationKey,
204 url: Cow<'static, str>,
205 sha256: Option<Cow<'static, str>>,
206 build: Option<&'static str>,
207}
208
209#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
210pub struct PythonDownloadRequest {
211 pub(crate) version: Option<VersionRequest>,
212 pub(crate) implementation: Option<ImplementationName>,
213 pub(crate) arch: Option<ArchRequest>,
214 pub(crate) os: Option<Os>,
215 pub(crate) libc: Option<Libc>,
216 pub(crate) build: Option<String>,
217
218 pub(crate) prereleases: Option<bool>,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
224pub enum ArchRequest {
225 Explicit(Arch),
226 Environment(Arch),
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
230pub struct PlatformRequest {
231 pub(crate) os: Option<Os>,
232 pub(crate) arch: Option<ArchRequest>,
233 pub(crate) libc: Option<Libc>,
234}
235
236impl PlatformRequest {
237 pub fn matches(&self, platform: &Platform) -> bool {
239 if let Some(os) = self.os {
240 if !platform.os.supports(os) {
241 return false;
242 }
243 }
244
245 if let Some(arch) = self.arch {
246 if !arch.satisfied_by(platform) {
247 return false;
248 }
249 }
250
251 if let Some(libc) = self.libc {
252 if platform.libc != libc {
253 return false;
254 }
255 }
256
257 true
258 }
259}
260
261impl Display for PlatformRequest {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 let mut parts = Vec::new();
264 if let Some(os) = &self.os {
265 parts.push(os.to_string());
266 }
267 if let Some(arch) = &self.arch {
268 parts.push(arch.to_string());
269 }
270 if let Some(libc) = &self.libc {
271 parts.push(libc.to_string());
272 }
273 write!(f, "{}", parts.join("-"))
274 }
275}
276
277impl Display for ArchRequest {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 match self {
280 Self::Explicit(arch) | Self::Environment(arch) => write!(f, "{arch}"),
281 }
282 }
283}
284
285impl ArchRequest {
286 fn satisfied_by(self, platform: &Platform) -> bool {
287 match self {
288 Self::Explicit(request) => request == platform.arch,
289 Self::Environment(env) => {
290 let env_platform = Platform::new(platform.os, env, platform.libc);
292 env_platform.supports(platform)
293 }
294 }
295 }
296
297 pub fn inner(&self) -> Arch {
298 match self {
299 Self::Explicit(arch) | Self::Environment(arch) => *arch,
300 }
301 }
302}
303
304impl PythonDownloadRequest {
305 pub fn new(
306 version: Option<VersionRequest>,
307 implementation: Option<ImplementationName>,
308 arch: Option<ArchRequest>,
309 os: Option<Os>,
310 libc: Option<Libc>,
311 prereleases: Option<bool>,
312 ) -> Self {
313 Self {
314 version,
315 implementation,
316 arch,
317 os,
318 libc,
319 build: None,
320 prereleases,
321 }
322 }
323
324 #[must_use]
325 pub(crate) fn with_implementation(mut self, implementation: ImplementationName) -> Self {
326 match implementation {
327 ImplementationName::Pyodide => {
329 self = self.with_os(Os::new(target_lexicon::OperatingSystem::Emscripten));
330 self = self.with_arch(Arch::new(target_lexicon::Architecture::Wasm32, None));
331 self = self.with_libc(Libc::Some(target_lexicon::Environment::Musl));
332 }
333 _ => {
334 self.implementation = Some(implementation);
335 }
336 }
337 self
338 }
339
340 #[must_use]
341 pub fn with_version(mut self, version: VersionRequest) -> Self {
342 self.version = Some(version);
343 self
344 }
345
346 #[must_use]
347 pub fn with_arch(mut self, arch: Arch) -> Self {
348 self.arch = Some(ArchRequest::Explicit(arch));
349 self
350 }
351
352 #[must_use]
353 pub fn with_any_arch(mut self) -> Self {
354 self.arch = None;
355 self
356 }
357
358 #[must_use]
359 fn with_os(mut self, os: Os) -> Self {
360 self.os = Some(os);
361 self
362 }
363
364 #[must_use]
365 fn with_libc(mut self, libc: Libc) -> Self {
366 self.libc = Some(libc);
367 self
368 }
369
370 #[must_use]
371 pub fn with_prereleases(mut self, prereleases: bool) -> Self {
372 self.prereleases = Some(prereleases);
373 self
374 }
375
376 pub fn from_request(request: &PythonRequest) -> Option<Self> {
381 match request {
382 PythonRequest::Version(version) => Some(Self::default().with_version(version.clone())),
383 PythonRequest::Implementation(implementation) => {
384 Some(Self::default().with_implementation(*implementation))
385 }
386 PythonRequest::ImplementationVersion(implementation, version) => Some(
387 Self::default()
388 .with_implementation(*implementation)
389 .with_version(version.clone()),
390 ),
391 PythonRequest::Key(request) => Some(request.clone()),
392 PythonRequest::Any => Some(Self {
393 prereleases: Some(true), ..Self::default()
395 }),
396 PythonRequest::Default => Some(Self::default()),
397 PythonRequest::Directory(_)
399 | PythonRequest::ExecutableName(_)
400 | PythonRequest::File(_) => None,
401 }
402 }
403
404 pub fn fill_platform(mut self) -> Result<Self, Error> {
408 let platform = Platform::from_env().map_err(|err| match err {
409 platform::Error::LibcDetectionError(err) => Error::LibcDetection(err),
410 err => Error::InvalidRequestPlatform(err),
411 })?;
412 if self.arch.is_none() {
413 self.arch = Some(ArchRequest::Environment(platform.arch));
414 }
415 if self.os.is_none() {
416 self.os = Some(platform.os);
417 }
418 if self.libc.is_none() {
419 self.libc = Some(platform.libc);
420 }
421 Ok(self)
422 }
423
424 fn fill_build_from_env(mut self) -> Result<Self, Error> {
426 if self.build.is_some() {
427 return Ok(self);
428 }
429 let Some(implementation) = self.implementation else {
430 return Ok(self);
431 };
432
433 self.build = python_build_version_from_env(implementation)?;
434 Ok(self)
435 }
436
437 pub fn fill(mut self) -> Result<Self, Error> {
438 if self.implementation.is_none() {
439 self.implementation = Some(ImplementationName::CPython);
440 }
441 self = self.fill_platform()?;
442 self = self.fill_build_from_env()?;
443 Ok(self)
444 }
445
446 pub fn implementation(&self) -> Option<&ImplementationName> {
447 self.implementation.as_ref()
448 }
449
450 pub fn version(&self) -> Option<&VersionRequest> {
451 self.version.as_ref()
452 }
453
454 pub fn arch(&self) -> Option<&ArchRequest> {
455 self.arch.as_ref()
456 }
457
458 pub fn os(&self) -> Option<&Os> {
459 self.os.as_ref()
460 }
461
462 pub fn libc(&self) -> Option<&Libc> {
463 self.libc.as_ref()
464 }
465
466 pub fn take_version(&mut self) -> Option<VersionRequest> {
467 self.version.take()
468 }
469
470 #[must_use]
473 pub(crate) fn unset_defaults(self) -> Self {
474 let request = self.unset_non_platform_defaults();
475
476 if let Ok(host) = Platform::from_env() {
477 request.unset_platform_defaults(&host)
478 } else {
479 request
480 }
481 }
482
483 fn unset_non_platform_defaults(mut self) -> Self {
484 self.implementation = self
485 .implementation
486 .filter(|implementation_name| *implementation_name != ImplementationName::default());
487
488 self.version = self
489 .version
490 .filter(|version| !matches!(version, VersionRequest::Any | VersionRequest::Default));
491
492 self.arch = self
494 .arch
495 .filter(|arch| !matches!(arch, ArchRequest::Environment(_)));
496
497 self
498 }
499
500 #[cfg(test)]
501 fn unset_defaults_for_host(self, host: &Platform) -> Self {
502 self.unset_non_platform_defaults()
503 .unset_platform_defaults(host)
504 }
505
506 fn unset_platform_defaults(mut self, host: &Platform) -> Self {
507 self.os = self.os.filter(|os| *os != host.os);
508
509 self.libc = self.libc.filter(|libc| *libc != host.libc);
510
511 self.arch = self
512 .arch
513 .filter(|arch| !matches!(arch, ArchRequest::Explicit(explicit_arch) if *explicit_arch == host.arch));
514
515 self
516 }
517
518 #[must_use]
520 pub(crate) fn without_patch(mut self) -> Self {
521 self.version = self.version.take().map(VersionRequest::only_minor);
522 self.prereleases = None;
523 self.build = None;
524 self
525 }
526
527 pub fn simplified_display(self) -> Option<String> {
532 let parts = [
533 self.implementation
534 .map(|implementation| implementation.to_string()),
535 self.version.map(|version| version.to_string()),
536 self.os.map(|os| os.to_string()),
537 self.arch.map(|arch| arch.to_string()),
538 self.libc.map(|libc| libc.to_string()),
539 ];
540
541 let joined = parts.into_iter().flatten().collect::<Vec<_>>().join("-");
542
543 if joined.is_empty() {
544 None
545 } else {
546 Some(joined)
547 }
548 }
549
550 pub fn satisfied_by_key(&self, key: &PythonInstallationKey) -> bool {
552 let request = PlatformRequest {
554 os: self.os,
555 arch: self.arch,
556 libc: self.libc,
557 };
558 if !request.matches(key.platform()) {
559 return false;
560 }
561
562 if let Some(implementation) = &self.implementation {
563 if key.implementation != LenientImplementationName::from(*implementation) {
564 return false;
565 }
566 }
567 if !self.allows_prereleases() && key.prerelease.is_some() {
569 return false;
570 }
571 if let Some(version) = &self.version {
572 if !version.matches_major_minor_patch_prerelease(
573 key.major,
574 key.minor,
575 key.patch,
576 key.prerelease,
577 ) {
578 return false;
579 }
580 if let Some(variant) = version.variant() {
581 if variant != key.variant {
582 return false;
583 }
584 }
585 }
586 true
587 }
588
589 fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool {
591 if !self.satisfied_by_key(download.key()) {
593 return false;
594 }
595
596 if let Some(ref requested_build) = self.build {
598 let Some(download_build) = download.build() else {
599 debug!(
600 "Skipping download `{}`: a build version was requested but is not available for this download",
601 download
602 );
603 return false;
604 };
605
606 if download_build != requested_build {
607 debug!(
608 "Skipping download `{}`: requested build version `{}` does not match download build version `{}`",
609 download, requested_build, download_build
610 );
611 return false;
612 }
613 }
614
615 true
616 }
617
618 pub fn allows_prereleases(&self) -> bool {
620 self.prereleases.unwrap_or_else(|| {
621 self.version
622 .as_ref()
623 .is_some_and(VersionRequest::allows_prereleases)
624 })
625 }
626
627 pub(crate) fn allows_debug(&self) -> bool {
629 self.version.as_ref().is_some_and(VersionRequest::is_debug)
630 }
631
632 pub(crate) fn allows_alternative_implementations(&self) -> bool {
634 self.implementation
635 .is_some_and(|implementation| !matches!(implementation, ImplementationName::CPython))
636 || self.os.is_some_and(|os| os.is_emscripten())
637 }
638
639 pub(crate) fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
640 let executable = interpreter.sys_executable().display();
641 if let Some(version) = self.version() {
642 if !version.matches_interpreter(interpreter) {
643 let interpreter_version = interpreter.python_version();
644 debug!(
645 "Skipping interpreter at `{executable}`: version `{interpreter_version}` does not match request `{version}`"
646 );
647 return false;
648 }
649 }
650 let platform = self.platform();
651 let interpreter_platform = Platform::from(interpreter.platform());
652 if !platform.matches(&interpreter_platform) {
653 debug!(
654 "Skipping interpreter at `{executable}`: platform `{interpreter_platform}` does not match request `{platform}`",
655 );
656 return false;
657 }
658 if let Some(implementation) = self.implementation() {
659 if !implementation.matches_interpreter(interpreter) {
660 debug!(
661 "Skipping interpreter at `{executable}`: implementation `{}` does not match request `{implementation}`",
662 interpreter.implementation_name(),
663 );
664 return false;
665 }
666 }
667 true
668 }
669
670 pub fn platform(&self) -> PlatformRequest {
672 PlatformRequest {
673 os: self.os,
674 arch: self.arch,
675 libc: self.libc,
676 }
677 }
678}
679
680impl TryFrom<&PythonInstallationKey> for PythonDownloadRequest {
681 type Error = LenientImplementationName;
682
683 fn try_from(key: &PythonInstallationKey) -> Result<Self, Self::Error> {
684 let implementation = match key.implementation().into_owned() {
685 LenientImplementationName::Known(name) => name,
686 unknown @ LenientImplementationName::Unknown(_) => return Err(unknown),
687 };
688
689 Ok(Self::new(
690 Some(VersionRequest::MajorMinor(
691 key.major(),
692 key.minor(),
693 *key.variant(),
694 )),
695 Some(implementation),
696 Some(ArchRequest::Explicit(*key.arch())),
697 Some(*key.os()),
698 Some(*key.libc()),
699 Some(key.prerelease().is_some()),
700 ))
701 }
702}
703
704impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
705 fn from(installation: &ManagedPythonInstallation) -> Self {
706 let key = installation.key();
707 Self::new(
708 Some(VersionRequest::from(&key.version())),
709 match &key.implementation {
710 LenientImplementationName::Known(implementation) => Some(*implementation),
711 LenientImplementationName::Unknown(name) => unreachable!(
712 "Managed Python installations are expected to always have known implementation names, found {name}"
713 ),
714 },
715 Some(ArchRequest::Explicit(*key.arch())),
716 Some(*key.os()),
717 Some(*key.libc()),
718 Some(key.prerelease.is_some()),
719 )
720 }
721}
722
723impl Display for PythonDownloadRequest {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 let mut parts = Vec::new();
726 if let Some(implementation) = self.implementation {
727 parts.push(implementation.to_string());
728 } else {
729 parts.push("any".to_string());
730 }
731 if let Some(version) = &self.version {
732 parts.push(version.to_string());
733 } else {
734 parts.push("any".to_string());
735 }
736 if let Some(os) = &self.os {
737 parts.push(os.to_string());
738 } else {
739 parts.push("any".to_string());
740 }
741 if let Some(arch) = self.arch {
742 parts.push(arch.to_string());
743 } else {
744 parts.push("any".to_string());
745 }
746 if let Some(libc) = self.libc {
747 parts.push(libc.to_string());
748 } else {
749 parts.push("any".to_string());
750 }
751 write!(f, "{}", parts.join("-"))
752 }
753}
754impl FromStr for PythonDownloadRequest {
755 type Err = Error;
756
757 fn from_str(s: &str) -> Result<Self, Self::Err> {
758 #[derive(Debug, Clone)]
759 enum Position {
760 Start,
761 Implementation,
762 Version,
763 Os,
764 Arch,
765 Libc,
766 End,
767 }
768
769 impl Position {
770 pub(crate) fn next(&self) -> Self {
771 match self {
772 Self::Start => Self::Implementation,
773 Self::Implementation => Self::Version,
774 Self::Version => Self::Os,
775 Self::Os => Self::Arch,
776 Self::Arch => Self::Libc,
777 Self::Libc => Self::End,
778 Self::End => Self::End,
779 }
780 }
781 }
782
783 #[derive(Debug)]
784 struct State<'a, P: Iterator<Item = &'a str>> {
785 parts: P,
786 part: Option<&'a str>,
787 position: Position,
788 error: Option<Error>,
789 count: usize,
790 }
791
792 impl<'a, P: Iterator<Item = &'a str>> State<'a, P> {
793 fn new(parts: P) -> Self {
794 Self {
795 parts,
796 part: None,
797 position: Position::Start,
798 error: None,
799 count: 0,
800 }
801 }
802
803 fn next_part(&mut self) {
804 self.next_position();
805 self.part = self.parts.next();
806 self.count += 1;
807 self.error.take();
808 }
809
810 fn next_position(&mut self) {
811 self.position = self.position.next();
812 }
813
814 fn record_err(&mut self, err: Error) {
815 self.error.get_or_insert(err);
818 }
819 }
820
821 if s.is_empty() {
822 return Err(Error::EmptyRequest);
823 }
824
825 let mut parts = s.split('-');
826
827 let mut implementation = None;
828 let mut version = None;
829 let mut os = None;
830 let mut arch = None;
831 let mut libc = None;
832
833 let mut state = State::new(parts.by_ref());
834 state.next_part();
835
836 while let Some(part) = state.part {
837 match state.position {
838 Position::Start => unreachable!("We start before the loop"),
839 Position::Implementation => {
840 if part.eq_ignore_ascii_case("any") {
841 state.next_part();
842 continue;
843 }
844 match ImplementationName::from_str(part) {
845 Ok(val) => {
846 implementation = Some(val);
847 state.next_part();
848 }
849 Err(err) => {
850 state.next_position();
851 state.record_err(err.into());
852 }
853 }
854 }
855 Position::Version => {
856 if part.eq_ignore_ascii_case("any") {
857 state.next_part();
858 continue;
859 }
860 match VersionRequest::from_str(part)
861 .map_err(|_| Error::InvalidPythonVersion(part.to_string()))
862 {
863 Ok(val) => {
865 version = Some(val);
866 state.next_part();
867 }
868 Err(err) => {
869 state.next_position();
870 state.record_err(err);
871 }
872 }
873 }
874 Position::Os => {
875 if part.eq_ignore_ascii_case("any") {
876 state.next_part();
877 continue;
878 }
879 match Os::from_str(part) {
880 Ok(val) => {
881 os = Some(val);
882 state.next_part();
883 }
884 Err(err) => {
885 state.next_position();
886 state.record_err(err.into());
887 }
888 }
889 }
890 Position::Arch => {
891 if part.eq_ignore_ascii_case("any") {
892 state.next_part();
893 continue;
894 }
895 match Arch::from_str(part) {
896 Ok(val) => {
897 arch = Some(ArchRequest::Explicit(val));
898 state.next_part();
899 }
900 Err(err) => {
901 state.next_position();
902 state.record_err(err.into());
903 }
904 }
905 }
906 Position::Libc => {
907 if part.eq_ignore_ascii_case("any") {
908 state.next_part();
909 continue;
910 }
911 match Libc::from_str(part) {
912 Ok(val) => {
913 libc = Some(val);
914 state.next_part();
915 }
916 Err(err) => {
917 state.next_position();
918 state.record_err(err.into());
919 }
920 }
921 }
922 Position::End => {
923 if state.count > 5 {
924 return Err(Error::TooManyParts(s.to_string()));
925 }
926
927 if let Some(err) = state.error {
934 return Err(err);
935 }
936 state.next_part();
937 }
938 }
939 }
940
941 Ok(Self::new(version, implementation, arch, os, libc, None))
942 }
943}
944
945const BUILTIN_PYTHON_DOWNLOADS_JSON: &[u8] =
946 include_bytes!(concat!(env!("OUT_DIR"), "/download-metadata-minified.json"));
947
948pub struct ManagedPythonDownloadList {
949 downloads: Vec<ManagedPythonDownload>,
950}
951
952#[derive(Debug, Deserialize, Clone)]
953struct JsonPythonDownload {
954 name: String,
955 arch: JsonArch,
956 os: String,
957 libc: String,
958 major: u8,
959 minor: u8,
960 patch: u8,
961 prerelease: Option<String>,
962 url: String,
963 sha256: Option<String>,
964 variant: Option<String>,
965 build: Option<String>,
966}
967
968#[derive(Debug, Deserialize, Clone)]
969struct JsonArch {
970 family: String,
971 variant: Option<String>,
972}
973
974#[derive(Debug, Clone)]
975pub enum DownloadResult {
976 AlreadyAvailable(PathBuf),
977 Fetched(PathBuf),
978}
979
980impl ManagedPythonDownloadList {
981 fn iter_all(&self) -> impl Iterator<Item = &ManagedPythonDownload> {
983 self.downloads.iter()
984 }
985
986 pub fn iter_matching(
988 &self,
989 request: &PythonDownloadRequest,
990 ) -> impl Iterator<Item = &ManagedPythonDownload> {
991 self.iter_all()
992 .filter(move |download| request.satisfied_by_download(download))
993 }
994
995 pub fn find(&self, request: &PythonDownloadRequest) -> Result<&ManagedPythonDownload, Error> {
1000 if let Some(download) = self.iter_matching(request).next() {
1001 return Ok(download);
1002 }
1003
1004 if !request.allows_prereleases() {
1005 if let Some(download) = self
1006 .iter_matching(&request.clone().with_prereleases(true))
1007 .next()
1008 {
1009 return Ok(download);
1010 }
1011 }
1012
1013 Err(Error::NoDownloadFound(request.clone()))
1014 }
1015
1016 pub async fn new(
1025 client: &BaseClient,
1026 python_downloads_json_url: Option<&str>,
1027 ) -> Result<Self, Error> {
1028 enum Source<'a> {
1033 BuiltIn,
1034 Path(Cow<'a, Path>),
1035 Http(DisplaySafeUrl),
1036 }
1037
1038 let json_source = if let Some(url_or_path) = python_downloads_json_url {
1039 if let Ok(url) = DisplaySafeUrl::parse(url_or_path) {
1040 match url.scheme() {
1041 "http" | "https" => Source::Http(url),
1042 "file" => Source::Path(Cow::Owned(
1043 url.to_file_path().or(Err(Error::InvalidUrlFormat(url)))?,
1044 )),
1045 _ => Source::Path(Cow::Borrowed(Path::new(url_or_path))),
1046 }
1047 } else {
1048 Source::Path(Cow::Borrowed(Path::new(url_or_path)))
1049 }
1050 } else {
1051 Source::BuiltIn
1052 };
1053
1054 let buf: Cow<'_, [u8]> = match json_source {
1055 Source::BuiltIn => BUILTIN_PYTHON_DOWNLOADS_JSON.into(),
1056 Source::Path(ref path) => fs_err::read(path.as_ref())?.into(),
1057 Source::Http(ref url) => fetch_bytes_from_url(client, url)
1058 .await
1059 .map_err(|e| Error::FetchingPythonDownloadsJSONError(url.to_string(), Box::new(e)))?
1060 .into(),
1061 };
1062 let json_downloads: HashMap<String, JsonPythonDownload> = serde_json::from_slice(&buf)
1063 .map_err(
1064 #[expect(clippy::zero_sized_map_values)]
1071 |e| {
1072 let source = match json_source {
1073 Source::BuiltIn => "EMBEDDED IN THE BINARY".to_owned(),
1074 Source::Path(path) => path.to_string_lossy().to_string(),
1075 Source::Http(url) => url.to_string(),
1076 };
1077 if let Ok(keys) =
1078 serde_json::from_slice::<HashMap<String, serde::de::IgnoredAny>>(&buf)
1079 && keys.contains_key("version")
1080 {
1081 Error::UnsupportedPythonDownloadsJSON(source)
1082 } else {
1083 Error::InvalidPythonDownloadsJSON(source, e)
1084 }
1085 },
1086 )?;
1087
1088 let result = parse_json_downloads(json_downloads);
1089 Ok(Self { downloads: result })
1090 }
1091
1092 pub fn new_only_embedded() -> Result<Self, Error> {
1095 let json_downloads: HashMap<String, JsonPythonDownload> =
1096 serde_json::from_slice(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| {
1097 Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_owned(), e)
1098 })?;
1099 let result = parse_json_downloads(json_downloads);
1100 Ok(Self { downloads: result })
1101 }
1102}
1103
1104async fn fetch_bytes_from_url(client: &BaseClient, url: &DisplaySafeUrl) -> Result<Vec<u8>, Error> {
1105 let (mut reader, size) = read_url(url, client).await?;
1106 let capacity = size.and_then(|s| s.try_into().ok()).unwrap_or(1_048_576);
1107 let mut buf = Vec::with_capacity(capacity);
1108 reader.read_to_end(&mut buf).await?;
1109 Ok(buf)
1110}
1111
1112impl ManagedPythonDownload {
1113 pub fn url(&self) -> &Cow<'static, str> {
1114 &self.url
1115 }
1116
1117 pub fn key(&self) -> &PythonInstallationKey {
1118 &self.key
1119 }
1120
1121 pub fn os(&self) -> &Os {
1122 self.key.os()
1123 }
1124
1125 pub fn sha256(&self) -> Option<&Cow<'static, str>> {
1126 self.sha256.as_ref()
1127 }
1128
1129 pub fn build(&self) -> Option<&'static str> {
1130 self.build
1131 }
1132
1133 #[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
1139 pub async fn fetch_with_retry(
1140 &self,
1141 client: &BaseClient,
1142 retry_policy: &ExponentialBackoff,
1143 installation_dir: &Path,
1144 scratch_dir: &Path,
1145 reinstall: bool,
1146 python_install_mirror: Option<&str>,
1147 pypy_install_mirror: Option<&str>,
1148 reporter: Option<&dyn Reporter>,
1149 ) -> Result<DownloadResult, Error> {
1150 let urls = self.download_urls(python_install_mirror, pypy_install_mirror)?;
1151 if urls.is_empty() {
1152 return Err(Error::NoPythonDownloadUrlFound);
1153 }
1154 fetch_with_url_fallback(&urls, *retry_policy, &format!("`{}`", self.key()), |url| {
1155 self.fetch_from_url(
1156 url,
1157 client,
1158 installation_dir,
1159 scratch_dir,
1160 reinstall,
1161 reporter,
1162 )
1163 })
1164 .await
1165 }
1166
1167 #[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
1169 pub async fn fetch(
1170 &self,
1171 client: &BaseClient,
1172 installation_dir: &Path,
1173 scratch_dir: &Path,
1174 reinstall: bool,
1175 python_install_mirror: Option<&str>,
1176 pypy_install_mirror: Option<&str>,
1177 reporter: Option<&dyn Reporter>,
1178 ) -> Result<DownloadResult, Error> {
1179 let urls = self.download_urls(python_install_mirror, pypy_install_mirror)?;
1180 let url = urls
1181 .into_iter()
1182 .next()
1183 .ok_or(Error::NoPythonDownloadUrlFound)?;
1184 self.fetch_from_url(
1185 url,
1186 client,
1187 installation_dir,
1188 scratch_dir,
1189 reinstall,
1190 reporter,
1191 )
1192 .await
1193 }
1194
1195 async fn fetch_from_url(
1197 &self,
1198 url: DisplaySafeUrl,
1199 client: &BaseClient,
1200 installation_dir: &Path,
1201 scratch_dir: &Path,
1202 reinstall: bool,
1203 reporter: Option<&dyn Reporter>,
1204 ) -> Result<DownloadResult, Error> {
1205 let path = installation_dir.join(self.key().to_string());
1206
1207 if !reinstall && path.is_dir() {
1209 return Ok(DownloadResult::AlreadyAvailable(path));
1210 }
1211
1212 let filename = url
1215 .path_segments()
1216 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1217 .next_back()
1218 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1219 .replace("%2B", "-");
1220 debug_assert!(
1221 filename
1222 .chars()
1223 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'),
1224 "Unexpected char in filename: {filename}"
1225 );
1226 let ext = SourceDistExtension::from_path(&filename)
1227 .map_err(|err| Error::MissingExtension(url.to_string(), err))?;
1228
1229 let temp_dir = tempfile::tempdir_in(scratch_dir).map_err(Error::DownloadDirError)?;
1230
1231 if let Some(python_builds_dir) =
1232 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).filter(|s| !s.is_empty())
1233 {
1234 let python_builds_dir = PathBuf::from(python_builds_dir);
1235 fs_err::create_dir_all(&python_builds_dir)?;
1236 let hash_prefix = match self.sha256.as_deref() {
1237 Some(sha) => {
1238 &sha[..9]
1240 }
1241 None => "none",
1242 };
1243 let target_cache_file = python_builds_dir.join(format!("{hash_prefix}-{filename}"));
1244
1245 let (reader, size): (Box<dyn AsyncRead + Unpin>, Option<u64>) =
1249 match fs_err::tokio::File::open(&target_cache_file).await {
1250 Ok(file) => {
1251 debug!(
1252 "Extracting existing `{}`",
1253 target_cache_file.simplified_display()
1254 );
1255 let size = file.metadata().await?.len();
1256 let reader = Box::new(tokio::io::BufReader::new(file));
1257 (reader, Some(size))
1258 }
1259 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1260 if client.connectivity().is_offline() {
1262 return Err(Error::OfflinePythonMissing {
1263 file: Box::new(self.key().clone()),
1264 url: Box::new(url.clone()),
1265 python_builds_dir,
1266 });
1267 }
1268
1269 self.download_archive(
1270 &url,
1271 client,
1272 reporter,
1273 &python_builds_dir,
1274 &target_cache_file,
1275 )
1276 .await?;
1277
1278 debug!("Extracting `{}`", target_cache_file.simplified_display());
1279 let file = fs_err::tokio::File::open(&target_cache_file).await?;
1280 let size = file.metadata().await?.len();
1281 let reader = Box::new(tokio::io::BufReader::new(file));
1282 (reader, Some(size))
1283 }
1284 Err(err) => return Err(err.into()),
1285 };
1286
1287 self.extract_reader(
1289 reader,
1290 temp_dir.path(),
1291 &filename,
1292 ext,
1293 size,
1294 reporter,
1295 Direction::Extract,
1296 )
1297 .await?;
1298 } else {
1299 debug!("Downloading {url}");
1301 debug!(
1302 "Extracting {filename} to temporary location: {}",
1303 temp_dir.path().simplified_display()
1304 );
1305
1306 let (reader, size) = read_url(&url, client).await?;
1307 self.extract_reader(
1308 reader,
1309 temp_dir.path(),
1310 &filename,
1311 ext,
1312 size,
1313 reporter,
1314 Direction::Download,
1315 )
1316 .await?;
1317 }
1318
1319 let mut extracted = match uv_extract::strip_component(temp_dir.path()) {
1321 Ok(top_level) => top_level,
1322 Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.keep(),
1323 Err(err) => return Err(Error::ExtractError(filename, err)),
1324 };
1325
1326 if extracted.join("install").is_dir() {
1328 extracted = extracted.join("install");
1329 } else if self.os().is_emscripten() {
1331 extracted = extracted.join("pyodide-root").join("dist");
1332 }
1333
1334 #[cfg(unix)]
1335 {
1336 if self.os().is_emscripten() {
1340 fs_err::create_dir_all(extracted.join("bin"))?;
1341 fs_err::os::unix::fs::symlink(
1342 "../python",
1343 extracted
1344 .join("bin")
1345 .join(format!("python{}.{}", self.key.major, self.key.minor)),
1346 )?;
1347 }
1348
1349 match fs_err::os::unix::fs::symlink(
1356 format!("python{}.{}", self.key.major, self.key.minor),
1357 extracted.join("bin").join("python"),
1358 ) {
1359 Ok(()) => {}
1360 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
1361 Err(err) => return Err(err.into()),
1362 }
1363 }
1364
1365 if path.is_dir() {
1367 debug!("Removing existing directory: {}", path.user_display());
1368 fs_err::tokio::remove_dir_all(&path).await?;
1369 }
1370
1371 debug!("Moving {} to {}", extracted.display(), path.user_display());
1373 rename_with_retry(extracted, &path)
1374 .await
1375 .map_err(|err| Error::CopyError {
1376 to: path.clone(),
1377 err,
1378 })?;
1379
1380 Ok(DownloadResult::Fetched(path))
1381 }
1382
1383 async fn download_archive(
1385 &self,
1386 url: &DisplaySafeUrl,
1387 client: &BaseClient,
1388 reporter: Option<&dyn Reporter>,
1389 python_builds_dir: &Path,
1390 target_cache_file: &Path,
1391 ) -> Result<(), Error> {
1392 debug!(
1393 "Downloading {} to `{}`",
1394 url,
1395 target_cache_file.simplified_display()
1396 );
1397
1398 let (mut reader, size) = read_url(url, client).await?;
1399 let temp_dir = tempfile::tempdir_in(python_builds_dir)?;
1400 let temp_file = temp_dir.path().join("download");
1401
1402 {
1404 let mut archive_writer = BufWriter::new(fs_err::tokio::File::create(&temp_file).await?);
1405
1406 if let Some(reporter) = reporter {
1408 let key = reporter.on_request_start(Direction::Download, &self.key, size);
1409 tokio::io::copy(
1410 &mut ProgressReader::new(reader, key, reporter),
1411 &mut archive_writer,
1412 )
1413 .await?;
1414 reporter.on_request_complete(Direction::Download, key);
1415 } else {
1416 tokio::io::copy(&mut reader, &mut archive_writer).await?;
1417 }
1418
1419 archive_writer.flush().await?;
1420 }
1421 match rename_with_retry(&temp_file, target_cache_file).await {
1423 Ok(()) => {}
1424 Err(_) if target_cache_file.is_file() => {}
1425 Err(err) => return Err(err.into()),
1426 }
1427 Ok(())
1428 }
1429
1430 async fn extract_reader(
1433 &self,
1434 reader: impl AsyncRead + Unpin,
1435 target: &Path,
1436 filename: &String,
1437 ext: SourceDistExtension,
1438 size: Option<u64>,
1439 reporter: Option<&dyn Reporter>,
1440 direction: Direction,
1441 ) -> Result<(), Error> {
1442 let mut hashers = if self.sha256.is_some() {
1443 vec![Hasher::from(HashAlgorithm::Sha256)]
1444 } else {
1445 vec![]
1446 };
1447 let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
1448
1449 if let Some(reporter) = reporter {
1450 let progress_key = reporter.on_request_start(direction, &self.key, size);
1451 let mut reader = ProgressReader::new(&mut hasher, progress_key, reporter);
1452 uv_extract::stream::archive(filename, &mut reader, ext, target)
1453 .await
1454 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1455 reporter.on_request_complete(direction, progress_key);
1456 } else {
1457 uv_extract::stream::archive(filename, &mut hasher, ext, target)
1458 .await
1459 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1460 }
1461 hasher.finish().await.map_err(Error::HashExhaustion)?;
1462
1463 if let Some(expected) = self.sha256.as_deref() {
1465 let actual = HashDigest::from(hashers.pop().unwrap()).digest;
1466 if !actual.eq_ignore_ascii_case(expected) {
1467 return Err(Error::HashMismatch {
1468 installation: self.key.to_string(),
1469 expected: expected.to_string(),
1470 actual: actual.to_string(),
1471 });
1472 }
1473 }
1474
1475 Ok(())
1476 }
1477
1478 pub fn python_version(&self) -> PythonVersion {
1479 self.key.version()
1480 }
1481
1482 pub fn download_url(
1488 &self,
1489 python_install_mirror: Option<&str>,
1490 pypy_install_mirror: Option<&str>,
1491 ) -> Result<DisplaySafeUrl, Error> {
1492 self.download_urls(python_install_mirror, pypy_install_mirror)
1493 .map(|mut urls| urls.remove(0))
1494 }
1495
1496 pub fn download_urls(
1504 &self,
1505 python_install_mirror: Option<&str>,
1506 pypy_install_mirror: Option<&str>,
1507 ) -> Result<Vec<DisplaySafeUrl>, Error> {
1508 let custom_astral_mirror = astral_mirror_url_from_env();
1509 self.download_urls_with_astral_mirror(
1510 python_install_mirror,
1511 pypy_install_mirror,
1512 custom_astral_mirror.as_deref(),
1513 )
1514 }
1515
1516 fn download_urls_with_astral_mirror(
1517 &self,
1518 python_install_mirror: Option<&str>,
1519 pypy_install_mirror: Option<&str>,
1520 astral_mirror_url: Option<&str>,
1521 ) -> Result<Vec<DisplaySafeUrl>, Error> {
1522 let astral_mirror_url = custom_astral_mirror_url(astral_mirror_url);
1523 match self.key.implementation {
1524 LenientImplementationName::Known(ImplementationName::CPython) => {
1525 if let Some(mirror) = python_install_mirror {
1526 let Some(suffix) = self.url.strip_prefix(CPYTHON_DOWNLOADS_URL_PREFIX) else {
1528 return Err(Error::Mirror(
1529 EnvVars::UV_PYTHON_INSTALL_MIRROR,
1530 self.url.to_string(),
1531 ));
1532 };
1533 return Ok(vec![DisplaySafeUrl::parse(
1534 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1535 )?]);
1536 }
1537 if let Some(suffix) = self.url.strip_prefix(CPYTHON_DOWNLOADS_URL_PREFIX) {
1539 let effective_mirror = effective_cpython_mirror(astral_mirror_url);
1540 let mirror_url = DisplaySafeUrl::parse(
1541 format!("{}/{}", effective_mirror.trim_end_matches('/'), suffix).as_str(),
1542 )?;
1543 if astral_mirror_url.is_some() {
1545 return Ok(vec![mirror_url]);
1546 }
1547 let canonical_url = DisplaySafeUrl::parse(&self.url)?;
1549 return Ok(vec![mirror_url, canonical_url]);
1550 }
1551 }
1552
1553 LenientImplementationName::Known(ImplementationName::PyPy) => {
1554 if let Some(mirror) = pypy_install_mirror {
1555 let Some(suffix) = self.url.strip_prefix("https://downloads.python.org/pypy/")
1556 else {
1557 return Err(Error::Mirror(
1558 EnvVars::UV_PYPY_INSTALL_MIRROR,
1559 self.url.to_string(),
1560 ));
1561 };
1562 return Ok(vec![DisplaySafeUrl::parse(
1563 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1564 )?]);
1565 }
1566 }
1567
1568 _ => {}
1569 }
1570
1571 Ok(vec![DisplaySafeUrl::parse(&self.url)?])
1572 }
1573}
1574
1575fn parse_json_downloads(
1576 json_downloads: HashMap<String, JsonPythonDownload>,
1577) -> Vec<ManagedPythonDownload> {
1578 json_downloads
1579 .into_iter()
1580 .filter_map(|(key, entry)| {
1581 let implementation = match entry.name.as_str() {
1582 "cpython" => LenientImplementationName::Known(ImplementationName::CPython),
1583 "pypy" => LenientImplementationName::Known(ImplementationName::PyPy),
1584 "graalpy" => LenientImplementationName::Known(ImplementationName::GraalPy),
1585 _ => LenientImplementationName::Unknown(entry.name.clone()),
1586 };
1587
1588 let arch_str = match entry.arch.family.as_str() {
1589 "armv5tel" => "armv5te".to_string(),
1590 "riscv64" => "riscv64gc".to_string(),
1594 value => value.to_string(),
1595 };
1596
1597 let arch_str = if let Some(variant) = entry.arch.variant {
1598 format!("{arch_str}_{variant}")
1599 } else {
1600 arch_str
1601 };
1602
1603 let arch = match Arch::from_str(&arch_str) {
1604 Ok(arch) => arch,
1605 Err(e) => {
1606 debug!("Skipping entry {key}: Invalid arch '{arch_str}' - {e}");
1607 return None;
1608 }
1609 };
1610
1611 let os = match Os::from_str(&entry.os) {
1612 Ok(os) => os,
1613 Err(e) => {
1614 debug!("Skipping entry {}: Invalid OS '{}' - {}", key, entry.os, e);
1615 return None;
1616 }
1617 };
1618
1619 let libc = match Libc::from_str(&entry.libc) {
1620 Ok(libc) => libc,
1621 Err(e) => {
1622 debug!(
1623 "Skipping entry {}: Invalid libc '{}' - {}",
1624 key, entry.libc, e
1625 );
1626 return None;
1627 }
1628 };
1629
1630 let variant = match entry
1631 .variant
1632 .as_deref()
1633 .map(PythonVariant::from_str)
1634 .transpose()
1635 {
1636 Ok(Some(variant)) => variant,
1637 Ok(None) => PythonVariant::default(),
1638 Err(()) => {
1639 debug!(
1640 "Skipping entry {key}: Unknown python variant - {}",
1641 entry.variant.unwrap_or_default()
1642 );
1643 return None;
1644 }
1645 };
1646
1647 let version_str = format!(
1648 "{}.{}.{}{}",
1649 entry.major,
1650 entry.minor,
1651 entry.patch,
1652 entry.prerelease.as_deref().unwrap_or_default()
1653 );
1654
1655 let version = match PythonVersion::from_str(&version_str) {
1656 Ok(version) => version,
1657 Err(e) => {
1658 debug!("Skipping entry {key}: Invalid version '{version_str}' - {e}");
1659 return None;
1660 }
1661 };
1662
1663 let url = Cow::Owned(entry.url);
1664 let sha256 = entry.sha256.map(Cow::Owned);
1665 let build = entry
1666 .build
1667 .map(|s| Box::leak(s.into_boxed_str()) as &'static str);
1668
1669 Some(ManagedPythonDownload {
1670 key: PythonInstallationKey::new_from_version(
1671 implementation,
1672 &version,
1673 Platform::new(os, arch, libc),
1674 variant,
1675 ),
1676 url,
1677 sha256,
1678 build,
1679 })
1680 })
1681 .sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
1682 .collect()
1683}
1684
1685impl Error {
1686 pub(crate) fn from_reqwest(
1687 url: DisplaySafeUrl,
1688 err: reqwest::Error,
1689 retries: Option<u32>,
1690 start: Instant,
1691 ) -> Self {
1692 let err = Self::NetworkError(url, WrappedReqwestError::from(err));
1693 if let Some(retries) = retries {
1694 Self::NetworkErrorWithRetries {
1695 err: Box::new(err),
1696 retries,
1697 duration: start.elapsed(),
1698 }
1699 } else {
1700 err
1701 }
1702 }
1703
1704 pub(crate) fn from_reqwest_middleware(
1705 url: DisplaySafeUrl,
1706 err: reqwest_middleware::Error,
1707 ) -> Self {
1708 match err {
1709 reqwest_middleware::Error::Middleware(error) => {
1710 Self::NetworkMiddlewareError(url, error)
1711 }
1712 reqwest_middleware::Error::Reqwest(error) => {
1713 Self::NetworkError(url, WrappedReqwestError::from(error))
1714 }
1715 }
1716 }
1717}
1718
1719impl Display for ManagedPythonDownload {
1720 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1721 write!(f, "{}", self.key)
1722 }
1723}
1724
1725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1726pub enum Direction {
1727 Download,
1728 Extract,
1729}
1730
1731impl Direction {
1732 fn as_str(&self) -> &str {
1733 match self {
1734 Self::Download => "download",
1735 Self::Extract => "extract",
1736 }
1737 }
1738}
1739
1740impl Display for Direction {
1741 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1742 f.write_str(self.as_str())
1743 }
1744}
1745
1746pub trait Reporter: Send + Sync {
1747 fn on_request_start(
1748 &self,
1749 direction: Direction,
1750 name: &PythonInstallationKey,
1751 size: Option<u64>,
1752 ) -> usize;
1753 fn on_request_progress(&self, id: usize, inc: u64);
1754 fn on_request_complete(&self, direction: Direction, id: usize);
1755}
1756
1757struct ProgressReader<'a, R> {
1759 reader: R,
1760 index: usize,
1761 reporter: &'a dyn Reporter,
1762}
1763
1764impl<'a, R> ProgressReader<'a, R> {
1765 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1767 Self {
1768 reader,
1769 index,
1770 reporter,
1771 }
1772 }
1773}
1774
1775impl<R> AsyncRead for ProgressReader<'_, R>
1776where
1777 R: AsyncRead + Unpin,
1778{
1779 fn poll_read(
1780 mut self: Pin<&mut Self>,
1781 cx: &mut Context<'_>,
1782 buf: &mut ReadBuf<'_>,
1783 ) -> Poll<io::Result<()>> {
1784 Pin::new(&mut self.as_mut().reader)
1785 .poll_read(cx, buf)
1786 .map_ok(|()| {
1787 self.reporter
1788 .on_request_progress(self.index, buf.filled().len() as u64);
1789 })
1790 }
1791}
1792
1793async fn read_url(
1795 url: &DisplaySafeUrl,
1796 client: &BaseClient,
1797) -> Result<(impl AsyncRead + Unpin, Option<u64>), Error> {
1798 if url.scheme() == "file" {
1799 let path = url
1801 .to_file_path()
1802 .map_err(|()| Error::InvalidFileUrl(url.to_string()))?;
1803
1804 let size = fs_err::tokio::metadata(&path).await?.len();
1805 let reader = fs_err::tokio::File::open(&path).await?;
1806
1807 Ok((Either::Left(reader), Some(size)))
1808 } else {
1809 let start = Instant::now();
1810 let response = client
1811 .for_host(url)
1812 .get(Url::from(url.clone()))
1813 .send()
1814 .await
1815 .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?;
1816
1817 let retry_count = response
1818 .extensions()
1819 .get::<reqwest_retry::RetryCount>()
1820 .map(|retries| retries.value());
1821
1822 let response = response
1824 .error_for_status()
1825 .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count, start))?;
1826
1827 let size = response.content_length();
1828 let stream = response
1829 .bytes_stream()
1830 .map_err(io::Error::other)
1831 .into_async_read();
1832
1833 Ok((Either::Right(stream.compat()), size))
1834 }
1835}
1836
1837#[cfg(test)]
1838mod tests {
1839 use std::collections::HashSet;
1840
1841 use crate::PythonVariant;
1842 use crate::implementation::LenientImplementationName;
1843 use crate::installation::PythonInstallationKey;
1844 use uv_platform::{Arch, Libc, Os, Platform};
1845
1846 use super::*;
1847
1848 #[test]
1850 fn test_python_download_request_from_str_complete() {
1851 let request = PythonDownloadRequest::from_str("cpython-3.12.0-linux-x86_64-gnu")
1852 .expect("Test request should be parsed");
1853
1854 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1855 assert_eq!(
1856 request.version,
1857 Some(VersionRequest::from_str("3.12.0").unwrap())
1858 );
1859 assert_eq!(
1860 request.os,
1861 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1862 );
1863 assert_eq!(
1864 request.arch,
1865 Some(ArchRequest::Explicit(Arch::new(
1866 target_lexicon::Architecture::X86_64,
1867 None
1868 )))
1869 );
1870 assert_eq!(
1871 request.libc,
1872 Some(Libc::Some(target_lexicon::Environment::Gnu))
1873 );
1874 }
1875
1876 #[test]
1878 fn test_python_download_request_from_str_with_any() {
1879 let request = PythonDownloadRequest::from_str("any-3.11-any-x86_64-any")
1880 .expect("Test request should be parsed");
1881
1882 assert_eq!(request.implementation, None);
1883 assert_eq!(
1884 request.version,
1885 Some(VersionRequest::from_str("3.11").unwrap())
1886 );
1887 assert_eq!(request.os, None);
1888 assert_eq!(
1889 request.arch,
1890 Some(ArchRequest::Explicit(Arch::new(
1891 target_lexicon::Architecture::X86_64,
1892 None
1893 )))
1894 );
1895 assert_eq!(request.libc, None);
1896 }
1897
1898 #[test]
1900 fn test_python_download_request_from_str_missing_segment() {
1901 let request =
1902 PythonDownloadRequest::from_str("pypy-linux").expect("Test request should be parsed");
1903
1904 assert_eq!(request.implementation, Some(ImplementationName::PyPy));
1905 assert_eq!(request.version, None);
1906 assert_eq!(
1907 request.os,
1908 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1909 );
1910 assert_eq!(request.arch, None);
1911 assert_eq!(request.libc, None);
1912 }
1913
1914 #[test]
1915 fn test_python_download_request_from_str_version_only() {
1916 let request =
1917 PythonDownloadRequest::from_str("3.10.5").expect("Test request should be parsed");
1918
1919 assert_eq!(request.implementation, None);
1920 assert_eq!(
1921 request.version,
1922 Some(VersionRequest::from_str("3.10.5").unwrap())
1923 );
1924 assert_eq!(request.os, None);
1925 assert_eq!(request.arch, None);
1926 assert_eq!(request.libc, None);
1927 }
1928
1929 #[test]
1930 fn test_python_download_request_from_str_implementation_only() {
1931 let request =
1932 PythonDownloadRequest::from_str("cpython").expect("Test request should be parsed");
1933
1934 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1935 assert_eq!(request.version, None);
1936 assert_eq!(request.os, None);
1937 assert_eq!(request.arch, None);
1938 assert_eq!(request.libc, None);
1939 }
1940
1941 #[test]
1943 fn test_python_download_request_from_str_os_arch() {
1944 let request = PythonDownloadRequest::from_str("windows-x86_64")
1945 .expect("Test request should be parsed");
1946
1947 assert_eq!(request.implementation, None);
1948 assert_eq!(request.version, None);
1949 assert_eq!(
1950 request.os,
1951 Some(Os::new(target_lexicon::OperatingSystem::Windows))
1952 );
1953 assert_eq!(
1954 request.arch,
1955 Some(ArchRequest::Explicit(Arch::new(
1956 target_lexicon::Architecture::X86_64,
1957 None
1958 )))
1959 );
1960 assert_eq!(request.libc, None);
1961 }
1962
1963 #[test]
1965 fn test_python_download_request_from_str_prerelease() {
1966 let request = PythonDownloadRequest::from_str("cpython-3.13.0rc1")
1967 .expect("Test request should be parsed");
1968
1969 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1970 assert_eq!(
1971 request.version,
1972 Some(VersionRequest::from_str("3.13.0rc1").unwrap())
1973 );
1974 assert_eq!(request.os, None);
1975 assert_eq!(request.arch, None);
1976 assert_eq!(request.libc, None);
1977 }
1978
1979 #[test]
1981 fn test_python_download_request_from_str_too_many_parts() {
1982 let result = PythonDownloadRequest::from_str("cpython-3.12-linux-x86_64-gnu-extra");
1983
1984 assert!(matches!(result, Err(Error::TooManyParts(_))));
1985 }
1986
1987 #[test]
1989 fn test_python_download_request_from_str_empty() {
1990 let result = PythonDownloadRequest::from_str("");
1991
1992 assert!(matches!(result, Err(Error::EmptyRequest)), "{result:?}");
1993 }
1994
1995 #[test]
1997 fn test_python_download_request_from_str_all_any() {
1998 let request = PythonDownloadRequest::from_str("any-any-any-any-any")
1999 .expect("Test request should be parsed");
2000
2001 assert_eq!(request.implementation, None);
2002 assert_eq!(request.version, None);
2003 assert_eq!(request.os, None);
2004 assert_eq!(request.arch, None);
2005 assert_eq!(request.libc, None);
2006 }
2007
2008 #[test]
2010 fn test_python_download_request_from_str_case_insensitive_any() {
2011 let request = PythonDownloadRequest::from_str("ANY-3.11-Any-x86_64-aNy")
2012 .expect("Test request should be parsed");
2013
2014 assert_eq!(request.implementation, None);
2015 assert_eq!(
2016 request.version,
2017 Some(VersionRequest::from_str("3.11").unwrap())
2018 );
2019 assert_eq!(request.os, None);
2020 assert_eq!(
2021 request.arch,
2022 Some(ArchRequest::Explicit(Arch::new(
2023 target_lexicon::Architecture::X86_64,
2024 None
2025 )))
2026 );
2027 assert_eq!(request.libc, None);
2028 }
2029
2030 #[test]
2032 fn test_python_download_request_from_str_invalid_leading_segment() {
2033 let result = PythonDownloadRequest::from_str("foobar-3.14-windows");
2034
2035 assert!(
2036 matches!(result, Err(Error::ImplementationError(_))),
2037 "{result:?}"
2038 );
2039 }
2040
2041 #[test]
2043 fn test_python_download_request_from_str_out_of_order() {
2044 let result = PythonDownloadRequest::from_str("3.12-cpython");
2045
2046 assert!(
2047 matches!(result, Err(Error::InvalidRequestPlatform(_))),
2048 "{result:?}"
2049 );
2050 }
2051
2052 #[test]
2054 fn test_python_download_request_from_str_too_many_any() {
2055 let result = PythonDownloadRequest::from_str("any-any-any-any-any-any");
2056
2057 assert!(matches!(result, Err(Error::TooManyParts(_))));
2058 }
2059
2060 #[tokio::test]
2062 async fn test_python_download_request_build_filtering() {
2063 let mut request = PythonDownloadRequest::default()
2064 .with_version(VersionRequest::from_str("3.12").unwrap())
2065 .with_implementation(ImplementationName::CPython);
2066 request.build = Some("20240814".to_string());
2067
2068 let client = uv_client::BaseClientBuilder::default()
2069 .build()
2070 .expect("failed to build base client");
2071 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
2072
2073 let downloads: Vec<_> = download_list
2074 .iter_all()
2075 .filter(|d| request.satisfied_by_download(d))
2076 .collect();
2077
2078 assert!(
2079 !downloads.is_empty(),
2080 "Should find at least one matching download"
2081 );
2082 for download in downloads {
2083 assert_eq!(download.build(), Some("20240814"));
2084 }
2085 }
2086
2087 #[tokio::test]
2089 async fn test_python_download_request_invalid_build() {
2090 let mut request = PythonDownloadRequest::default()
2092 .with_version(VersionRequest::from_str("3.12").unwrap())
2093 .with_implementation(ImplementationName::CPython);
2094 request.build = Some("99999999".to_string());
2095
2096 let client = uv_client::BaseClientBuilder::default()
2097 .build()
2098 .expect("failed to build base client");
2099 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
2100
2101 let downloads: Vec<_> = download_list
2103 .iter_all()
2104 .filter(|d| request.satisfied_by_download(d))
2105 .collect();
2106
2107 assert_eq!(downloads.len(), 0);
2108 }
2109
2110 #[test]
2111 fn upgrade_request_native_defaults() {
2112 let request = PythonDownloadRequest::default()
2113 .with_implementation(ImplementationName::CPython)
2114 .with_version(VersionRequest::MajorMinorPatch(
2115 3,
2116 13,
2117 1,
2118 PythonVariant::Default,
2119 ))
2120 .with_os(Os::from_str("linux").unwrap())
2121 .with_arch(Arch::from_str("x86_64").unwrap())
2122 .with_libc(Libc::from_str("gnu").unwrap())
2123 .with_prereleases(false);
2124
2125 let host = Platform::new(
2126 Os::from_str("linux").unwrap(),
2127 Arch::from_str("x86_64").unwrap(),
2128 Libc::from_str("gnu").unwrap(),
2129 );
2130
2131 assert_eq!(
2132 request
2133 .clone()
2134 .unset_defaults_for_host(&host)
2135 .without_patch()
2136 .simplified_display()
2137 .as_deref(),
2138 Some("3.13")
2139 );
2140 }
2141
2142 #[test]
2143 fn upgrade_request_preserves_variant() {
2144 let request = PythonDownloadRequest::default()
2145 .with_implementation(ImplementationName::CPython)
2146 .with_version(VersionRequest::MajorMinorPatch(
2147 3,
2148 13,
2149 0,
2150 PythonVariant::Freethreaded,
2151 ))
2152 .with_os(Os::from_str("linux").unwrap())
2153 .with_arch(Arch::from_str("x86_64").unwrap())
2154 .with_libc(Libc::from_str("gnu").unwrap())
2155 .with_prereleases(false);
2156
2157 let host = Platform::new(
2158 Os::from_str("linux").unwrap(),
2159 Arch::from_str("x86_64").unwrap(),
2160 Libc::from_str("gnu").unwrap(),
2161 );
2162
2163 assert_eq!(
2164 request
2165 .clone()
2166 .unset_defaults_for_host(&host)
2167 .without_patch()
2168 .simplified_display()
2169 .as_deref(),
2170 Some("3.13+freethreaded")
2171 );
2172 }
2173
2174 #[test]
2175 fn upgrade_request_preserves_non_default_platform() {
2176 let request = PythonDownloadRequest::default()
2177 .with_implementation(ImplementationName::CPython)
2178 .with_version(VersionRequest::MajorMinorPatch(
2179 3,
2180 12,
2181 4,
2182 PythonVariant::Default,
2183 ))
2184 .with_os(Os::from_str("linux").unwrap())
2185 .with_arch(Arch::from_str("aarch64").unwrap())
2186 .with_libc(Libc::from_str("gnu").unwrap())
2187 .with_prereleases(false);
2188
2189 let host = Platform::new(
2190 Os::from_str("linux").unwrap(),
2191 Arch::from_str("x86_64").unwrap(),
2192 Libc::from_str("gnu").unwrap(),
2193 );
2194
2195 assert_eq!(
2196 request
2197 .clone()
2198 .unset_defaults_for_host(&host)
2199 .without_patch()
2200 .simplified_display()
2201 .as_deref(),
2202 Some("3.12-aarch64")
2203 );
2204 }
2205
2206 #[test]
2207 fn upgrade_request_preserves_custom_implementation() {
2208 let request = PythonDownloadRequest::default()
2209 .with_implementation(ImplementationName::PyPy)
2210 .with_version(VersionRequest::MajorMinorPatch(
2211 3,
2212 10,
2213 5,
2214 PythonVariant::Default,
2215 ))
2216 .with_os(Os::from_str("linux").unwrap())
2217 .with_arch(Arch::from_str("x86_64").unwrap())
2218 .with_libc(Libc::from_str("gnu").unwrap())
2219 .with_prereleases(false);
2220
2221 let host = Platform::new(
2222 Os::from_str("linux").unwrap(),
2223 Arch::from_str("x86_64").unwrap(),
2224 Libc::from_str("gnu").unwrap(),
2225 );
2226
2227 assert_eq!(
2228 request
2229 .clone()
2230 .unset_defaults_for_host(&host)
2231 .without_patch()
2232 .simplified_display()
2233 .as_deref(),
2234 Some("pypy-3.10")
2235 );
2236 }
2237
2238 #[test]
2239 fn simplified_display_returns_none_when_empty() {
2240 let request = PythonDownloadRequest::default()
2241 .fill_platform()
2242 .expect("should populate defaults");
2243
2244 let host = Platform::from_env().expect("host platform");
2245
2246 assert_eq!(
2247 request.unset_defaults_for_host(&host).simplified_display(),
2248 None
2249 );
2250 }
2251
2252 #[test]
2253 fn simplified_display_omits_environment_arch() {
2254 let mut request = PythonDownloadRequest::default()
2255 .with_version(VersionRequest::MajorMinor(3, 12, PythonVariant::Default))
2256 .with_os(Os::from_str("linux").unwrap())
2257 .with_libc(Libc::from_str("gnu").unwrap());
2258
2259 request.arch = Some(ArchRequest::Environment(Arch::from_str("x86_64").unwrap()));
2260
2261 let host = Platform::new(
2262 Os::from_str("linux").unwrap(),
2263 Arch::from_str("aarch64").unwrap(),
2264 Libc::from_str("gnu").unwrap(),
2265 );
2266
2267 assert_eq!(
2268 request
2269 .unset_defaults_for_host(&host)
2270 .simplified_display()
2271 .as_deref(),
2272 Some("3.12")
2273 );
2274 }
2275
2276 fn cpython_download_for_url(url: &'static str) -> ManagedPythonDownload {
2277 let key = PythonInstallationKey::new(
2278 LenientImplementationName::Known(crate::implementation::ImplementationName::CPython),
2279 3,
2280 12,
2281 4,
2282 None,
2283 Platform::new(
2284 Os::from_str("linux").unwrap(),
2285 Arch::from_str("x86_64").unwrap(),
2286 Libc::from_str("gnu").unwrap(),
2287 ),
2288 crate::PythonVariant::default(),
2289 );
2290
2291 ManagedPythonDownload {
2292 key,
2293 url: Cow::Borrowed(url),
2294 sha256: Some(Cow::Borrowed("abc123")),
2295 build: Some("20240713"),
2296 }
2297 }
2298
2299 #[test]
2300 fn test_cpython_download_urls_custom_astral_mirror() {
2301 let download = cpython_download_for_url(
2302 "https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz",
2303 );
2304
2305 let urls = download
2306 .download_urls_with_astral_mirror(
2307 None,
2308 None,
2309 Some("https://nexus.example.com/repository/releases.astral.sh/"),
2310 )
2311 .expect("download URLs should be valid");
2312 let urls = urls
2313 .into_iter()
2314 .map(|url| url.to_string())
2315 .collect::<Vec<_>>();
2316 assert_eq!(
2317 urls,
2318 vec![
2319 "https://nexus.example.com/repository/releases.astral.sh/github/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz"
2320 .to_string(),
2321 ]
2322 );
2323 }
2324
2325 #[test]
2326 fn test_cpython_specific_mirror_takes_precedence_over_astral_mirror() {
2327 let download = cpython_download_for_url(
2328 "https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz",
2329 );
2330
2331 let urls = download
2332 .download_urls_with_astral_mirror(
2333 Some("https://python-mirror.example.com/releases/"),
2334 None,
2335 Some("https://nexus.example.com/repository/releases.astral.sh/"),
2336 )
2337 .expect("download URLs should be valid");
2338 let urls = urls
2339 .into_iter()
2340 .map(|url| url.to_string())
2341 .collect::<Vec<_>>();
2342 assert_eq!(
2343 urls,
2344 vec![
2345 "https://python-mirror.example.com/releases/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz"
2346 .to_string(),
2347 ]
2348 );
2349 }
2350
2351 #[test]
2352 fn test_cpython_download_urls_empty_astral_mirror_uses_default() {
2353 let download = cpython_download_for_url(
2354 "https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz",
2355 );
2356
2357 let default_urls = download
2358 .download_urls_with_astral_mirror(None, None, None)
2359 .expect("download URLs should be valid");
2360 let empty_urls = download
2361 .download_urls_with_astral_mirror(None, None, Some(""))
2362 .expect("download URLs should be valid");
2363
2364 assert_eq!(default_urls, empty_urls);
2365 }
2366
2367 #[test]
2370 fn test_should_try_next_url_hash_mismatch() {
2371 let err = Error::HashMismatch {
2372 installation: "cpython-3.12.0".to_string(),
2373 expected: "abc".to_string(),
2374 actual: "def".to_string(),
2375 };
2376 assert!(!err.should_try_next_url());
2377 }
2378
2379 #[test]
2382 fn test_should_try_next_url_extract_error_filesystem() {
2383 let err = Error::ExtractError(
2384 "archive.tar.gz".to_string(),
2385 uv_extract::Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, "")),
2386 );
2387 assert!(!err.should_try_next_url());
2388 }
2389
2390 #[test]
2393 fn test_should_try_next_url_io_error_filesystem() {
2394 let err = Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, ""));
2395 assert!(!err.should_try_next_url());
2396 }
2397
2398 #[test]
2401 fn test_should_try_next_url_io_error_network() {
2402 let err = Error::Io(io::Error::new(io::ErrorKind::ConnectionReset, ""));
2403 assert!(err.should_try_next_url());
2404 }
2405
2406 #[test]
2409 fn test_should_try_next_url_network_error_404() {
2410 let url =
2411 DisplaySafeUrl::from_str("https://releases.astral.sh/python/cpython-3.12.0.tar.gz")
2412 .unwrap();
2413 let wrapped = WrappedReqwestError::with_problem_details(
2416 reqwest_middleware::Error::Middleware(anyhow::anyhow!("404 Not Found")),
2417 None,
2418 );
2419 let err = Error::NetworkError(url, wrapped);
2420 assert!(err.should_try_next_url());
2421 }
2422
2423 #[test]
2426 fn embedded_download_versions_convert_to_version_requests() {
2427 let downloads = ManagedPythonDownloadList::new_only_embedded()
2428 .expect("embedded download metadata should load");
2429
2430 let unique_versions: HashSet<PythonVersion> = downloads
2431 .iter_all()
2432 .map(ManagedPythonDownload::python_version)
2433 .collect();
2434
2435 for version in &unique_versions {
2436 let _ = VersionRequest::from(version);
2437 }
2438 }
2439}