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