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