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