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