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 os: Option<Os>,
232 arch: Option<ArchRequest>,
233 libc: Option<Libc>,
234}
235
236impl PlatformRequest {
237 pub(crate) 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 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 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(crate) fn implementation(&self) -> Option<&ImplementationName> {
447 self.implementation.as_ref()
448 }
449
450 pub(crate) 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 libc(&self) -> Option<&Libc> {
459 self.libc.as_ref()
460 }
461
462 pub fn take_version(&mut self) -> Option<VersionRequest> {
463 self.version.take()
464 }
465
466 #[must_use]
469 pub(crate) fn unset_defaults(self) -> Self {
470 let request = self.unset_non_platform_defaults();
471
472 if let Ok(host) = Platform::from_env() {
473 request.unset_platform_defaults(&host)
474 } else {
475 request
476 }
477 }
478
479 fn unset_non_platform_defaults(mut self) -> Self {
480 self.implementation = self
481 .implementation
482 .filter(|implementation_name| *implementation_name != ImplementationName::default());
483
484 self.version = self
485 .version
486 .filter(|version| !matches!(version, VersionRequest::Any | VersionRequest::Default));
487
488 self.arch = self
490 .arch
491 .filter(|arch| !matches!(arch, ArchRequest::Environment(_)));
492
493 self
494 }
495
496 #[cfg(test)]
497 fn unset_defaults_for_host(self, host: &Platform) -> Self {
498 self.unset_non_platform_defaults()
499 .unset_platform_defaults(host)
500 }
501
502 fn unset_platform_defaults(mut self, host: &Platform) -> Self {
503 self.os = self.os.filter(|os| *os != host.os);
504
505 self.libc = self.libc.filter(|libc| *libc != host.libc);
506
507 self.arch = self
508 .arch
509 .filter(|arch| !matches!(arch, ArchRequest::Explicit(explicit_arch) if *explicit_arch == host.arch));
510
511 self
512 }
513
514 #[must_use]
516 pub(crate) fn without_patch(mut self) -> Self {
517 self.version = self.version.take().map(VersionRequest::only_minor);
518 self.prereleases = None;
519 self.build = None;
520 self
521 }
522
523 pub(crate) fn simplified_display(self) -> Option<String> {
528 let parts = [
529 self.implementation
530 .map(|implementation| implementation.to_string()),
531 self.version.map(|version| version.to_string()),
532 self.os.map(|os| os.to_string()),
533 self.arch.map(|arch| arch.to_string()),
534 self.libc.map(|libc| libc.to_string()),
535 ];
536
537 let joined = parts.into_iter().flatten().collect::<Vec<_>>().join("-");
538
539 if joined.is_empty() {
540 None
541 } else {
542 Some(joined)
543 }
544 }
545
546 pub fn satisfied_by_key(&self, key: &PythonInstallationKey) -> bool {
548 let request = PlatformRequest {
550 os: self.os,
551 arch: self.arch,
552 libc: self.libc,
553 };
554 if !request.matches(key.platform()) {
555 return false;
556 }
557
558 if let Some(implementation) = &self.implementation {
559 if key.implementation != LenientImplementationName::from(*implementation) {
560 return false;
561 }
562 }
563 if !self.allows_prereleases() && key.prerelease.is_some() {
565 return false;
566 }
567 if let Some(version) = &self.version {
568 if !version.matches_major_minor_patch_prerelease(
569 key.major,
570 key.minor,
571 key.patch,
572 key.prerelease,
573 ) {
574 return false;
575 }
576 if let Some(variant) = version.variant() {
577 if variant != key.variant {
578 return false;
579 }
580 }
581 }
582 true
583 }
584
585 fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool {
587 if !self.satisfied_by_key(download.key()) {
589 return false;
590 }
591
592 if let Some(ref requested_build) = self.build {
594 let Some(download_build) = download.build() else {
595 debug!(
596 "Skipping download `{}`: a build version was requested but is not available for this download",
597 download
598 );
599 return false;
600 };
601
602 if download_build != requested_build {
603 debug!(
604 "Skipping download `{}`: requested build version `{}` does not match download build version `{}`",
605 download, requested_build, download_build
606 );
607 return false;
608 }
609 }
610
611 true
612 }
613
614 pub(crate) fn allows_prereleases(&self) -> bool {
616 self.prereleases.unwrap_or_else(|| {
617 self.version
618 .as_ref()
619 .is_some_and(VersionRequest::allows_prereleases)
620 })
621 }
622
623 pub(crate) fn allows_debug(&self) -> bool {
625 self.version.as_ref().is_some_and(VersionRequest::is_debug)
626 }
627
628 pub(crate) fn allows_alternative_implementations(&self) -> bool {
630 self.implementation
631 .is_some_and(|implementation| !matches!(implementation, ImplementationName::CPython))
632 || self.os.is_some_and(|os| os.is_emscripten())
633 }
634
635 pub(crate) fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
636 let executable = interpreter.sys_executable().display();
637 if let Some(version) = self.version() {
638 if !version.matches_interpreter(interpreter) {
639 let interpreter_version = interpreter.python_version();
640 debug!(
641 "Skipping interpreter at `{executable}`: version `{interpreter_version}` does not match request `{version}`"
642 );
643 return false;
644 }
645 }
646 let platform = self.platform();
647 let interpreter_platform = Platform::from(interpreter.platform());
648 if !platform.matches(&interpreter_platform) {
649 debug!(
650 "Skipping interpreter at `{executable}`: platform `{interpreter_platform}` does not match request `{platform}`",
651 );
652 return false;
653 }
654 if let Some(implementation) = self.implementation() {
655 if !implementation.matches_interpreter(interpreter) {
656 debug!(
657 "Skipping interpreter at `{executable}`: implementation `{}` does not match request `{implementation}`",
658 interpreter.implementation_name(),
659 );
660 return false;
661 }
662 }
663 true
664 }
665
666 pub(crate) fn platform(&self) -> PlatformRequest {
668 PlatformRequest {
669 os: self.os,
670 arch: self.arch,
671 libc: self.libc,
672 }
673 }
674}
675
676impl TryFrom<&PythonInstallationKey> for PythonDownloadRequest {
677 type Error = LenientImplementationName;
678
679 fn try_from(key: &PythonInstallationKey) -> Result<Self, Self::Error> {
680 let implementation = match key.implementation().into_owned() {
681 LenientImplementationName::Known(name) => name,
682 unknown @ LenientImplementationName::Unknown(_) => return Err(unknown),
683 };
684
685 Ok(Self::new(
686 Some(VersionRequest::MajorMinor(
687 key.major(),
688 key.minor(),
689 *key.variant(),
690 )),
691 Some(implementation),
692 Some(ArchRequest::Explicit(*key.arch())),
693 Some(*key.os()),
694 Some(*key.libc()),
695 Some(key.prerelease().is_some()),
696 ))
697 }
698}
699
700impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
701 fn from(installation: &ManagedPythonInstallation) -> Self {
702 let key = installation.key();
703 Self::new(
704 Some(VersionRequest::from(&key.version())),
705 match &key.implementation {
706 LenientImplementationName::Known(implementation) => Some(*implementation),
707 LenientImplementationName::Unknown(name) => unreachable!(
708 "Managed Python installations are expected to always have known implementation names, found {name}"
709 ),
710 },
711 Some(ArchRequest::Explicit(*key.arch())),
712 Some(*key.os()),
713 Some(*key.libc()),
714 Some(key.prerelease.is_some()),
715 )
716 }
717}
718
719impl Display for PythonDownloadRequest {
720 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
721 let mut parts = Vec::new();
722 if let Some(implementation) = self.implementation {
723 parts.push(implementation.to_string());
724 } else {
725 parts.push("any".to_string());
726 }
727 if let Some(version) = &self.version {
728 parts.push(version.to_string());
729 } else {
730 parts.push("any".to_string());
731 }
732 if let Some(os) = &self.os {
733 parts.push(os.to_string());
734 } else {
735 parts.push("any".to_string());
736 }
737 if let Some(arch) = self.arch {
738 parts.push(arch.to_string());
739 } else {
740 parts.push("any".to_string());
741 }
742 if let Some(libc) = self.libc {
743 parts.push(libc.to_string());
744 } else {
745 parts.push("any".to_string());
746 }
747 write!(f, "{}", parts.join("-"))
748 }
749}
750impl FromStr for PythonDownloadRequest {
751 type Err = Error;
752
753 fn from_str(s: &str) -> Result<Self, Self::Err> {
754 #[derive(Debug, Clone)]
755 enum Position {
756 Start,
757 Implementation,
758 Version,
759 Os,
760 Arch,
761 Libc,
762 End,
763 }
764
765 impl Position {
766 fn next(&self) -> Self {
767 match self {
768 Self::Start => Self::Implementation,
769 Self::Implementation => Self::Version,
770 Self::Version => Self::Os,
771 Self::Os => Self::Arch,
772 Self::Arch => Self::Libc,
773 Self::Libc => Self::End,
774 Self::End => Self::End,
775 }
776 }
777 }
778
779 #[derive(Debug)]
780 struct State<'a, P: Iterator<Item = &'a str>> {
781 parts: P,
782 part: Option<&'a str>,
783 position: Position,
784 error: Option<Error>,
785 count: usize,
786 }
787
788 impl<'a, P: Iterator<Item = &'a str>> State<'a, P> {
789 fn new(parts: P) -> Self {
790 Self {
791 parts,
792 part: None,
793 position: Position::Start,
794 error: None,
795 count: 0,
796 }
797 }
798
799 fn next_part(&mut self) {
800 self.next_position();
801 self.part = self.parts.next();
802 self.count += 1;
803 self.error.take();
804 }
805
806 fn next_position(&mut self) {
807 self.position = self.position.next();
808 }
809
810 fn record_err(&mut self, err: Error) {
811 self.error.get_or_insert(err);
814 }
815 }
816
817 if s.is_empty() {
818 return Err(Error::EmptyRequest);
819 }
820
821 let mut parts = s.split('-');
822
823 let mut implementation = None;
824 let mut version = None;
825 let mut os = None;
826 let mut arch = None;
827 let mut libc = None;
828
829 let mut state = State::new(parts.by_ref());
830 state.next_part();
831
832 while let Some(part) = state.part {
833 match state.position {
834 Position::Start => unreachable!("We start before the loop"),
835 Position::Implementation => {
836 if part.eq_ignore_ascii_case("any") {
837 state.next_part();
838 continue;
839 }
840 match ImplementationName::from_str(part) {
841 Ok(val) => {
842 implementation = Some(val);
843 state.next_part();
844 }
845 Err(err) => {
846 state.next_position();
847 state.record_err(err.into());
848 }
849 }
850 }
851 Position::Version => {
852 if part.eq_ignore_ascii_case("any") {
853 state.next_part();
854 continue;
855 }
856 match VersionRequest::from_str(part)
857 .map_err(|_| Error::InvalidPythonVersion(part.to_string()))
858 {
859 Ok(val) => {
861 version = Some(val);
862 state.next_part();
863 }
864 Err(err) => {
865 state.next_position();
866 state.record_err(err);
867 }
868 }
869 }
870 Position::Os => {
871 if part.eq_ignore_ascii_case("any") {
872 state.next_part();
873 continue;
874 }
875 match Os::from_str(part) {
876 Ok(val) => {
877 os = Some(val);
878 state.next_part();
879 }
880 Err(err) => {
881 state.next_position();
882 state.record_err(err.into());
883 }
884 }
885 }
886 Position::Arch => {
887 if part.eq_ignore_ascii_case("any") {
888 state.next_part();
889 continue;
890 }
891 match Arch::from_str(part) {
892 Ok(val) => {
893 arch = Some(ArchRequest::Explicit(val));
894 state.next_part();
895 }
896 Err(err) => {
897 state.next_position();
898 state.record_err(err.into());
899 }
900 }
901 }
902 Position::Libc => {
903 if part.eq_ignore_ascii_case("any") {
904 state.next_part();
905 continue;
906 }
907 match Libc::from_str(part) {
908 Ok(val) => {
909 libc = Some(val);
910 state.next_part();
911 }
912 Err(err) => {
913 state.next_position();
914 state.record_err(err.into());
915 }
916 }
917 }
918 Position::End => {
919 if state.count > 5 {
920 return Err(Error::TooManyParts(s.to_string()));
921 }
922
923 if let Some(err) = state.error {
930 return Err(err);
931 }
932 state.next_part();
933 }
934 }
935 }
936
937 Ok(Self::new(version, implementation, arch, os, libc, None))
938 }
939}
940
941const BUILTIN_PYTHON_DOWNLOADS_JSON: &[u8] =
942 include_bytes!(concat!(env!("OUT_DIR"), "/download-metadata-minified.json"));
943
944pub struct ManagedPythonDownloadList {
945 downloads: Vec<ManagedPythonDownload>,
946}
947
948#[derive(Debug, Deserialize, Clone)]
949struct JsonPythonDownload {
950 name: String,
951 arch: JsonArch,
952 os: String,
953 libc: String,
954 major: u8,
955 minor: u8,
956 patch: u8,
957 prerelease: Option<String>,
958 url: String,
959 sha256: Option<String>,
960 variant: Option<String>,
961 build: Option<String>,
962}
963
964#[derive(Debug, Deserialize, Clone)]
965struct JsonArch {
966 family: String,
967 variant: Option<String>,
968}
969
970#[derive(Debug, Clone)]
971pub enum DownloadResult {
972 AlreadyAvailable(PathBuf),
973 Fetched(PathBuf),
974}
975
976impl ManagedPythonDownloadList {
977 fn iter_all(&self) -> impl Iterator<Item = &ManagedPythonDownload> {
979 self.downloads.iter()
980 }
981
982 pub fn iter_matching(
984 &self,
985 request: &PythonDownloadRequest,
986 ) -> impl Iterator<Item = &ManagedPythonDownload> {
987 self.iter_all()
988 .filter(move |download| request.satisfied_by_download(download))
989 }
990
991 pub fn find(&self, request: &PythonDownloadRequest) -> Result<&ManagedPythonDownload, Error> {
996 if let Some(download) = self.iter_matching(request).next() {
997 return Ok(download);
998 }
999
1000 if !request.allows_prereleases() {
1001 if let Some(download) = self
1002 .iter_matching(&request.clone().with_prereleases(true))
1003 .next()
1004 {
1005 return Ok(download);
1006 }
1007 }
1008
1009 Err(Error::NoDownloadFound(request.clone()))
1010 }
1011
1012 pub async fn new(
1021 client: &BaseClient,
1022 python_downloads_json_url: Option<&str>,
1023 ) -> Result<Self, Error> {
1024 enum Source<'a> {
1029 BuiltIn,
1030 Path(Cow<'a, Path>),
1031 Http(DisplaySafeUrl),
1032 }
1033
1034 let json_source = if let Some(url_or_path) = python_downloads_json_url {
1035 if let Ok(url) = DisplaySafeUrl::parse(url_or_path) {
1036 match url.scheme() {
1037 "http" | "https" => Source::Http(url),
1038 "file" => Source::Path(Cow::Owned(
1039 url.to_file_path().or(Err(Error::InvalidUrlFormat(url)))?,
1040 )),
1041 _ => Source::Path(Cow::Borrowed(Path::new(url_or_path))),
1042 }
1043 } else {
1044 Source::Path(Cow::Borrowed(Path::new(url_or_path)))
1045 }
1046 } else {
1047 Source::BuiltIn
1048 };
1049
1050 let buf: Cow<'_, [u8]> = match json_source {
1051 Source::BuiltIn => BUILTIN_PYTHON_DOWNLOADS_JSON.into(),
1052 Source::Path(ref path) => fs_err::read(path.as_ref())?.into(),
1053 Source::Http(ref url) => fetch_bytes_from_url(client, url)
1054 .await
1055 .map_err(|e| Error::FetchingPythonDownloadsJSONError(url.to_string(), Box::new(e)))?
1056 .into(),
1057 };
1058 let json_downloads: HashMap<String, JsonPythonDownload> = serde_json::from_slice(&buf)
1059 .map_err(
1060 #[expect(clippy::zero_sized_map_values)]
1067 |e| {
1068 let source = match json_source {
1069 Source::BuiltIn => "EMBEDDED IN THE BINARY".to_owned(),
1070 Source::Path(path) => path.to_string_lossy().to_string(),
1071 Source::Http(url) => url.to_string(),
1072 };
1073 if let Ok(keys) =
1074 serde_json::from_slice::<HashMap<String, serde::de::IgnoredAny>>(&buf)
1075 && keys.contains_key("version")
1076 {
1077 Error::UnsupportedPythonDownloadsJSON(source)
1078 } else {
1079 Error::InvalidPythonDownloadsJSON(source, e)
1080 }
1081 },
1082 )?;
1083
1084 let result = parse_json_downloads(json_downloads);
1085 Ok(Self { downloads: result })
1086 }
1087
1088 pub fn new_only_embedded() -> Result<Self, Error> {
1091 let json_downloads: HashMap<String, JsonPythonDownload> =
1092 serde_json::from_slice(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| {
1093 Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_owned(), e)
1094 })?;
1095 let result = parse_json_downloads(json_downloads);
1096 Ok(Self { downloads: result })
1097 }
1098}
1099
1100async fn fetch_bytes_from_url(client: &BaseClient, url: &DisplaySafeUrl) -> Result<Vec<u8>, Error> {
1101 let (mut reader, size) = read_url(url, client).await?;
1102 let capacity = size.and_then(|s| s.try_into().ok()).unwrap_or(1_048_576);
1103 let mut buf = Vec::with_capacity(capacity);
1104 reader.read_to_end(&mut buf).await?;
1105 Ok(buf)
1106}
1107
1108impl ManagedPythonDownload {
1109 pub(crate) fn url(&self) -> &Cow<'static, str> {
1110 &self.url
1111 }
1112
1113 pub fn key(&self) -> &PythonInstallationKey {
1114 &self.key
1115 }
1116
1117 fn os(&self) -> &Os {
1118 self.key.os()
1119 }
1120
1121 pub(crate) fn sha256(&self) -> Option<&Cow<'static, str>> {
1122 self.sha256.as_ref()
1123 }
1124
1125 pub fn build(&self) -> Option<&'static str> {
1126 self.build
1127 }
1128
1129 #[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
1135 pub async fn fetch_with_retry(
1136 &self,
1137 client: &BaseClient,
1138 retry_policy: &ExponentialBackoff,
1139 installation_dir: &Path,
1140 scratch_dir: &Path,
1141 reinstall: bool,
1142 python_install_mirror: Option<&str>,
1143 pypy_install_mirror: Option<&str>,
1144 reporter: Option<&dyn Reporter>,
1145 ) -> Result<DownloadResult, Error> {
1146 let urls = self.download_urls(python_install_mirror, pypy_install_mirror)?;
1147 if urls.is_empty() {
1148 return Err(Error::NoPythonDownloadUrlFound);
1149 }
1150 fetch_with_url_fallback(&urls, *retry_policy, &format!("`{}`", self.key()), |url| {
1151 self.fetch_from_url(
1152 url,
1153 client,
1154 installation_dir,
1155 scratch_dir,
1156 reinstall,
1157 reporter,
1158 )
1159 })
1160 .await
1161 }
1162
1163 async fn fetch_from_url(
1165 &self,
1166 url: DisplaySafeUrl,
1167 client: &BaseClient,
1168 installation_dir: &Path,
1169 scratch_dir: &Path,
1170 reinstall: bool,
1171 reporter: Option<&dyn Reporter>,
1172 ) -> Result<DownloadResult, Error> {
1173 let path = installation_dir.join(self.key().to_string());
1174
1175 if !reinstall && path.is_dir() {
1177 return Ok(DownloadResult::AlreadyAvailable(path));
1178 }
1179
1180 let filename = url
1183 .path_segments()
1184 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1185 .next_back()
1186 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1187 .replace("%2B", "-");
1188 debug_assert!(
1189 filename
1190 .chars()
1191 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'),
1192 "Unexpected char in filename: {filename}"
1193 );
1194 let ext = SourceDistExtension::from_path(&filename)
1195 .map_err(|err| Error::MissingExtension(url.to_string(), err))?;
1196
1197 let temp_dir = tempfile::tempdir_in(scratch_dir).map_err(Error::DownloadDirError)?;
1198
1199 if let Some(python_builds_dir) =
1200 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).filter(|s| !s.is_empty())
1201 {
1202 let python_builds_dir = PathBuf::from(python_builds_dir);
1203 fs_err::create_dir_all(&python_builds_dir)?;
1204 let hash_prefix = match self.sha256.as_deref() {
1205 Some(sha) => {
1206 &sha[..9]
1208 }
1209 None => "none",
1210 };
1211 let target_cache_file = python_builds_dir.join(format!("{hash_prefix}-{filename}"));
1212
1213 let (reader, size): (Box<dyn AsyncRead + Unpin>, Option<u64>) =
1217 match fs_err::tokio::File::open(&target_cache_file).await {
1218 Ok(file) => {
1219 debug!(
1220 "Extracting existing `{}`",
1221 target_cache_file.simplified_display()
1222 );
1223 let size = file.metadata().await?.len();
1224 let reader = Box::new(tokio::io::BufReader::new(file));
1225 (reader, Some(size))
1226 }
1227 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1228 if client.connectivity().is_offline() {
1230 return Err(Error::OfflinePythonMissing {
1231 file: Box::new(self.key().clone()),
1232 url: Box::new(url.clone()),
1233 python_builds_dir,
1234 });
1235 }
1236
1237 self.download_archive(
1238 &url,
1239 client,
1240 reporter,
1241 &python_builds_dir,
1242 &target_cache_file,
1243 )
1244 .await?;
1245
1246 debug!("Extracting `{}`", target_cache_file.simplified_display());
1247 let file = fs_err::tokio::File::open(&target_cache_file).await?;
1248 let size = file.metadata().await?.len();
1249 let reader = Box::new(tokio::io::BufReader::new(file));
1250 (reader, Some(size))
1251 }
1252 Err(err) => return Err(err.into()),
1253 };
1254
1255 self.extract_reader(
1257 reader,
1258 temp_dir.path(),
1259 &filename,
1260 ext,
1261 size,
1262 reporter,
1263 Direction::Extract,
1264 )
1265 .await?;
1266 } else {
1267 debug!("Downloading {url}");
1269 debug!(
1270 "Extracting {filename} to temporary location: {}",
1271 temp_dir.path().simplified_display()
1272 );
1273
1274 let (reader, size) = read_url(&url, client).await?;
1275 self.extract_reader(
1276 reader,
1277 temp_dir.path(),
1278 &filename,
1279 ext,
1280 size,
1281 reporter,
1282 Direction::Download,
1283 )
1284 .await?;
1285 }
1286
1287 let mut extracted = match uv_extract::strip_component(temp_dir.path()) {
1289 Ok(top_level) => top_level,
1290 Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.keep(),
1291 Err(err) => return Err(Error::ExtractError(filename, err)),
1292 };
1293
1294 if extracted.join("install").is_dir() {
1296 extracted = extracted.join("install");
1297 } else if self.os().is_emscripten() {
1299 extracted = extracted.join("pyodide-root").join("dist");
1300 }
1301
1302 #[cfg(unix)]
1303 {
1304 if self.os().is_emscripten() {
1308 fs_err::create_dir_all(extracted.join("bin"))?;
1309 fs_err::os::unix::fs::symlink(
1310 "../python",
1311 extracted
1312 .join("bin")
1313 .join(format!("python{}.{}", self.key.major, self.key.minor)),
1314 )?;
1315 }
1316
1317 if !self.os().is_windows() {
1326 match fs_err::os::unix::fs::symlink(
1327 format!("python{}.{}", self.key.major, self.key.minor),
1328 extracted.join("bin").join("python"),
1329 ) {
1330 Ok(()) => {}
1331 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
1332 Err(err) => return Err(err.into()),
1333 }
1334 }
1335 }
1336
1337 if path.is_dir() {
1339 debug!("Removing existing directory: {}", path.user_display());
1340 fs_err::tokio::remove_dir_all(&path).await?;
1341 }
1342
1343 debug!("Moving {} to {}", extracted.display(), path.user_display());
1345 rename_with_retry(extracted, &path)
1346 .await
1347 .map_err(|err| Error::CopyError {
1348 to: path.clone(),
1349 err,
1350 })?;
1351
1352 Ok(DownloadResult::Fetched(path))
1353 }
1354
1355 async fn download_archive(
1357 &self,
1358 url: &DisplaySafeUrl,
1359 client: &BaseClient,
1360 reporter: Option<&dyn Reporter>,
1361 python_builds_dir: &Path,
1362 target_cache_file: &Path,
1363 ) -> Result<(), Error> {
1364 debug!(
1365 "Downloading {} to `{}`",
1366 url,
1367 target_cache_file.simplified_display()
1368 );
1369
1370 let (mut reader, size) = read_url(url, client).await?;
1371 let temp_dir = tempfile::tempdir_in(python_builds_dir)?;
1372 let temp_file = temp_dir.path().join("download");
1373
1374 {
1376 let mut archive_writer = BufWriter::new(fs_err::tokio::File::create(&temp_file).await?);
1377
1378 if let Some(reporter) = reporter {
1380 let key = reporter.on_request_start(Direction::Download, &self.key, size);
1381 tokio::io::copy(
1382 &mut ProgressReader::new(reader, key, reporter),
1383 &mut archive_writer,
1384 )
1385 .await?;
1386 reporter.on_request_complete(Direction::Download, key);
1387 } else {
1388 tokio::io::copy(&mut reader, &mut archive_writer).await?;
1389 }
1390
1391 archive_writer.flush().await?;
1392 }
1393 match rename_with_retry(&temp_file, target_cache_file).await {
1395 Ok(()) => {}
1396 Err(_) if target_cache_file.is_file() => {}
1397 Err(err) => return Err(err.into()),
1398 }
1399 Ok(())
1400 }
1401
1402 async fn extract_reader(
1405 &self,
1406 reader: impl AsyncRead + Unpin,
1407 target: &Path,
1408 filename: &String,
1409 ext: SourceDistExtension,
1410 size: Option<u64>,
1411 reporter: Option<&dyn Reporter>,
1412 direction: Direction,
1413 ) -> Result<(), Error> {
1414 let mut hashers = if self.sha256.is_some() {
1415 vec![Hasher::from(HashAlgorithm::Sha256)]
1416 } else {
1417 vec![]
1418 };
1419 let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
1420
1421 if let Some(reporter) = reporter {
1422 let progress_key = reporter.on_request_start(direction, &self.key, size);
1423 let mut reader = ProgressReader::new(&mut hasher, progress_key, reporter);
1424 uv_extract::stream::archive(filename, &mut reader, ext, target)
1425 .await
1426 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1427 reporter.on_request_complete(direction, progress_key);
1428 } else {
1429 uv_extract::stream::archive(filename, &mut hasher, ext, target)
1430 .await
1431 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1432 }
1433 hasher.finish().await.map_err(Error::HashExhaustion)?;
1434
1435 if let Some(expected) = self.sha256.as_deref() {
1437 let actual = HashDigest::from(hashers.pop().unwrap()).digest;
1438 if !actual.eq_ignore_ascii_case(expected) {
1439 return Err(Error::HashMismatch {
1440 installation: self.key.to_string(),
1441 expected: expected.to_string(),
1442 actual: actual.to_string(),
1443 });
1444 }
1445 }
1446
1447 Ok(())
1448 }
1449
1450 pub fn python_version(&self) -> PythonVersion {
1451 self.key.version()
1452 }
1453
1454 pub fn download_urls(
1462 &self,
1463 python_install_mirror: Option<&str>,
1464 pypy_install_mirror: Option<&str>,
1465 ) -> Result<Vec<DisplaySafeUrl>, Error> {
1466 let custom_astral_mirror = astral_mirror_url_from_env();
1467 self.download_urls_with_astral_mirror(
1468 python_install_mirror,
1469 pypy_install_mirror,
1470 custom_astral_mirror.as_deref(),
1471 )
1472 }
1473
1474 fn download_urls_with_astral_mirror(
1475 &self,
1476 python_install_mirror: Option<&str>,
1477 pypy_install_mirror: Option<&str>,
1478 astral_mirror_url: Option<&str>,
1479 ) -> Result<Vec<DisplaySafeUrl>, Error> {
1480 let astral_mirror_url = custom_astral_mirror_url(astral_mirror_url);
1481 match self.key.implementation {
1482 LenientImplementationName::Known(ImplementationName::CPython) => {
1483 if let Some(mirror) = python_install_mirror {
1484 let Some(suffix) = self.url.strip_prefix(CPYTHON_DOWNLOADS_URL_PREFIX) else {
1486 return Err(Error::Mirror(
1487 EnvVars::UV_PYTHON_INSTALL_MIRROR,
1488 self.url.to_string(),
1489 ));
1490 };
1491 return Ok(vec![DisplaySafeUrl::parse(
1492 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1493 )?]);
1494 }
1495 if let Some(suffix) = self.url.strip_prefix(CPYTHON_DOWNLOADS_URL_PREFIX) {
1497 let effective_mirror = effective_cpython_mirror(astral_mirror_url);
1498 let mirror_url = DisplaySafeUrl::parse(
1499 format!("{}/{}", effective_mirror.trim_end_matches('/'), suffix).as_str(),
1500 )?;
1501 if astral_mirror_url.is_some() {
1503 return Ok(vec![mirror_url]);
1504 }
1505 let canonical_url = DisplaySafeUrl::parse(&self.url)?;
1507 return Ok(vec![mirror_url, canonical_url]);
1508 }
1509 }
1510
1511 LenientImplementationName::Known(ImplementationName::PyPy) => {
1512 if let Some(mirror) = pypy_install_mirror {
1513 let Some(suffix) = self.url.strip_prefix("https://downloads.python.org/pypy/")
1514 else {
1515 return Err(Error::Mirror(
1516 EnvVars::UV_PYPY_INSTALL_MIRROR,
1517 self.url.to_string(),
1518 ));
1519 };
1520 return Ok(vec![DisplaySafeUrl::parse(
1521 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1522 )?]);
1523 }
1524 }
1525
1526 _ => {}
1527 }
1528
1529 Ok(vec![DisplaySafeUrl::parse(&self.url)?])
1530 }
1531}
1532
1533fn parse_json_downloads(
1534 json_downloads: HashMap<String, JsonPythonDownload>,
1535) -> Vec<ManagedPythonDownload> {
1536 json_downloads
1537 .into_iter()
1538 .filter_map(|(key, entry)| {
1539 let implementation = match entry.name.as_str() {
1540 "cpython" => LenientImplementationName::Known(ImplementationName::CPython),
1541 "pypy" => LenientImplementationName::Known(ImplementationName::PyPy),
1542 "graalpy" => LenientImplementationName::Known(ImplementationName::GraalPy),
1543 _ => LenientImplementationName::Unknown(entry.name.clone()),
1544 };
1545
1546 let arch_str = match entry.arch.family.as_str() {
1547 "armv5tel" => "armv5te".to_string(),
1548 "riscv64" => "riscv64gc".to_string(),
1552 value => value.to_string(),
1553 };
1554
1555 let arch_str = if let Some(variant) = entry.arch.variant {
1556 format!("{arch_str}_{variant}")
1557 } else {
1558 arch_str
1559 };
1560
1561 let arch = match Arch::from_str(&arch_str) {
1562 Ok(arch) => arch,
1563 Err(e) => {
1564 debug!("Skipping entry {key}: Invalid arch '{arch_str}' - {e}");
1565 return None;
1566 }
1567 };
1568
1569 let os = match Os::from_str(&entry.os) {
1570 Ok(os) => os,
1571 Err(e) => {
1572 debug!("Skipping entry {}: Invalid OS '{}' - {}", key, entry.os, e);
1573 return None;
1574 }
1575 };
1576
1577 let libc = match Libc::from_str(&entry.libc) {
1578 Ok(libc) => libc,
1579 Err(e) => {
1580 debug!(
1581 "Skipping entry {}: Invalid libc '{}' - {}",
1582 key, entry.libc, e
1583 );
1584 return None;
1585 }
1586 };
1587
1588 let variant = match entry
1589 .variant
1590 .as_deref()
1591 .map(PythonVariant::from_str)
1592 .transpose()
1593 {
1594 Ok(Some(variant)) => variant,
1595 Ok(None) => PythonVariant::default(),
1596 Err(()) => {
1597 debug!(
1598 "Skipping entry {key}: Unknown python variant - {}",
1599 entry.variant.unwrap_or_default()
1600 );
1601 return None;
1602 }
1603 };
1604
1605 let version_str = format!(
1606 "{}.{}.{}{}",
1607 entry.major,
1608 entry.minor,
1609 entry.patch,
1610 entry.prerelease.as_deref().unwrap_or_default()
1611 );
1612
1613 let version = match PythonVersion::from_str(&version_str) {
1614 Ok(version) => version,
1615 Err(e) => {
1616 debug!("Skipping entry {key}: Invalid version '{version_str}' - {e}");
1617 return None;
1618 }
1619 };
1620
1621 let url = Cow::Owned(entry.url);
1622 let sha256 = entry.sha256.map(Cow::Owned);
1623 let build = entry
1624 .build
1625 .map(|s| Box::leak(s.into_boxed_str()) as &'static str);
1626
1627 Some(ManagedPythonDownload {
1628 key: PythonInstallationKey::new_from_version(
1629 implementation,
1630 &version,
1631 Platform::new(os, arch, libc),
1632 variant,
1633 ),
1634 url,
1635 sha256,
1636 build,
1637 })
1638 })
1639 .sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
1640 .collect()
1641}
1642
1643impl Error {
1644 fn from_reqwest(
1645 url: DisplaySafeUrl,
1646 err: reqwest::Error,
1647 retries: Option<u32>,
1648 start: Instant,
1649 ) -> Self {
1650 let err = Self::NetworkError(url, WrappedReqwestError::from(err));
1651 if let Some(retries) = retries {
1652 Self::NetworkErrorWithRetries {
1653 err: Box::new(err),
1654 retries,
1655 duration: start.elapsed(),
1656 }
1657 } else {
1658 err
1659 }
1660 }
1661
1662 fn from_reqwest_middleware(url: DisplaySafeUrl, err: reqwest_middleware::Error) -> Self {
1663 match err {
1664 reqwest_middleware::Error::Middleware(error) => {
1665 Self::NetworkMiddlewareError(url, error)
1666 }
1667 reqwest_middleware::Error::Reqwest(error) => {
1668 Self::NetworkError(url, WrappedReqwestError::from(error))
1669 }
1670 }
1671 }
1672}
1673
1674impl Display for ManagedPythonDownload {
1675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1676 write!(f, "{}", self.key)
1677 }
1678}
1679
1680#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1681pub enum Direction {
1682 Download,
1683 Extract,
1684}
1685
1686impl Direction {
1687 fn as_str(&self) -> &str {
1688 match self {
1689 Self::Download => "download",
1690 Self::Extract => "extract",
1691 }
1692 }
1693}
1694
1695impl Display for Direction {
1696 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1697 f.write_str(self.as_str())
1698 }
1699}
1700
1701pub trait Reporter: Send + Sync {
1702 fn on_request_start(
1703 &self,
1704 direction: Direction,
1705 name: &PythonInstallationKey,
1706 size: Option<u64>,
1707 ) -> usize;
1708 fn on_request_progress(&self, id: usize, inc: u64);
1709 fn on_request_complete(&self, direction: Direction, id: usize);
1710}
1711
1712struct ProgressReader<'a, R> {
1714 reader: R,
1715 index: usize,
1716 reporter: &'a dyn Reporter,
1717}
1718
1719impl<'a, R> ProgressReader<'a, R> {
1720 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1722 Self {
1723 reader,
1724 index,
1725 reporter,
1726 }
1727 }
1728}
1729
1730impl<R> AsyncRead for ProgressReader<'_, R>
1731where
1732 R: AsyncRead + Unpin,
1733{
1734 fn poll_read(
1735 mut self: Pin<&mut Self>,
1736 cx: &mut Context<'_>,
1737 buf: &mut ReadBuf<'_>,
1738 ) -> Poll<io::Result<()>> {
1739 Pin::new(&mut self.as_mut().reader)
1740 .poll_read(cx, buf)
1741 .map_ok(|()| {
1742 self.reporter
1743 .on_request_progress(self.index, buf.filled().len() as u64);
1744 })
1745 }
1746}
1747
1748async fn read_url(
1750 url: &DisplaySafeUrl,
1751 client: &BaseClient,
1752) -> Result<(impl AsyncRead + Unpin, Option<u64>), Error> {
1753 if url.scheme() == "file" {
1754 let path = url
1756 .to_file_path()
1757 .map_err(|()| Error::InvalidFileUrl(url.to_string()))?;
1758
1759 let size = fs_err::tokio::metadata(&path).await?.len();
1760 let reader = fs_err::tokio::File::open(&path).await?;
1761
1762 Ok((Either::Left(reader), Some(size)))
1763 } else {
1764 let start = Instant::now();
1765 let response = client
1766 .for_host(url)
1767 .get(Url::from(url.clone()))
1768 .send()
1769 .await
1770 .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?;
1771
1772 let retry_count = response
1773 .extensions()
1774 .get::<reqwest_retry::RetryCount>()
1775 .map(|retries| retries.value());
1776
1777 let response = response
1779 .error_for_status()
1780 .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count, start))?;
1781
1782 let size = response.content_length();
1783 let stream = response
1784 .bytes_stream()
1785 .map_err(io::Error::other)
1786 .into_async_read();
1787
1788 Ok((Either::Right(stream.compat()), size))
1789 }
1790}
1791
1792#[cfg(test)]
1793mod tests {
1794 use std::collections::HashSet;
1795
1796 use crate::PythonVariant;
1797 use crate::implementation::LenientImplementationName;
1798 use crate::installation::PythonInstallationKey;
1799 use uv_platform::{Arch, Libc, Os, Platform};
1800
1801 use super::*;
1802
1803 #[test]
1805 fn test_python_download_request_from_str_complete() {
1806 let request = PythonDownloadRequest::from_str("cpython-3.12.0-linux-x86_64-gnu")
1807 .expect("Test request should be parsed");
1808
1809 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1810 assert_eq!(
1811 request.version,
1812 Some(VersionRequest::from_str("3.12.0").unwrap())
1813 );
1814 assert_eq!(
1815 request.os,
1816 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1817 );
1818 assert_eq!(
1819 request.arch,
1820 Some(ArchRequest::Explicit(Arch::new(
1821 target_lexicon::Architecture::X86_64,
1822 None
1823 )))
1824 );
1825 assert_eq!(
1826 request.libc,
1827 Some(Libc::Some(target_lexicon::Environment::Gnu))
1828 );
1829 }
1830
1831 #[test]
1833 fn test_python_download_request_from_str_with_any() {
1834 let request = PythonDownloadRequest::from_str("any-3.11-any-x86_64-any")
1835 .expect("Test request should be parsed");
1836
1837 assert_eq!(request.implementation, None);
1838 assert_eq!(
1839 request.version,
1840 Some(VersionRequest::from_str("3.11").unwrap())
1841 );
1842 assert_eq!(request.os, None);
1843 assert_eq!(
1844 request.arch,
1845 Some(ArchRequest::Explicit(Arch::new(
1846 target_lexicon::Architecture::X86_64,
1847 None
1848 )))
1849 );
1850 assert_eq!(request.libc, None);
1851 }
1852
1853 #[test]
1855 fn test_python_download_request_from_str_missing_segment() {
1856 let request =
1857 PythonDownloadRequest::from_str("pypy-linux").expect("Test request should be parsed");
1858
1859 assert_eq!(request.implementation, Some(ImplementationName::PyPy));
1860 assert_eq!(request.version, None);
1861 assert_eq!(
1862 request.os,
1863 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1864 );
1865 assert_eq!(request.arch, None);
1866 assert_eq!(request.libc, None);
1867 }
1868
1869 #[test]
1870 fn test_python_download_request_from_str_version_only() {
1871 let request =
1872 PythonDownloadRequest::from_str("3.10.5").expect("Test request should be parsed");
1873
1874 assert_eq!(request.implementation, None);
1875 assert_eq!(
1876 request.version,
1877 Some(VersionRequest::from_str("3.10.5").unwrap())
1878 );
1879 assert_eq!(request.os, None);
1880 assert_eq!(request.arch, None);
1881 assert_eq!(request.libc, None);
1882 }
1883
1884 #[test]
1885 fn test_python_download_request_from_str_implementation_only() {
1886 let request =
1887 PythonDownloadRequest::from_str("cpython").expect("Test request should be parsed");
1888
1889 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1890 assert_eq!(request.version, None);
1891 assert_eq!(request.os, None);
1892 assert_eq!(request.arch, None);
1893 assert_eq!(request.libc, None);
1894 }
1895
1896 #[test]
1898 fn test_python_download_request_from_str_os_arch() {
1899 let request = PythonDownloadRequest::from_str("windows-x86_64")
1900 .expect("Test request should be parsed");
1901
1902 assert_eq!(request.implementation, None);
1903 assert_eq!(request.version, None);
1904 assert_eq!(
1905 request.os,
1906 Some(Os::new(target_lexicon::OperatingSystem::Windows))
1907 );
1908 assert_eq!(
1909 request.arch,
1910 Some(ArchRequest::Explicit(Arch::new(
1911 target_lexicon::Architecture::X86_64,
1912 None
1913 )))
1914 );
1915 assert_eq!(request.libc, None);
1916 }
1917
1918 #[test]
1920 fn test_python_download_request_from_str_prerelease() {
1921 let request = PythonDownloadRequest::from_str("cpython-3.13.0rc1")
1922 .expect("Test request should be parsed");
1923
1924 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1925 assert_eq!(
1926 request.version,
1927 Some(VersionRequest::from_str("3.13.0rc1").unwrap())
1928 );
1929 assert_eq!(request.os, None);
1930 assert_eq!(request.arch, None);
1931 assert_eq!(request.libc, None);
1932 }
1933
1934 #[test]
1936 fn test_python_download_request_from_str_too_many_parts() {
1937 let result = PythonDownloadRequest::from_str("cpython-3.12-linux-x86_64-gnu-extra");
1938
1939 assert!(matches!(result, Err(Error::TooManyParts(_))));
1940 }
1941
1942 #[test]
1944 fn test_python_download_request_from_str_empty() {
1945 let result = PythonDownloadRequest::from_str("");
1946
1947 assert!(matches!(result, Err(Error::EmptyRequest)), "{result:?}");
1948 }
1949
1950 #[test]
1952 fn test_python_download_request_from_str_all_any() {
1953 let request = PythonDownloadRequest::from_str("any-any-any-any-any")
1954 .expect("Test request should be parsed");
1955
1956 assert_eq!(request.implementation, None);
1957 assert_eq!(request.version, None);
1958 assert_eq!(request.os, None);
1959 assert_eq!(request.arch, None);
1960 assert_eq!(request.libc, None);
1961 }
1962
1963 #[test]
1965 fn test_python_download_request_from_str_case_insensitive_any() {
1966 let request = PythonDownloadRequest::from_str("ANY-3.11-Any-x86_64-aNy")
1967 .expect("Test request should be parsed");
1968
1969 assert_eq!(request.implementation, None);
1970 assert_eq!(
1971 request.version,
1972 Some(VersionRequest::from_str("3.11").unwrap())
1973 );
1974 assert_eq!(request.os, None);
1975 assert_eq!(
1976 request.arch,
1977 Some(ArchRequest::Explicit(Arch::new(
1978 target_lexicon::Architecture::X86_64,
1979 None
1980 )))
1981 );
1982 assert_eq!(request.libc, None);
1983 }
1984
1985 #[test]
1987 fn test_python_download_request_from_str_invalid_leading_segment() {
1988 let result = PythonDownloadRequest::from_str("foobar-3.14-windows");
1989
1990 assert!(
1991 matches!(result, Err(Error::ImplementationError(_))),
1992 "{result:?}"
1993 );
1994 }
1995
1996 #[test]
1998 fn test_python_download_request_from_str_out_of_order() {
1999 let result = PythonDownloadRequest::from_str("3.12-cpython");
2000
2001 assert!(
2002 matches!(result, Err(Error::InvalidRequestPlatform(_))),
2003 "{result:?}"
2004 );
2005 }
2006
2007 #[test]
2009 fn test_python_download_request_from_str_too_many_any() {
2010 let result = PythonDownloadRequest::from_str("any-any-any-any-any-any");
2011
2012 assert!(matches!(result, Err(Error::TooManyParts(_))));
2013 }
2014
2015 #[tokio::test]
2017 async fn test_python_download_request_build_filtering() {
2018 let mut request = PythonDownloadRequest::default()
2019 .with_version(VersionRequest::from_str("3.12").unwrap())
2020 .with_implementation(ImplementationName::CPython);
2021 request.build = Some("20240814".to_string());
2022
2023 let client = uv_client::BaseClientBuilder::default()
2024 .build()
2025 .expect("failed to build base client");
2026 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
2027
2028 let downloads: Vec<_> = download_list
2029 .iter_all()
2030 .filter(|d| request.satisfied_by_download(d))
2031 .collect();
2032
2033 assert!(
2034 !downloads.is_empty(),
2035 "Should find at least one matching download"
2036 );
2037 for download in downloads {
2038 assert_eq!(download.build(), Some("20240814"));
2039 }
2040 }
2041
2042 #[tokio::test]
2044 async fn test_python_download_request_invalid_build() {
2045 let mut request = PythonDownloadRequest::default()
2047 .with_version(VersionRequest::from_str("3.12").unwrap())
2048 .with_implementation(ImplementationName::CPython);
2049 request.build = Some("99999999".to_string());
2050
2051 let client = uv_client::BaseClientBuilder::default()
2052 .build()
2053 .expect("failed to build base client");
2054 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
2055
2056 let downloads: Vec<_> = download_list
2058 .iter_all()
2059 .filter(|d| request.satisfied_by_download(d))
2060 .collect();
2061
2062 assert_eq!(downloads.len(), 0);
2063 }
2064
2065 #[test]
2066 fn upgrade_request_native_defaults() {
2067 let request = PythonDownloadRequest::default()
2068 .with_implementation(ImplementationName::CPython)
2069 .with_version(VersionRequest::MajorMinorPatch(
2070 3,
2071 13,
2072 1,
2073 PythonVariant::Default,
2074 ))
2075 .with_os(Os::from_str("linux").unwrap())
2076 .with_arch(Arch::from_str("x86_64").unwrap())
2077 .with_libc(Libc::from_str("gnu").unwrap())
2078 .with_prereleases(false);
2079
2080 let host = Platform::new(
2081 Os::from_str("linux").unwrap(),
2082 Arch::from_str("x86_64").unwrap(),
2083 Libc::from_str("gnu").unwrap(),
2084 );
2085
2086 assert_eq!(
2087 request
2088 .clone()
2089 .unset_defaults_for_host(&host)
2090 .without_patch()
2091 .simplified_display()
2092 .as_deref(),
2093 Some("3.13")
2094 );
2095 }
2096
2097 #[test]
2098 fn upgrade_request_preserves_variant() {
2099 let request = PythonDownloadRequest::default()
2100 .with_implementation(ImplementationName::CPython)
2101 .with_version(VersionRequest::MajorMinorPatch(
2102 3,
2103 13,
2104 0,
2105 PythonVariant::Freethreaded,
2106 ))
2107 .with_os(Os::from_str("linux").unwrap())
2108 .with_arch(Arch::from_str("x86_64").unwrap())
2109 .with_libc(Libc::from_str("gnu").unwrap())
2110 .with_prereleases(false);
2111
2112 let host = Platform::new(
2113 Os::from_str("linux").unwrap(),
2114 Arch::from_str("x86_64").unwrap(),
2115 Libc::from_str("gnu").unwrap(),
2116 );
2117
2118 assert_eq!(
2119 request
2120 .clone()
2121 .unset_defaults_for_host(&host)
2122 .without_patch()
2123 .simplified_display()
2124 .as_deref(),
2125 Some("3.13+freethreaded")
2126 );
2127 }
2128
2129 #[test]
2130 fn upgrade_request_preserves_non_default_platform() {
2131 let request = PythonDownloadRequest::default()
2132 .with_implementation(ImplementationName::CPython)
2133 .with_version(VersionRequest::MajorMinorPatch(
2134 3,
2135 12,
2136 4,
2137 PythonVariant::Default,
2138 ))
2139 .with_os(Os::from_str("linux").unwrap())
2140 .with_arch(Arch::from_str("aarch64").unwrap())
2141 .with_libc(Libc::from_str("gnu").unwrap())
2142 .with_prereleases(false);
2143
2144 let host = Platform::new(
2145 Os::from_str("linux").unwrap(),
2146 Arch::from_str("x86_64").unwrap(),
2147 Libc::from_str("gnu").unwrap(),
2148 );
2149
2150 assert_eq!(
2151 request
2152 .clone()
2153 .unset_defaults_for_host(&host)
2154 .without_patch()
2155 .simplified_display()
2156 .as_deref(),
2157 Some("3.12-aarch64")
2158 );
2159 }
2160
2161 #[test]
2162 fn upgrade_request_preserves_custom_implementation() {
2163 let request = PythonDownloadRequest::default()
2164 .with_implementation(ImplementationName::PyPy)
2165 .with_version(VersionRequest::MajorMinorPatch(
2166 3,
2167 10,
2168 5,
2169 PythonVariant::Default,
2170 ))
2171 .with_os(Os::from_str("linux").unwrap())
2172 .with_arch(Arch::from_str("x86_64").unwrap())
2173 .with_libc(Libc::from_str("gnu").unwrap())
2174 .with_prereleases(false);
2175
2176 let host = Platform::new(
2177 Os::from_str("linux").unwrap(),
2178 Arch::from_str("x86_64").unwrap(),
2179 Libc::from_str("gnu").unwrap(),
2180 );
2181
2182 assert_eq!(
2183 request
2184 .clone()
2185 .unset_defaults_for_host(&host)
2186 .without_patch()
2187 .simplified_display()
2188 .as_deref(),
2189 Some("pypy-3.10")
2190 );
2191 }
2192
2193 #[test]
2194 fn simplified_display_returns_none_when_empty() {
2195 let request = PythonDownloadRequest::default()
2196 .fill_platform()
2197 .expect("should populate defaults");
2198
2199 let host = Platform::from_env().expect("host platform");
2200
2201 assert_eq!(
2202 request.unset_defaults_for_host(&host).simplified_display(),
2203 None
2204 );
2205 }
2206
2207 #[test]
2208 fn simplified_display_omits_environment_arch() {
2209 let mut request = PythonDownloadRequest::default()
2210 .with_version(VersionRequest::MajorMinor(3, 12, PythonVariant::Default))
2211 .with_os(Os::from_str("linux").unwrap())
2212 .with_libc(Libc::from_str("gnu").unwrap());
2213
2214 request.arch = Some(ArchRequest::Environment(Arch::from_str("x86_64").unwrap()));
2215
2216 let host = Platform::new(
2217 Os::from_str("linux").unwrap(),
2218 Arch::from_str("aarch64").unwrap(),
2219 Libc::from_str("gnu").unwrap(),
2220 );
2221
2222 assert_eq!(
2223 request
2224 .unset_defaults_for_host(&host)
2225 .simplified_display()
2226 .as_deref(),
2227 Some("3.12")
2228 );
2229 }
2230
2231 fn cpython_download_for_url(url: &'static str) -> ManagedPythonDownload {
2232 let key = PythonInstallationKey::new(
2233 LenientImplementationName::Known(crate::implementation::ImplementationName::CPython),
2234 3,
2235 12,
2236 4,
2237 None,
2238 Platform::new(
2239 Os::from_str("linux").unwrap(),
2240 Arch::from_str("x86_64").unwrap(),
2241 Libc::from_str("gnu").unwrap(),
2242 ),
2243 crate::PythonVariant::default(),
2244 );
2245
2246 ManagedPythonDownload {
2247 key,
2248 url: Cow::Borrowed(url),
2249 sha256: Some(Cow::Borrowed("abc123")),
2250 build: Some("20240713"),
2251 }
2252 }
2253
2254 #[test]
2255 fn test_cpython_download_urls_custom_astral_mirror() {
2256 let download = cpython_download_for_url(
2257 "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",
2258 );
2259
2260 let urls = download
2261 .download_urls_with_astral_mirror(
2262 None,
2263 None,
2264 Some("https://nexus.example.com/repository/releases.astral.sh/"),
2265 )
2266 .expect("download URLs should be valid");
2267 let urls = urls
2268 .into_iter()
2269 .map(|url| url.to_string())
2270 .collect::<Vec<_>>();
2271 assert_eq!(
2272 urls,
2273 vec![
2274 "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"
2275 .to_string(),
2276 ]
2277 );
2278 }
2279
2280 #[test]
2281 fn test_cpython_specific_mirror_takes_precedence_over_astral_mirror() {
2282 let download = cpython_download_for_url(
2283 "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",
2284 );
2285
2286 let urls = download
2287 .download_urls_with_astral_mirror(
2288 Some("https://python-mirror.example.com/releases/"),
2289 None,
2290 Some("https://nexus.example.com/repository/releases.astral.sh/"),
2291 )
2292 .expect("download URLs should be valid");
2293 let urls = urls
2294 .into_iter()
2295 .map(|url| url.to_string())
2296 .collect::<Vec<_>>();
2297 assert_eq!(
2298 urls,
2299 vec![
2300 "https://python-mirror.example.com/releases/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz"
2301 .to_string(),
2302 ]
2303 );
2304 }
2305
2306 #[test]
2307 fn test_cpython_download_urls_empty_astral_mirror_uses_default() {
2308 let download = cpython_download_for_url(
2309 "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",
2310 );
2311
2312 let default_urls = download
2313 .download_urls_with_astral_mirror(None, None, None)
2314 .expect("download URLs should be valid");
2315 let empty_urls = download
2316 .download_urls_with_astral_mirror(None, None, Some(""))
2317 .expect("download URLs should be valid");
2318
2319 assert_eq!(default_urls, empty_urls);
2320 }
2321
2322 #[test]
2325 fn test_should_try_next_url_hash_mismatch() {
2326 let err = Error::HashMismatch {
2327 installation: "cpython-3.12.0".to_string(),
2328 expected: "abc".to_string(),
2329 actual: "def".to_string(),
2330 };
2331 assert!(!err.should_try_next_url());
2332 }
2333
2334 #[test]
2337 fn test_should_try_next_url_extract_error_filesystem() {
2338 let err = Error::ExtractError(
2339 "archive.tar.gz".to_string(),
2340 uv_extract::Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, "")),
2341 );
2342 assert!(!err.should_try_next_url());
2343 }
2344
2345 #[test]
2348 fn test_should_try_next_url_io_error_filesystem() {
2349 let err = Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, ""));
2350 assert!(!err.should_try_next_url());
2351 }
2352
2353 #[test]
2356 fn test_should_try_next_url_io_error_network() {
2357 let err = Error::Io(io::Error::new(io::ErrorKind::ConnectionReset, ""));
2358 assert!(err.should_try_next_url());
2359 }
2360
2361 #[test]
2364 fn test_should_try_next_url_network_error_404() {
2365 let url =
2366 DisplaySafeUrl::from_str("https://releases.astral.sh/python/cpython-3.12.0.tar.gz")
2367 .unwrap();
2368 let wrapped = WrappedReqwestError::with_problem_details(
2371 reqwest_middleware::Error::Middleware(anyhow::anyhow!("404 Not Found")),
2372 None,
2373 );
2374 let err = Error::NetworkError(url, wrapped);
2375 assert!(err.should_try_next_url());
2376 }
2377
2378 #[test]
2381 fn embedded_download_versions_convert_to_version_requests() {
2382 let downloads = ManagedPythonDownloadList::new_only_embedded()
2383 .expect("embedded download metadata should load");
2384
2385 let unique_versions: HashSet<PythonVersion> = downloads
2386 .iter_all()
2387 .map(ManagedPythonDownload::python_version)
2388 .collect();
2389
2390 for version in &unique_versions {
2391 let _ = VersionRequest::from(version);
2392 }
2393 }
2394}