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 && !platform.os.supports(os)
241 {
242 return false;
243 }
244
245 if let Some(arch) = self.arch
246 && !arch.satisfied_by(platform)
247 {
248 return false;
249 }
250
251 if let Some(libc) = self.libc
252 && platform.libc != libc
253 {
254 return false;
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 && key.implementation != LenientImplementationName::from(*implementation)
560 {
561 return false;
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 && variant != key.variant
578 {
579 return false;
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 && !version.matches_interpreter(interpreter)
639 {
640 let interpreter_version = interpreter.python_version();
641 debug!(
642 "Skipping interpreter at `{executable}`: version `{interpreter_version}` does not match request `{version}`"
643 );
644 return false;
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 && !implementation.matches_interpreter(interpreter)
656 {
657 debug!(
658 "Skipping interpreter at `{executable}`: implementation `{}` does not match request `{implementation}`",
659 interpreter.implementation_name(),
660 );
661 return false;
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 && let Some(download) = self
1002 .iter_matching(&request.clone().with_prereleases(true))
1003 .next()
1004 {
1005 return Ok(download);
1006 }
1007
1008 Err(Error::NoDownloadFound(request.clone()))
1009 }
1010
1011 pub async fn new(
1020 client: &BaseClient,
1021 python_downloads_json_url: Option<&str>,
1022 ) -> Result<Self, Error> {
1023 enum Source<'a> {
1028 BuiltIn,
1029 Path(Cow<'a, Path>),
1030 Http(DisplaySafeUrl),
1031 }
1032
1033 let json_source = if let Some(url_or_path) = python_downloads_json_url {
1034 if let Ok(url) = DisplaySafeUrl::parse(url_or_path) {
1035 match url.scheme() {
1036 "http" | "https" => Source::Http(url),
1037 "file" => Source::Path(Cow::Owned(
1038 url.to_file_path().or(Err(Error::InvalidUrlFormat(url)))?,
1039 )),
1040 _ => Source::Path(Cow::Borrowed(Path::new(url_or_path))),
1041 }
1042 } else {
1043 Source::Path(Cow::Borrowed(Path::new(url_or_path)))
1044 }
1045 } else {
1046 Source::BuiltIn
1047 };
1048
1049 let buf: Cow<'_, [u8]> = match json_source {
1050 Source::BuiltIn => BUILTIN_PYTHON_DOWNLOADS_JSON.into(),
1051 Source::Path(ref path) => fs_err::read(path.as_ref())?.into(),
1052 Source::Http(ref url) => fetch_bytes_from_url(client, url)
1053 .await
1054 .map_err(|e| Error::FetchingPythonDownloadsJSONError(url.to_string(), Box::new(e)))?
1055 .into(),
1056 };
1057 let json_downloads: HashMap<String, JsonPythonDownload> = serde_json::from_slice(&buf)
1058 .map_err(
1059 #[expect(clippy::zero_sized_map_values)]
1066 |e| {
1067 let source = match json_source {
1068 Source::BuiltIn => "EMBEDDED IN THE BINARY".to_owned(),
1069 Source::Path(path) => path.to_string_lossy().to_string(),
1070 Source::Http(url) => url.to_string(),
1071 };
1072 if let Ok(keys) =
1073 serde_json::from_slice::<HashMap<String, serde::de::IgnoredAny>>(&buf)
1074 && keys.contains_key("version")
1075 {
1076 Error::UnsupportedPythonDownloadsJSON(source)
1077 } else {
1078 Error::InvalidPythonDownloadsJSON(source, e)
1079 }
1080 },
1081 )?;
1082
1083 let result = parse_json_downloads(json_downloads);
1084 Ok(Self { downloads: result })
1085 }
1086
1087 pub fn new_only_embedded() -> Result<Self, Error> {
1090 let json_downloads: HashMap<String, JsonPythonDownload> =
1091 serde_json::from_slice(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| {
1092 Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_owned(), e)
1093 })?;
1094 let result = parse_json_downloads(json_downloads);
1095 Ok(Self { downloads: result })
1096 }
1097}
1098
1099async fn fetch_bytes_from_url(client: &BaseClient, url: &DisplaySafeUrl) -> Result<Vec<u8>, Error> {
1100 let (mut reader, size) = read_url(url, client).await?;
1101 let capacity = size.and_then(|s| s.try_into().ok()).unwrap_or(1_048_576);
1102 let mut buf = Vec::with_capacity(capacity);
1103 reader.read_to_end(&mut buf).await?;
1104 Ok(buf)
1105}
1106
1107impl ManagedPythonDownload {
1108 pub(crate) fn url(&self) -> &Cow<'static, str> {
1109 &self.url
1110 }
1111
1112 pub fn key(&self) -> &PythonInstallationKey {
1113 &self.key
1114 }
1115
1116 fn os(&self) -> &Os {
1117 self.key.os()
1118 }
1119
1120 pub(crate) fn sha256(&self) -> Option<&Cow<'static, str>> {
1121 self.sha256.as_ref()
1122 }
1123
1124 pub fn build(&self) -> Option<&'static str> {
1125 self.build
1126 }
1127
1128 #[instrument(skip_all, fields(download = % self.key()))]
1134 pub async fn fetch_with_retry(
1135 &self,
1136 client: &BaseClient,
1137 retry_policy: &ExponentialBackoff,
1138 installation_dir: &Path,
1139 scratch_dir: &Path,
1140 reinstall: bool,
1141 python_install_mirror: Option<&str>,
1142 pypy_install_mirror: Option<&str>,
1143 reporter: Option<&dyn Reporter>,
1144 ) -> Result<DownloadResult, Error> {
1145 let urls = self.download_urls(python_install_mirror, pypy_install_mirror)?;
1146 if urls.is_empty() {
1147 return Err(Error::NoPythonDownloadUrlFound);
1148 }
1149 fetch_with_url_fallback(&urls, *retry_policy, &format!("`{}`", self.key()), |url| {
1150 self.fetch_from_url(
1151 url,
1152 client,
1153 installation_dir,
1154 scratch_dir,
1155 reinstall,
1156 reporter,
1157 )
1158 })
1159 .await
1160 }
1161
1162 async fn fetch_from_url(
1164 &self,
1165 url: DisplaySafeUrl,
1166 client: &BaseClient,
1167 installation_dir: &Path,
1168 scratch_dir: &Path,
1169 reinstall: bool,
1170 reporter: Option<&dyn Reporter>,
1171 ) -> Result<DownloadResult, Error> {
1172 let path = installation_dir.join(self.key().to_string());
1173
1174 if !reinstall && path.is_dir() {
1176 return Ok(DownloadResult::AlreadyAvailable(path));
1177 }
1178
1179 let filename = url
1182 .path_segments()
1183 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1184 .next_back()
1185 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1186 .replace("%2B", "-");
1187 debug_assert!(
1188 filename
1189 .chars()
1190 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'),
1191 "Unexpected char in filename: {filename}"
1192 );
1193 let ext = SourceDistExtension::from_path(&filename)
1194 .map_err(|err| Error::MissingExtension(url.to_string(), err))?;
1195
1196 let temp_dir = tempfile::tempdir_in(scratch_dir).map_err(Error::DownloadDirError)?;
1197
1198 if let Some(python_builds_dir) =
1199 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).filter(|s| !s.is_empty())
1200 {
1201 let python_builds_dir = PathBuf::from(python_builds_dir);
1202 fs_err::create_dir_all(&python_builds_dir)?;
1203 let hash_prefix = match self.sha256.as_deref() {
1204 Some(sha) => {
1205 &sha[..9]
1207 }
1208 None => "none",
1209 };
1210 let target_cache_file = python_builds_dir.join(format!("{hash_prefix}-{filename}"));
1211
1212 let (reader, size): (Box<dyn AsyncRead + Unpin>, Option<u64>) =
1216 match fs_err::tokio::File::open(&target_cache_file).await {
1217 Ok(file) => {
1218 debug!(
1219 "Extracting existing `{}`",
1220 target_cache_file.simplified_display()
1221 );
1222 let size = file.metadata().await?.len();
1223 let reader = Box::new(tokio::io::BufReader::new(file));
1224 (reader, Some(size))
1225 }
1226 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1227 if client.connectivity().is_offline() {
1229 return Err(Error::OfflinePythonMissing {
1230 file: Box::new(self.key().clone()),
1231 url: Box::new(url.clone()),
1232 python_builds_dir,
1233 });
1234 }
1235
1236 self.download_archive(
1237 &url,
1238 client,
1239 reporter,
1240 &python_builds_dir,
1241 &target_cache_file,
1242 )
1243 .await?;
1244
1245 debug!("Extracting `{}`", target_cache_file.simplified_display());
1246 let file = fs_err::tokio::File::open(&target_cache_file).await?;
1247 let size = file.metadata().await?.len();
1248 let reader = Box::new(tokio::io::BufReader::new(file));
1249 (reader, Some(size))
1250 }
1251 Err(err) => return Err(err.into()),
1252 };
1253
1254 self.extract_reader(
1256 reader,
1257 temp_dir.path(),
1258 &filename,
1259 ext,
1260 size,
1261 reporter,
1262 Direction::Extract,
1263 )
1264 .await?;
1265 } else {
1266 debug!("Downloading {url}");
1268 debug!(
1269 "Extracting {filename} to temporary location: {}",
1270 temp_dir.path().simplified_display()
1271 );
1272
1273 let (reader, size) = read_url(&url, client).await?;
1274 self.extract_reader(
1275 reader,
1276 temp_dir.path(),
1277 &filename,
1278 ext,
1279 size,
1280 reporter,
1281 Direction::Download,
1282 )
1283 .await?;
1284 }
1285
1286 let mut extracted = match uv_extract::strip_component(temp_dir.path()) {
1288 Ok(top_level) => top_level,
1289 Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.keep(),
1290 Err(err) => return Err(Error::ExtractError(filename, err)),
1291 };
1292
1293 if extracted.join("install").is_dir() {
1295 extracted = extracted.join("install");
1296 } else if self.os().is_emscripten() {
1298 extracted = extracted.join("pyodide-root").join("dist");
1299 }
1300
1301 #[cfg(unix)]
1302 {
1303 if self.os().is_emscripten() {
1307 fs_err::create_dir_all(extracted.join("bin"))?;
1308 fs_err::os::unix::fs::symlink(
1309 "../python",
1310 extracted
1311 .join("bin")
1312 .join(format!("python{}.{}", self.key.major, self.key.minor)),
1313 )?;
1314 }
1315
1316 if !self.os().is_windows() {
1325 match fs_err::os::unix::fs::symlink(
1326 format!("python{}.{}", self.key.major, self.key.minor),
1327 extracted.join("bin").join("python"),
1328 ) {
1329 Ok(()) => {}
1330 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
1331 Err(err) => return Err(err.into()),
1332 }
1333 }
1334 }
1335
1336 if path.is_dir() {
1338 debug!("Removing existing directory: {}", path.user_display());
1339 fs_err::tokio::remove_dir_all(&path).await?;
1340 }
1341
1342 debug!("Moving {} to {}", extracted.display(), path.user_display());
1344 rename_with_retry(extracted, &path)
1345 .await
1346 .map_err(|err| Error::CopyError {
1347 to: path.clone(),
1348 err,
1349 })?;
1350
1351 Ok(DownloadResult::Fetched(path))
1352 }
1353
1354 async fn download_archive(
1356 &self,
1357 url: &DisplaySafeUrl,
1358 client: &BaseClient,
1359 reporter: Option<&dyn Reporter>,
1360 python_builds_dir: &Path,
1361 target_cache_file: &Path,
1362 ) -> Result<(), Error> {
1363 debug!(
1364 "Downloading {} to `{}`",
1365 url,
1366 target_cache_file.simplified_display()
1367 );
1368
1369 let (mut reader, size) = read_url(url, client).await?;
1370 let temp_dir = tempfile::tempdir_in(python_builds_dir)?;
1371 let temp_file = temp_dir.path().join("download");
1372
1373 {
1375 let mut archive_writer = BufWriter::new(fs_err::tokio::File::create(&temp_file).await?);
1376
1377 if let Some(reporter) = reporter {
1379 let key = reporter.on_request_start(Direction::Download, &self.key, size);
1380 tokio::io::copy(
1381 &mut ProgressReader::new(reader, key, reporter),
1382 &mut archive_writer,
1383 )
1384 .await?;
1385 reporter.on_request_complete(Direction::Download, key);
1386 } else {
1387 tokio::io::copy(&mut reader, &mut archive_writer).await?;
1388 }
1389
1390 archive_writer.flush().await?;
1391 }
1392 match rename_with_retry(&temp_file, target_cache_file).await {
1394 Ok(()) => {}
1395 Err(_) if target_cache_file.is_file() => {}
1396 Err(err) => return Err(err.into()),
1397 }
1398 Ok(())
1399 }
1400
1401 async fn extract_reader(
1404 &self,
1405 reader: impl AsyncRead + Unpin,
1406 target: &Path,
1407 filename: &String,
1408 ext: SourceDistExtension,
1409 size: Option<u64>,
1410 reporter: Option<&dyn Reporter>,
1411 direction: Direction,
1412 ) -> Result<(), Error> {
1413 let mut hashers = if self.sha256.is_some() {
1414 vec![Hasher::from(HashAlgorithm::Sha256)]
1415 } else {
1416 vec![]
1417 };
1418 let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
1419
1420 if let Some(reporter) = reporter {
1421 let progress_key = reporter.on_request_start(direction, &self.key, size);
1422 let mut reader = ProgressReader::new(&mut hasher, progress_key, reporter);
1423 uv_extract::stream::archive(filename, &mut reader, ext, target)
1424 .await
1425 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1426 reporter.on_request_complete(direction, progress_key);
1427 } else {
1428 uv_extract::stream::archive(filename, &mut hasher, ext, target)
1429 .await
1430 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1431 }
1432 hasher.finish().await.map_err(Error::HashExhaustion)?;
1433
1434 if let Some(expected) = self.sha256.as_deref() {
1436 let actual = HashDigest::from(hashers.pop().unwrap()).digest;
1437 if !actual.eq_ignore_ascii_case(expected) {
1438 return Err(Error::HashMismatch {
1439 installation: self.key.to_string(),
1440 expected: expected.to_string(),
1441 actual: actual.to_string(),
1442 });
1443 }
1444 }
1445
1446 Ok(())
1447 }
1448
1449 pub fn python_version(&self) -> PythonVersion {
1450 self.key.version()
1451 }
1452
1453 pub fn download_urls(
1461 &self,
1462 python_install_mirror: Option<&str>,
1463 pypy_install_mirror: Option<&str>,
1464 ) -> Result<Vec<DisplaySafeUrl>, Error> {
1465 let custom_astral_mirror = astral_mirror_url_from_env();
1466 self.download_urls_with_astral_mirror(
1467 python_install_mirror,
1468 pypy_install_mirror,
1469 custom_astral_mirror.as_deref(),
1470 )
1471 }
1472
1473 fn download_urls_with_astral_mirror(
1474 &self,
1475 python_install_mirror: Option<&str>,
1476 pypy_install_mirror: Option<&str>,
1477 astral_mirror_url: Option<&str>,
1478 ) -> Result<Vec<DisplaySafeUrl>, Error> {
1479 let astral_mirror_url = custom_astral_mirror_url(astral_mirror_url);
1480 match self.key.implementation {
1481 LenientImplementationName::Known(ImplementationName::CPython) => {
1482 if let Some(mirror) = python_install_mirror {
1483 let Some(suffix) = self.url.strip_prefix(CPYTHON_DOWNLOADS_URL_PREFIX) else {
1485 return Err(Error::Mirror(
1486 EnvVars::UV_PYTHON_INSTALL_MIRROR,
1487 self.url.to_string(),
1488 ));
1489 };
1490 return Ok(vec![DisplaySafeUrl::parse(
1491 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1492 )?]);
1493 }
1494 if let Some(suffix) = self.url.strip_prefix(CPYTHON_DOWNLOADS_URL_PREFIX) {
1496 let effective_mirror = effective_cpython_mirror(astral_mirror_url);
1497 let mirror_url = DisplaySafeUrl::parse(
1498 format!("{}/{}", effective_mirror.trim_end_matches('/'), suffix).as_str(),
1499 )?;
1500 if astral_mirror_url.is_some() {
1502 return Ok(vec![mirror_url]);
1503 }
1504 let canonical_url = DisplaySafeUrl::parse(&self.url)?;
1506 return Ok(vec![mirror_url, canonical_url]);
1507 }
1508 }
1509
1510 LenientImplementationName::Known(ImplementationName::PyPy) => {
1511 if let Some(mirror) = pypy_install_mirror {
1512 let Some(suffix) = self.url.strip_prefix("https://downloads.python.org/pypy/")
1513 else {
1514 return Err(Error::Mirror(
1515 EnvVars::UV_PYPY_INSTALL_MIRROR,
1516 self.url.to_string(),
1517 ));
1518 };
1519 return Ok(vec![DisplaySafeUrl::parse(
1520 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1521 )?]);
1522 }
1523 }
1524
1525 _ => {}
1526 }
1527
1528 Ok(vec![DisplaySafeUrl::parse(&self.url)?])
1529 }
1530}
1531
1532fn parse_json_downloads(
1533 json_downloads: HashMap<String, JsonPythonDownload>,
1534) -> Vec<ManagedPythonDownload> {
1535 json_downloads
1536 .into_iter()
1537 .filter_map(|(key, entry)| {
1538 let implementation = match entry.name.as_str() {
1539 "cpython" => LenientImplementationName::Known(ImplementationName::CPython),
1540 "pypy" => LenientImplementationName::Known(ImplementationName::PyPy),
1541 "graalpy" => LenientImplementationName::Known(ImplementationName::GraalPy),
1542 _ => LenientImplementationName::Unknown(entry.name.clone()),
1543 };
1544
1545 let arch_str = match entry.arch.family.as_str() {
1546 "armv5tel" => "armv5te".to_string(),
1547 "riscv64" => "riscv64gc".to_string(),
1551 value => value.to_string(),
1552 };
1553
1554 let arch_str = if let Some(variant) = entry.arch.variant {
1555 format!("{arch_str}_{variant}")
1556 } else {
1557 arch_str
1558 };
1559
1560 let arch = match Arch::from_str(&arch_str) {
1561 Ok(arch) => arch,
1562 Err(e) => {
1563 debug!("Skipping entry {key}: Invalid arch '{arch_str}' - {e}");
1564 return None;
1565 }
1566 };
1567
1568 let os = match Os::from_str(&entry.os) {
1569 Ok(os) => os,
1570 Err(e) => {
1571 debug!("Skipping entry {}: Invalid OS '{}' - {}", key, entry.os, e);
1572 return None;
1573 }
1574 };
1575
1576 let libc = match Libc::from_str(&entry.libc) {
1577 Ok(libc) => libc,
1578 Err(e) => {
1579 debug!(
1580 "Skipping entry {}: Invalid libc '{}' - {}",
1581 key, entry.libc, e
1582 );
1583 return None;
1584 }
1585 };
1586
1587 let variant = match entry
1588 .variant
1589 .as_deref()
1590 .map(PythonVariant::from_str)
1591 .transpose()
1592 {
1593 Ok(Some(variant)) => variant,
1594 Ok(None) => PythonVariant::default(),
1595 Err(()) => {
1596 debug!(
1597 "Skipping entry {key}: Unknown python variant - {}",
1598 entry.variant.unwrap_or_default()
1599 );
1600 return None;
1601 }
1602 };
1603
1604 let version_str = format!(
1605 "{}.{}.{}{}",
1606 entry.major,
1607 entry.minor,
1608 entry.patch,
1609 entry.prerelease.as_deref().unwrap_or_default()
1610 );
1611
1612 let version = match PythonVersion::from_str(&version_str) {
1613 Ok(version) => version,
1614 Err(e) => {
1615 debug!("Skipping entry {key}: Invalid version '{version_str}' - {e}");
1616 return None;
1617 }
1618 };
1619
1620 let url = Cow::Owned(entry.url);
1621 let sha256 = entry.sha256.map(Cow::Owned);
1622 let build = entry
1623 .build
1624 .map(|s| Box::leak(s.into_boxed_str()) as &'static str);
1625
1626 Some(ManagedPythonDownload {
1627 key: PythonInstallationKey::new_from_version(
1628 implementation,
1629 &version,
1630 Platform::new(os, arch, libc),
1631 variant,
1632 ),
1633 url,
1634 sha256,
1635 build,
1636 })
1637 })
1638 .sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
1639 .collect()
1640}
1641
1642impl Error {
1643 fn from_reqwest(
1644 url: DisplaySafeUrl,
1645 err: reqwest::Error,
1646 retries: Option<u32>,
1647 start: Instant,
1648 ) -> Self {
1649 let err = Self::NetworkError(url, WrappedReqwestError::from(err));
1650 if let Some(retries) = retries {
1651 Self::NetworkErrorWithRetries {
1652 err: Box::new(err),
1653 retries,
1654 duration: start.elapsed(),
1655 }
1656 } else {
1657 err
1658 }
1659 }
1660
1661 fn from_reqwest_middleware(url: DisplaySafeUrl, err: reqwest_middleware::Error) -> Self {
1662 match err {
1663 reqwest_middleware::Error::Middleware(error) => {
1664 Self::NetworkMiddlewareError(url, error)
1665 }
1666 reqwest_middleware::Error::Reqwest(error) => {
1667 Self::NetworkError(url, WrappedReqwestError::from(error))
1668 }
1669 }
1670 }
1671}
1672
1673impl Display for ManagedPythonDownload {
1674 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1675 write!(f, "{}", self.key)
1676 }
1677}
1678
1679#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1680pub enum Direction {
1681 Download,
1682 Extract,
1683}
1684
1685impl Direction {
1686 fn as_str(&self) -> &str {
1687 match self {
1688 Self::Download => "download",
1689 Self::Extract => "extract",
1690 }
1691 }
1692}
1693
1694impl Display for Direction {
1695 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1696 f.write_str(self.as_str())
1697 }
1698}
1699
1700pub trait Reporter: Send + Sync {
1701 fn on_request_start(
1702 &self,
1703 direction: Direction,
1704 name: &PythonInstallationKey,
1705 size: Option<u64>,
1706 ) -> usize;
1707 fn on_request_progress(&self, id: usize, inc: u64);
1708 fn on_request_complete(&self, direction: Direction, id: usize);
1709}
1710
1711struct ProgressReader<'a, R> {
1713 reader: R,
1714 index: usize,
1715 reporter: &'a dyn Reporter,
1716}
1717
1718impl<'a, R> ProgressReader<'a, R> {
1719 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1721 Self {
1722 reader,
1723 index,
1724 reporter,
1725 }
1726 }
1727}
1728
1729impl<R> AsyncRead for ProgressReader<'_, R>
1730where
1731 R: AsyncRead + Unpin,
1732{
1733 fn poll_read(
1734 mut self: Pin<&mut Self>,
1735 cx: &mut Context<'_>,
1736 buf: &mut ReadBuf<'_>,
1737 ) -> Poll<io::Result<()>> {
1738 Pin::new(&mut self.as_mut().reader)
1739 .poll_read(cx, buf)
1740 .map_ok(|()| {
1741 self.reporter
1742 .on_request_progress(self.index, buf.filled().len() as u64);
1743 })
1744 }
1745}
1746
1747async fn read_url(
1749 url: &DisplaySafeUrl,
1750 client: &BaseClient,
1751) -> Result<(impl AsyncRead + Unpin, Option<u64>), Error> {
1752 if url.scheme() == "file" {
1753 let path = url
1755 .to_file_path()
1756 .map_err(|()| Error::InvalidFileUrl(url.to_string()))?;
1757
1758 let size = fs_err::tokio::metadata(&path).await?.len();
1759 let reader = fs_err::tokio::File::open(&path).await?;
1760
1761 Ok((Either::Left(reader), Some(size)))
1762 } else {
1763 let start = Instant::now();
1764 let response = client
1765 .for_host(url)
1766 .get(Url::from(url.clone()))
1767 .send()
1768 .await
1769 .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?;
1770
1771 let retry_count = response
1772 .extensions()
1773 .get::<reqwest_retry::RetryCount>()
1774 .map(|retries| retries.value());
1775
1776 let response = response
1778 .error_for_status()
1779 .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count, start))?;
1780
1781 let size = response.content_length();
1782 let stream = response
1783 .bytes_stream()
1784 .map_err(io::Error::other)
1785 .into_async_read();
1786
1787 Ok((Either::Right(stream.compat()), size))
1788 }
1789}
1790
1791#[cfg(test)]
1792mod tests {
1793 use std::collections::HashSet;
1794
1795 use crate::PythonVariant;
1796 use crate::implementation::LenientImplementationName;
1797 use crate::installation::PythonInstallationKey;
1798 use uv_platform::{Arch, Libc, Os, Platform};
1799
1800 use super::*;
1801
1802 #[test]
1804 fn test_python_download_request_from_str_complete() {
1805 let request = PythonDownloadRequest::from_str("cpython-3.12.0-linux-x86_64-gnu")
1806 .expect("Test request should be parsed");
1807
1808 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1809 assert_eq!(
1810 request.version,
1811 Some(VersionRequest::from_str("3.12.0").unwrap())
1812 );
1813 assert_eq!(
1814 request.os,
1815 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1816 );
1817 assert_eq!(
1818 request.arch,
1819 Some(ArchRequest::Explicit(Arch::new(
1820 target_lexicon::Architecture::X86_64,
1821 None
1822 )))
1823 );
1824 assert_eq!(
1825 request.libc,
1826 Some(Libc::Some(target_lexicon::Environment::Gnu))
1827 );
1828 }
1829
1830 #[test]
1832 fn test_python_download_request_from_str_with_any() {
1833 let request = PythonDownloadRequest::from_str("any-3.11-any-x86_64-any")
1834 .expect("Test request should be parsed");
1835
1836 assert_eq!(request.implementation, None);
1837 assert_eq!(
1838 request.version,
1839 Some(VersionRequest::from_str("3.11").unwrap())
1840 );
1841 assert_eq!(request.os, None);
1842 assert_eq!(
1843 request.arch,
1844 Some(ArchRequest::Explicit(Arch::new(
1845 target_lexicon::Architecture::X86_64,
1846 None
1847 )))
1848 );
1849 assert_eq!(request.libc, None);
1850 }
1851
1852 #[test]
1854 fn test_python_download_request_from_str_missing_segment() {
1855 let request =
1856 PythonDownloadRequest::from_str("pypy-linux").expect("Test request should be parsed");
1857
1858 assert_eq!(request.implementation, Some(ImplementationName::PyPy));
1859 assert_eq!(request.version, None);
1860 assert_eq!(
1861 request.os,
1862 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1863 );
1864 assert_eq!(request.arch, None);
1865 assert_eq!(request.libc, None);
1866 }
1867
1868 #[test]
1869 fn test_python_download_request_from_str_version_only() {
1870 let request =
1871 PythonDownloadRequest::from_str("3.10.5").expect("Test request should be parsed");
1872
1873 assert_eq!(request.implementation, None);
1874 assert_eq!(
1875 request.version,
1876 Some(VersionRequest::from_str("3.10.5").unwrap())
1877 );
1878 assert_eq!(request.os, None);
1879 assert_eq!(request.arch, None);
1880 assert_eq!(request.libc, None);
1881 }
1882
1883 #[test]
1884 fn test_python_download_request_from_str_implementation_only() {
1885 let request =
1886 PythonDownloadRequest::from_str("cpython").expect("Test request should be parsed");
1887
1888 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1889 assert_eq!(request.version, None);
1890 assert_eq!(request.os, None);
1891 assert_eq!(request.arch, None);
1892 assert_eq!(request.libc, None);
1893 }
1894
1895 #[test]
1897 fn test_python_download_request_from_str_os_arch() {
1898 let request = PythonDownloadRequest::from_str("windows-x86_64")
1899 .expect("Test request should be parsed");
1900
1901 assert_eq!(request.implementation, None);
1902 assert_eq!(request.version, None);
1903 assert_eq!(
1904 request.os,
1905 Some(Os::new(target_lexicon::OperatingSystem::Windows))
1906 );
1907 assert_eq!(
1908 request.arch,
1909 Some(ArchRequest::Explicit(Arch::new(
1910 target_lexicon::Architecture::X86_64,
1911 None
1912 )))
1913 );
1914 assert_eq!(request.libc, None);
1915 }
1916
1917 #[test]
1919 fn test_python_download_request_from_str_prerelease() {
1920 let request = PythonDownloadRequest::from_str("cpython-3.13.0rc1")
1921 .expect("Test request should be parsed");
1922
1923 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1924 assert_eq!(
1925 request.version,
1926 Some(VersionRequest::from_str("3.13.0rc1").unwrap())
1927 );
1928 assert_eq!(request.os, None);
1929 assert_eq!(request.arch, None);
1930 assert_eq!(request.libc, None);
1931 }
1932
1933 #[test]
1935 fn test_python_download_request_from_str_too_many_parts() {
1936 let result = PythonDownloadRequest::from_str("cpython-3.12-linux-x86_64-gnu-extra");
1937
1938 assert!(matches!(result, Err(Error::TooManyParts(_))));
1939 }
1940
1941 #[test]
1943 fn test_python_download_request_from_str_empty() {
1944 let result = PythonDownloadRequest::from_str("");
1945
1946 assert!(matches!(result, Err(Error::EmptyRequest)), "{result:?}");
1947 }
1948
1949 #[test]
1951 fn test_python_download_request_from_str_all_any() {
1952 let request = PythonDownloadRequest::from_str("any-any-any-any-any")
1953 .expect("Test request should be parsed");
1954
1955 assert_eq!(request.implementation, None);
1956 assert_eq!(request.version, None);
1957 assert_eq!(request.os, None);
1958 assert_eq!(request.arch, None);
1959 assert_eq!(request.libc, None);
1960 }
1961
1962 #[test]
1964 fn test_python_download_request_from_str_case_insensitive_any() {
1965 let request = PythonDownloadRequest::from_str("ANY-3.11-Any-x86_64-aNy")
1966 .expect("Test request should be parsed");
1967
1968 assert_eq!(request.implementation, None);
1969 assert_eq!(
1970 request.version,
1971 Some(VersionRequest::from_str("3.11").unwrap())
1972 );
1973 assert_eq!(request.os, None);
1974 assert_eq!(
1975 request.arch,
1976 Some(ArchRequest::Explicit(Arch::new(
1977 target_lexicon::Architecture::X86_64,
1978 None
1979 )))
1980 );
1981 assert_eq!(request.libc, None);
1982 }
1983
1984 #[test]
1986 fn test_python_download_request_from_str_invalid_leading_segment() {
1987 let result = PythonDownloadRequest::from_str("foobar-3.14-windows");
1988
1989 assert!(
1990 matches!(result, Err(Error::ImplementationError(_))),
1991 "{result:?}"
1992 );
1993 }
1994
1995 #[test]
1997 fn test_python_download_request_from_str_out_of_order() {
1998 let result = PythonDownloadRequest::from_str("3.12-cpython");
1999
2000 assert!(
2001 matches!(result, Err(Error::InvalidRequestPlatform(_))),
2002 "{result:?}"
2003 );
2004 }
2005
2006 #[test]
2008 fn test_python_download_request_from_str_too_many_any() {
2009 let result = PythonDownloadRequest::from_str("any-any-any-any-any-any");
2010
2011 assert!(matches!(result, Err(Error::TooManyParts(_))));
2012 }
2013
2014 #[tokio::test]
2016 async fn test_python_download_request_build_filtering() {
2017 let mut request = PythonDownloadRequest::default()
2018 .with_version(VersionRequest::from_str("3.12").unwrap())
2019 .with_implementation(ImplementationName::CPython);
2020 request.build = Some("20240814".to_string());
2021
2022 let client = uv_client::BaseClientBuilder::default()
2023 .build()
2024 .expect("failed to build base client");
2025 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
2026
2027 let downloads: Vec<_> = download_list
2028 .iter_all()
2029 .filter(|d| request.satisfied_by_download(d))
2030 .collect();
2031
2032 assert!(
2033 !downloads.is_empty(),
2034 "Should find at least one matching download"
2035 );
2036 for download in downloads {
2037 assert_eq!(download.build(), Some("20240814"));
2038 }
2039 }
2040
2041 #[tokio::test]
2043 async fn test_python_download_request_invalid_build() {
2044 let mut request = PythonDownloadRequest::default()
2046 .with_version(VersionRequest::from_str("3.12").unwrap())
2047 .with_implementation(ImplementationName::CPython);
2048 request.build = Some("99999999".to_string());
2049
2050 let client = uv_client::BaseClientBuilder::default()
2051 .build()
2052 .expect("failed to build base client");
2053 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
2054
2055 let downloads: Vec<_> = download_list
2057 .iter_all()
2058 .filter(|d| request.satisfied_by_download(d))
2059 .collect();
2060
2061 assert_eq!(downloads.len(), 0);
2062 }
2063
2064 #[test]
2065 fn upgrade_request_native_defaults() {
2066 let request = PythonDownloadRequest::default()
2067 .with_implementation(ImplementationName::CPython)
2068 .with_version(VersionRequest::MajorMinorPatch(
2069 3,
2070 13,
2071 1,
2072 PythonVariant::Default,
2073 ))
2074 .with_os(Os::from_str("linux").unwrap())
2075 .with_arch(Arch::from_str("x86_64").unwrap())
2076 .with_libc(Libc::from_str("gnu").unwrap())
2077 .with_prereleases(false);
2078
2079 let host = Platform::new(
2080 Os::from_str("linux").unwrap(),
2081 Arch::from_str("x86_64").unwrap(),
2082 Libc::from_str("gnu").unwrap(),
2083 );
2084
2085 assert_eq!(
2086 request
2087 .clone()
2088 .unset_defaults_for_host(&host)
2089 .without_patch()
2090 .simplified_display()
2091 .as_deref(),
2092 Some("3.13")
2093 );
2094 }
2095
2096 #[test]
2097 fn upgrade_request_preserves_variant() {
2098 let request = PythonDownloadRequest::default()
2099 .with_implementation(ImplementationName::CPython)
2100 .with_version(VersionRequest::MajorMinorPatch(
2101 3,
2102 13,
2103 0,
2104 PythonVariant::Freethreaded,
2105 ))
2106 .with_os(Os::from_str("linux").unwrap())
2107 .with_arch(Arch::from_str("x86_64").unwrap())
2108 .with_libc(Libc::from_str("gnu").unwrap())
2109 .with_prereleases(false);
2110
2111 let host = Platform::new(
2112 Os::from_str("linux").unwrap(),
2113 Arch::from_str("x86_64").unwrap(),
2114 Libc::from_str("gnu").unwrap(),
2115 );
2116
2117 assert_eq!(
2118 request
2119 .clone()
2120 .unset_defaults_for_host(&host)
2121 .without_patch()
2122 .simplified_display()
2123 .as_deref(),
2124 Some("3.13+freethreaded")
2125 );
2126 }
2127
2128 #[test]
2129 fn upgrade_request_preserves_non_default_platform() {
2130 let request = PythonDownloadRequest::default()
2131 .with_implementation(ImplementationName::CPython)
2132 .with_version(VersionRequest::MajorMinorPatch(
2133 3,
2134 12,
2135 4,
2136 PythonVariant::Default,
2137 ))
2138 .with_os(Os::from_str("linux").unwrap())
2139 .with_arch(Arch::from_str("aarch64").unwrap())
2140 .with_libc(Libc::from_str("gnu").unwrap())
2141 .with_prereleases(false);
2142
2143 let host = Platform::new(
2144 Os::from_str("linux").unwrap(),
2145 Arch::from_str("x86_64").unwrap(),
2146 Libc::from_str("gnu").unwrap(),
2147 );
2148
2149 assert_eq!(
2150 request
2151 .clone()
2152 .unset_defaults_for_host(&host)
2153 .without_patch()
2154 .simplified_display()
2155 .as_deref(),
2156 Some("3.12-aarch64")
2157 );
2158 }
2159
2160 #[test]
2161 fn upgrade_request_preserves_custom_implementation() {
2162 let request = PythonDownloadRequest::default()
2163 .with_implementation(ImplementationName::PyPy)
2164 .with_version(VersionRequest::MajorMinorPatch(
2165 3,
2166 10,
2167 5,
2168 PythonVariant::Default,
2169 ))
2170 .with_os(Os::from_str("linux").unwrap())
2171 .with_arch(Arch::from_str("x86_64").unwrap())
2172 .with_libc(Libc::from_str("gnu").unwrap())
2173 .with_prereleases(false);
2174
2175 let host = Platform::new(
2176 Os::from_str("linux").unwrap(),
2177 Arch::from_str("x86_64").unwrap(),
2178 Libc::from_str("gnu").unwrap(),
2179 );
2180
2181 assert_eq!(
2182 request
2183 .clone()
2184 .unset_defaults_for_host(&host)
2185 .without_patch()
2186 .simplified_display()
2187 .as_deref(),
2188 Some("pypy-3.10")
2189 );
2190 }
2191
2192 #[test]
2193 fn simplified_display_returns_none_when_empty() {
2194 let request = PythonDownloadRequest::default()
2195 .fill_platform()
2196 .expect("should populate defaults");
2197
2198 let host = Platform::from_env().expect("host platform");
2199
2200 assert_eq!(
2201 request.unset_defaults_for_host(&host).simplified_display(),
2202 None
2203 );
2204 }
2205
2206 #[test]
2207 fn simplified_display_omits_environment_arch() {
2208 let mut request = PythonDownloadRequest::default()
2209 .with_version(VersionRequest::MajorMinor(3, 12, PythonVariant::Default))
2210 .with_os(Os::from_str("linux").unwrap())
2211 .with_libc(Libc::from_str("gnu").unwrap());
2212
2213 request.arch = Some(ArchRequest::Environment(Arch::from_str("x86_64").unwrap()));
2214
2215 let host = Platform::new(
2216 Os::from_str("linux").unwrap(),
2217 Arch::from_str("aarch64").unwrap(),
2218 Libc::from_str("gnu").unwrap(),
2219 );
2220
2221 assert_eq!(
2222 request
2223 .unset_defaults_for_host(&host)
2224 .simplified_display()
2225 .as_deref(),
2226 Some("3.12")
2227 );
2228 }
2229
2230 fn cpython_download_for_url(url: &'static str) -> ManagedPythonDownload {
2231 let key = PythonInstallationKey::new(
2232 LenientImplementationName::Known(crate::implementation::ImplementationName::CPython),
2233 3,
2234 12,
2235 4,
2236 None,
2237 Platform::new(
2238 Os::from_str("linux").unwrap(),
2239 Arch::from_str("x86_64").unwrap(),
2240 Libc::from_str("gnu").unwrap(),
2241 ),
2242 crate::PythonVariant::default(),
2243 );
2244
2245 ManagedPythonDownload {
2246 key,
2247 url: Cow::Borrowed(url),
2248 sha256: Some(Cow::Borrowed("abc123")),
2249 build: Some("20240713"),
2250 }
2251 }
2252
2253 #[test]
2254 fn test_cpython_download_urls_custom_astral_mirror() {
2255 let download = cpython_download_for_url(
2256 "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",
2257 );
2258
2259 let urls = download
2260 .download_urls_with_astral_mirror(
2261 None,
2262 None,
2263 Some("https://nexus.example.com/repository/releases.astral.sh/"),
2264 )
2265 .expect("download URLs should be valid");
2266 let urls = urls
2267 .into_iter()
2268 .map(|url| url.to_string())
2269 .collect::<Vec<_>>();
2270 assert_eq!(
2271 urls,
2272 vec![
2273 "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"
2274 .to_string(),
2275 ]
2276 );
2277 }
2278
2279 #[test]
2280 fn test_cpython_specific_mirror_takes_precedence_over_astral_mirror() {
2281 let download = cpython_download_for_url(
2282 "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",
2283 );
2284
2285 let urls = download
2286 .download_urls_with_astral_mirror(
2287 Some("https://python-mirror.example.com/releases/"),
2288 None,
2289 Some("https://nexus.example.com/repository/releases.astral.sh/"),
2290 )
2291 .expect("download URLs should be valid");
2292 let urls = urls
2293 .into_iter()
2294 .map(|url| url.to_string())
2295 .collect::<Vec<_>>();
2296 assert_eq!(
2297 urls,
2298 vec![
2299 "https://python-mirror.example.com/releases/20240713/cpython-3.12.4%2B20240713-x86_64-unknown-linux-gnu-install_only.tar.gz"
2300 .to_string(),
2301 ]
2302 );
2303 }
2304
2305 #[test]
2306 fn test_cpython_download_urls_empty_astral_mirror_uses_default() {
2307 let download = cpython_download_for_url(
2308 "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",
2309 );
2310
2311 let default_urls = download
2312 .download_urls_with_astral_mirror(None, None, None)
2313 .expect("download URLs should be valid");
2314 let empty_urls = download
2315 .download_urls_with_astral_mirror(None, None, Some(""))
2316 .expect("download URLs should be valid");
2317
2318 assert_eq!(default_urls, empty_urls);
2319 }
2320
2321 #[test]
2324 fn test_should_try_next_url_hash_mismatch() {
2325 let err = Error::HashMismatch {
2326 installation: "cpython-3.12.0".to_string(),
2327 expected: "abc".to_string(),
2328 actual: "def".to_string(),
2329 };
2330 assert!(!err.should_try_next_url());
2331 }
2332
2333 #[test]
2336 fn test_should_try_next_url_extract_error_filesystem() {
2337 let err = Error::ExtractError(
2338 "archive.tar.gz".to_string(),
2339 uv_extract::Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, "")),
2340 );
2341 assert!(!err.should_try_next_url());
2342 }
2343
2344 #[test]
2347 fn test_should_try_next_url_io_error_filesystem() {
2348 let err = Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, ""));
2349 assert!(!err.should_try_next_url());
2350 }
2351
2352 #[test]
2355 fn test_should_try_next_url_io_error_network() {
2356 let err = Error::Io(io::Error::new(io::ErrorKind::ConnectionReset, ""));
2357 assert!(err.should_try_next_url());
2358 }
2359
2360 #[test]
2363 fn test_should_try_next_url_network_error_404() {
2364 let url =
2365 DisplaySafeUrl::from_str("https://releases.astral.sh/python/cpython-3.12.0.tar.gz")
2366 .unwrap();
2367 let wrapped = WrappedReqwestError::with_problem_details(
2370 reqwest_middleware::Error::Middleware(anyhow::anyhow!("404 Not Found")),
2371 None,
2372 );
2373 let err = Error::NetworkError(url, wrapped);
2374 assert!(err.should_try_next_url());
2375 }
2376
2377 #[test]
2380 fn embedded_download_versions_convert_to_version_requests() {
2381 let downloads = ManagedPythonDownloadList::new_only_embedded()
2382 .expect("embedded download metadata should load");
2383
2384 let unique_versions: HashSet<PythonVersion> = downloads
2385 .iter_all()
2386 .map(ManagedPythonDownload::python_version)
2387 .collect();
2388
2389 for version in &unique_versions {
2390 let _ = VersionRequest::from(version);
2391 }
2392 }
2393}