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