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<Error>,
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<Error>),
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 loop {
780 let Some(part) = state.part else { break };
781 match state.position {
782 Position::Start => unreachable!("We start before the loop"),
783 Position::Implementation => {
784 if part.eq_ignore_ascii_case("any") {
785 state.next_part();
786 continue;
787 }
788 match ImplementationName::from_str(part) {
789 Ok(val) => {
790 implementation = Some(val);
791 state.next_part();
792 }
793 Err(err) => {
794 state.next_position();
795 state.record_err(err.into());
796 }
797 }
798 }
799 Position::Version => {
800 if part.eq_ignore_ascii_case("any") {
801 state.next_part();
802 continue;
803 }
804 match VersionRequest::from_str(part)
805 .map_err(|_| Error::InvalidPythonVersion(part.to_string()))
806 {
807 Ok(val) => {
809 version = Some(val);
810 state.next_part();
811 }
812 Err(err) => {
813 state.next_position();
814 state.record_err(err);
815 }
816 }
817 }
818 Position::Os => {
819 if part.eq_ignore_ascii_case("any") {
820 state.next_part();
821 continue;
822 }
823 match Os::from_str(part) {
824 Ok(val) => {
825 os = Some(val);
826 state.next_part();
827 }
828 Err(err) => {
829 state.next_position();
830 state.record_err(err.into());
831 }
832 }
833 }
834 Position::Arch => {
835 if part.eq_ignore_ascii_case("any") {
836 state.next_part();
837 continue;
838 }
839 match Arch::from_str(part) {
840 Ok(val) => {
841 arch = Some(ArchRequest::Explicit(val));
842 state.next_part();
843 }
844 Err(err) => {
845 state.next_position();
846 state.record_err(err.into());
847 }
848 }
849 }
850 Position::Libc => {
851 if part.eq_ignore_ascii_case("any") {
852 state.next_part();
853 continue;
854 }
855 match Libc::from_str(part) {
856 Ok(val) => {
857 libc = Some(val);
858 state.next_part();
859 }
860 Err(err) => {
861 state.next_position();
862 state.record_err(err.into());
863 }
864 }
865 }
866 Position::End => {
867 if state.count > 5 {
868 return Err(Error::TooManyParts(s.to_string()));
869 }
870
871 if let Some(err) = state.error {
878 return Err(err);
879 }
880 state.next_part();
881 }
882 }
883 }
884
885 Ok(Self::new(version, implementation, arch, os, libc, None))
886 }
887}
888
889const BUILTIN_PYTHON_DOWNLOADS_JSON: &[u8] =
890 include_bytes!(concat!(env!("OUT_DIR"), "/download-metadata-minified.json"));
891
892pub struct ManagedPythonDownloadList {
893 downloads: Vec<ManagedPythonDownload>,
894}
895
896#[derive(Debug, Deserialize, Clone)]
897struct JsonPythonDownload {
898 name: String,
899 arch: JsonArch,
900 os: String,
901 libc: String,
902 major: u8,
903 minor: u8,
904 patch: u8,
905 prerelease: Option<String>,
906 url: String,
907 sha256: Option<String>,
908 variant: Option<String>,
909 build: Option<String>,
910}
911
912#[derive(Debug, Deserialize, Clone)]
913struct JsonArch {
914 family: String,
915 variant: Option<String>,
916}
917
918#[derive(Debug, Clone)]
919pub enum DownloadResult {
920 AlreadyAvailable(PathBuf),
921 Fetched(PathBuf),
922}
923
924pub struct ManagedPythonDownloadWithBuild<'a>(&'a ManagedPythonDownload);
926
927impl Display for ManagedPythonDownloadWithBuild<'_> {
928 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
929 if let Some(build) = self.0.build {
930 write!(f, "{}+{}", self.0.key, build)
931 } else {
932 write!(f, "{}", self.0.key)
933 }
934 }
935}
936
937impl ManagedPythonDownloadList {
938 fn iter_all(&self) -> impl Iterator<Item = &ManagedPythonDownload> {
940 self.downloads.iter()
941 }
942
943 pub fn iter_matching(
945 &self,
946 request: &PythonDownloadRequest,
947 ) -> impl Iterator<Item = &ManagedPythonDownload> {
948 self.iter_all()
949 .filter(move |download| request.satisfied_by_download(download))
950 }
951
952 pub fn find(&self, request: &PythonDownloadRequest) -> Result<&ManagedPythonDownload, Error> {
957 if let Some(download) = self.iter_matching(request).next() {
958 return Ok(download);
959 }
960
961 if !request.allows_prereleases() {
962 if let Some(download) = self
963 .iter_matching(&request.clone().with_prereleases(true))
964 .next()
965 {
966 return Ok(download);
967 }
968 }
969
970 Err(Error::NoDownloadFound(request.clone()))
971 }
972
973 pub async fn new(
982 client: &BaseClient,
983 python_downloads_json_url: Option<&str>,
984 ) -> Result<Self, Error> {
985 enum Source<'a> {
990 BuiltIn,
991 Path(Cow<'a, Path>),
992 Http(DisplaySafeUrl),
993 }
994
995 let json_source = if let Some(url_or_path) = python_downloads_json_url {
996 if let Ok(url) = DisplaySafeUrl::parse(url_or_path) {
997 match url.scheme() {
998 "http" | "https" => Source::Http(url),
999 "file" => Source::Path(Cow::Owned(
1000 url.to_file_path().or(Err(Error::InvalidUrlFormat(url)))?,
1001 )),
1002 _ => Source::Path(Cow::Borrowed(Path::new(url_or_path))),
1003 }
1004 } else {
1005 Source::Path(Cow::Borrowed(Path::new(url_or_path)))
1006 }
1007 } else {
1008 Source::BuiltIn
1009 };
1010
1011 let buf: Cow<'_, [u8]> = match json_source {
1012 Source::BuiltIn => BUILTIN_PYTHON_DOWNLOADS_JSON.into(),
1013 Source::Path(ref path) => fs_err::read(path.as_ref())?.into(),
1014 Source::Http(ref url) => fetch_bytes_from_url(client, url)
1015 .await
1016 .map_err(|e| Error::FetchingPythonDownloadsJSONError(url.to_string(), Box::new(e)))?
1017 .into(),
1018 };
1019 let json_downloads: HashMap<String, JsonPythonDownload> = serde_json::from_slice(&buf)
1020 .map_err(
1021 #[allow(clippy::zero_sized_map_values)]
1028 |e| {
1029 let source = match json_source {
1030 Source::BuiltIn => "EMBEDDED IN THE BINARY".to_owned(),
1031 Source::Path(path) => path.to_string_lossy().to_string(),
1032 Source::Http(url) => url.to_string(),
1033 };
1034 if let Ok(keys) =
1035 serde_json::from_slice::<HashMap<String, serde::de::IgnoredAny>>(&buf)
1036 && keys.contains_key("version")
1037 {
1038 Error::UnsupportedPythonDownloadsJSON(source)
1039 } else {
1040 Error::InvalidPythonDownloadsJSON(source, e)
1041 }
1042 },
1043 )?;
1044
1045 let result = parse_json_downloads(json_downloads);
1046 Ok(Self { downloads: result })
1047 }
1048
1049 pub fn new_only_embedded() -> Result<Self, Error> {
1052 let json_downloads: HashMap<String, JsonPythonDownload> =
1053 serde_json::from_slice(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| {
1054 Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_owned(), e)
1055 })?;
1056 let result = parse_json_downloads(json_downloads);
1057 Ok(Self { downloads: result })
1058 }
1059}
1060
1061async fn fetch_bytes_from_url(client: &BaseClient, url: &DisplaySafeUrl) -> Result<Vec<u8>, Error> {
1062 let (mut reader, size) = read_url(url, client).await?;
1063 let capacity = size.and_then(|s| s.try_into().ok()).unwrap_or(1_048_576);
1064 let mut buf = Vec::with_capacity(capacity);
1065 reader.read_to_end(&mut buf).await?;
1066 Ok(buf)
1067}
1068
1069impl ManagedPythonDownload {
1070 pub fn to_display_with_build(&self) -> ManagedPythonDownloadWithBuild<'_> {
1072 ManagedPythonDownloadWithBuild(self)
1073 }
1074
1075 pub fn url(&self) -> &Cow<'static, str> {
1076 &self.url
1077 }
1078
1079 pub fn key(&self) -> &PythonInstallationKey {
1080 &self.key
1081 }
1082
1083 pub fn os(&self) -> &Os {
1084 self.key.os()
1085 }
1086
1087 pub fn sha256(&self) -> Option<&Cow<'static, str>> {
1088 self.sha256.as_ref()
1089 }
1090
1091 pub fn build(&self) -> Option<&'static str> {
1092 self.build
1093 }
1094
1095 #[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
1097 pub async fn fetch_with_retry(
1098 &self,
1099 client: &BaseClient,
1100 retry_policy: &ExponentialBackoff,
1101 installation_dir: &Path,
1102 scratch_dir: &Path,
1103 reinstall: bool,
1104 python_install_mirror: Option<&str>,
1105 pypy_install_mirror: Option<&str>,
1106 reporter: Option<&dyn Reporter>,
1107 ) -> Result<DownloadResult, Error> {
1108 let mut retry_state = RetryState::start(
1109 *retry_policy,
1110 self.download_url(python_install_mirror, pypy_install_mirror)?,
1111 );
1112
1113 loop {
1114 let result = self
1115 .fetch(
1116 client,
1117 installation_dir,
1118 scratch_dir,
1119 reinstall,
1120 python_install_mirror,
1121 pypy_install_mirror,
1122 reporter,
1123 )
1124 .await;
1125 match result {
1126 Ok(download_result) => return Ok(download_result),
1127 Err(err) => {
1128 if let Some(backoff) = retry_state.should_retry(&err, err.retries()) {
1129 retry_state.sleep_backoff(backoff).await;
1130 continue;
1131 }
1132 return if retry_state.total_retries() > 0 {
1133 Err(Error::NetworkErrorWithRetries {
1134 err: Box::new(err),
1135 retries: retry_state.total_retries(),
1136 })
1137 } else {
1138 Err(err)
1139 };
1140 }
1141 };
1142 }
1143 }
1144
1145 #[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
1147 pub async fn fetch(
1148 &self,
1149 client: &BaseClient,
1150 installation_dir: &Path,
1151 scratch_dir: &Path,
1152 reinstall: bool,
1153 python_install_mirror: Option<&str>,
1154 pypy_install_mirror: Option<&str>,
1155 reporter: Option<&dyn Reporter>,
1156 ) -> Result<DownloadResult, Error> {
1157 let url = self.download_url(python_install_mirror, pypy_install_mirror)?;
1158 let path = installation_dir.join(self.key().to_string());
1159
1160 if !reinstall && path.is_dir() {
1162 return Ok(DownloadResult::AlreadyAvailable(path));
1163 }
1164
1165 let filename = url
1168 .path_segments()
1169 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1170 .next_back()
1171 .ok_or_else(|| Error::InvalidUrlFormat(url.clone()))?
1172 .replace("%2B", "-");
1173 debug_assert!(
1174 filename
1175 .chars()
1176 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'),
1177 "Unexpected char in filename: {filename}"
1178 );
1179 let ext = SourceDistExtension::from_path(&filename)
1180 .map_err(|err| Error::MissingExtension(url.to_string(), err))?;
1181
1182 let temp_dir = tempfile::tempdir_in(scratch_dir).map_err(Error::DownloadDirError)?;
1183
1184 if let Some(python_builds_dir) =
1185 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).filter(|s| !s.is_empty())
1186 {
1187 let python_builds_dir = PathBuf::from(python_builds_dir);
1188 fs_err::create_dir_all(&python_builds_dir)?;
1189 let hash_prefix = match self.sha256.as_deref() {
1190 Some(sha) => {
1191 &sha[..9]
1193 }
1194 None => "none",
1195 };
1196 let target_cache_file = python_builds_dir.join(format!("{hash_prefix}-{filename}"));
1197
1198 let (reader, size): (Box<dyn AsyncRead + Unpin>, Option<u64>) =
1202 match fs_err::tokio::File::open(&target_cache_file).await {
1203 Ok(file) => {
1204 debug!(
1205 "Extracting existing `{}`",
1206 target_cache_file.simplified_display()
1207 );
1208 let size = file.metadata().await?.len();
1209 let reader = Box::new(tokio::io::BufReader::new(file));
1210 (reader, Some(size))
1211 }
1212 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1213 if client.connectivity().is_offline() {
1215 return Err(Error::OfflinePythonMissing {
1216 file: Box::new(self.key().clone()),
1217 url: Box::new(url),
1218 python_builds_dir,
1219 });
1220 }
1221
1222 self.download_archive(
1223 &url,
1224 client,
1225 reporter,
1226 &python_builds_dir,
1227 &target_cache_file,
1228 )
1229 .await?;
1230
1231 debug!("Extracting `{}`", target_cache_file.simplified_display());
1232 let file = fs_err::tokio::File::open(&target_cache_file).await?;
1233 let size = file.metadata().await?.len();
1234 let reader = Box::new(tokio::io::BufReader::new(file));
1235 (reader, Some(size))
1236 }
1237 Err(err) => return Err(err.into()),
1238 };
1239
1240 self.extract_reader(
1242 reader,
1243 temp_dir.path(),
1244 &filename,
1245 ext,
1246 size,
1247 reporter,
1248 Direction::Extract,
1249 )
1250 .await?;
1251 } else {
1252 debug!("Downloading {url}");
1254 debug!(
1255 "Extracting {filename} to temporary location: {}",
1256 temp_dir.path().simplified_display()
1257 );
1258
1259 let (reader, size) = read_url(&url, client).await?;
1260 self.extract_reader(
1261 reader,
1262 temp_dir.path(),
1263 &filename,
1264 ext,
1265 size,
1266 reporter,
1267 Direction::Download,
1268 )
1269 .await?;
1270 }
1271
1272 let mut extracted = match uv_extract::strip_component(temp_dir.path()) {
1274 Ok(top_level) => top_level,
1275 Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.keep(),
1276 Err(err) => return Err(Error::ExtractError(filename, err)),
1277 };
1278
1279 if extracted.join("install").is_dir() {
1281 extracted = extracted.join("install");
1282 } else if self.os().is_emscripten() {
1284 extracted = extracted.join("pyodide-root").join("dist");
1285 }
1286
1287 #[cfg(unix)]
1288 {
1289 if self.os().is_emscripten() {
1293 fs_err::create_dir_all(extracted.join("bin"))?;
1294 fs_err::os::unix::fs::symlink(
1295 "../python",
1296 extracted
1297 .join("bin")
1298 .join(format!("python{}.{}", self.key.major, self.key.minor)),
1299 )?;
1300 }
1301
1302 match fs_err::os::unix::fs::symlink(
1309 format!("python{}.{}", self.key.major, self.key.minor),
1310 extracted.join("bin").join("python"),
1311 ) {
1312 Ok(()) => {}
1313 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
1314 Err(err) => return Err(err.into()),
1315 }
1316 }
1317
1318 if path.is_dir() {
1320 debug!("Removing existing directory: {}", path.user_display());
1321 fs_err::tokio::remove_dir_all(&path).await?;
1322 }
1323
1324 debug!("Moving {} to {}", extracted.display(), path.user_display());
1326 rename_with_retry(extracted, &path)
1327 .await
1328 .map_err(|err| Error::CopyError {
1329 to: path.clone(),
1330 err,
1331 })?;
1332
1333 Ok(DownloadResult::Fetched(path))
1334 }
1335
1336 async fn download_archive(
1338 &self,
1339 url: &DisplaySafeUrl,
1340 client: &BaseClient,
1341 reporter: Option<&dyn Reporter>,
1342 python_builds_dir: &Path,
1343 target_cache_file: &Path,
1344 ) -> Result<(), Error> {
1345 debug!(
1346 "Downloading {} to `{}`",
1347 url,
1348 target_cache_file.simplified_display()
1349 );
1350
1351 let (mut reader, size) = read_url(url, client).await?;
1352 let temp_dir = tempfile::tempdir_in(python_builds_dir)?;
1353 let temp_file = temp_dir.path().join("download");
1354
1355 {
1357 let mut archive_writer = BufWriter::new(fs_err::tokio::File::create(&temp_file).await?);
1358
1359 if let Some(reporter) = reporter {
1361 let key = reporter.on_request_start(Direction::Download, &self.key, size);
1362 tokio::io::copy(
1363 &mut ProgressReader::new(reader, key, reporter),
1364 &mut archive_writer,
1365 )
1366 .await?;
1367 reporter.on_request_complete(Direction::Download, key);
1368 } else {
1369 tokio::io::copy(&mut reader, &mut archive_writer).await?;
1370 }
1371
1372 archive_writer.flush().await?;
1373 }
1374 match rename_with_retry(&temp_file, target_cache_file).await {
1376 Ok(()) => {}
1377 Err(_) if target_cache_file.is_file() => {}
1378 Err(err) => return Err(err.into()),
1379 }
1380 Ok(())
1381 }
1382
1383 async fn extract_reader(
1386 &self,
1387 reader: impl AsyncRead + Unpin,
1388 target: &Path,
1389 filename: &String,
1390 ext: SourceDistExtension,
1391 size: Option<u64>,
1392 reporter: Option<&dyn Reporter>,
1393 direction: Direction,
1394 ) -> Result<(), Error> {
1395 let mut hashers = if self.sha256.is_some() {
1396 vec![Hasher::from(HashAlgorithm::Sha256)]
1397 } else {
1398 vec![]
1399 };
1400 let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
1401
1402 if let Some(reporter) = reporter {
1403 let progress_key = reporter.on_request_start(direction, &self.key, size);
1404 let mut reader = ProgressReader::new(&mut hasher, progress_key, reporter);
1405 uv_extract::stream::archive(&mut reader, ext, target)
1406 .await
1407 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1408 reporter.on_request_complete(direction, progress_key);
1409 } else {
1410 uv_extract::stream::archive(&mut hasher, ext, target)
1411 .await
1412 .map_err(|err| Error::ExtractError(filename.to_owned(), err))?;
1413 }
1414 hasher.finish().await.map_err(Error::HashExhaustion)?;
1415
1416 if let Some(expected) = self.sha256.as_deref() {
1418 let actual = HashDigest::from(hashers.pop().unwrap()).digest;
1419 if !actual.eq_ignore_ascii_case(expected) {
1420 return Err(Error::HashMismatch {
1421 installation: self.key.to_string(),
1422 expected: expected.to_string(),
1423 actual: actual.to_string(),
1424 });
1425 }
1426 }
1427
1428 Ok(())
1429 }
1430
1431 pub fn python_version(&self) -> PythonVersion {
1432 self.key.version()
1433 }
1434
1435 pub fn download_url(
1438 &self,
1439 python_install_mirror: Option<&str>,
1440 pypy_install_mirror: Option<&str>,
1441 ) -> Result<DisplaySafeUrl, Error> {
1442 match self.key.implementation {
1443 LenientImplementationName::Known(ImplementationName::CPython) => {
1444 if let Some(mirror) = python_install_mirror {
1445 let Some(suffix) = self.url.strip_prefix(
1446 "https://github.com/astral-sh/python-build-standalone/releases/download/",
1447 ) else {
1448 return Err(Error::Mirror(
1449 EnvVars::UV_PYTHON_INSTALL_MIRROR,
1450 self.url.to_string(),
1451 ));
1452 };
1453 return Ok(DisplaySafeUrl::parse(
1454 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1455 )?);
1456 }
1457 }
1458
1459 LenientImplementationName::Known(ImplementationName::PyPy) => {
1460 if let Some(mirror) = pypy_install_mirror {
1461 let Some(suffix) = self.url.strip_prefix("https://downloads.python.org/pypy/")
1462 else {
1463 return Err(Error::Mirror(
1464 EnvVars::UV_PYPY_INSTALL_MIRROR,
1465 self.url.to_string(),
1466 ));
1467 };
1468 return Ok(DisplaySafeUrl::parse(
1469 format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
1470 )?);
1471 }
1472 }
1473
1474 _ => {}
1475 }
1476
1477 Ok(DisplaySafeUrl::parse(&self.url)?)
1478 }
1479}
1480
1481fn parse_json_downloads(
1482 json_downloads: HashMap<String, JsonPythonDownload>,
1483) -> Vec<ManagedPythonDownload> {
1484 json_downloads
1485 .into_iter()
1486 .filter_map(|(key, entry)| {
1487 let implementation = match entry.name.as_str() {
1488 "cpython" => LenientImplementationName::Known(ImplementationName::CPython),
1489 "pypy" => LenientImplementationName::Known(ImplementationName::PyPy),
1490 "graalpy" => LenientImplementationName::Known(ImplementationName::GraalPy),
1491 _ => LenientImplementationName::Unknown(entry.name.clone()),
1492 };
1493
1494 let arch_str = match entry.arch.family.as_str() {
1495 "armv5tel" => "armv5te".to_string(),
1496 "riscv64" => "riscv64gc".to_string(),
1500 value => value.to_string(),
1501 };
1502
1503 let arch_str = if let Some(variant) = entry.arch.variant {
1504 format!("{arch_str}_{variant}")
1505 } else {
1506 arch_str
1507 };
1508
1509 let arch = match Arch::from_str(&arch_str) {
1510 Ok(arch) => arch,
1511 Err(e) => {
1512 debug!("Skipping entry {key}: Invalid arch '{arch_str}' - {e}");
1513 return None;
1514 }
1515 };
1516
1517 let os = match Os::from_str(&entry.os) {
1518 Ok(os) => os,
1519 Err(e) => {
1520 debug!("Skipping entry {}: Invalid OS '{}' - {}", key, entry.os, e);
1521 return None;
1522 }
1523 };
1524
1525 let libc = match Libc::from_str(&entry.libc) {
1526 Ok(libc) => libc,
1527 Err(e) => {
1528 debug!(
1529 "Skipping entry {}: Invalid libc '{}' - {}",
1530 key, entry.libc, e
1531 );
1532 return None;
1533 }
1534 };
1535
1536 let variant = match entry
1537 .variant
1538 .as_deref()
1539 .map(PythonVariant::from_str)
1540 .transpose()
1541 {
1542 Ok(Some(variant)) => variant,
1543 Ok(None) => PythonVariant::default(),
1544 Err(()) => {
1545 debug!(
1546 "Skipping entry {key}: Unknown python variant - {}",
1547 entry.variant.unwrap_or_default()
1548 );
1549 return None;
1550 }
1551 };
1552
1553 let version_str = format!(
1554 "{}.{}.{}{}",
1555 entry.major,
1556 entry.minor,
1557 entry.patch,
1558 entry.prerelease.as_deref().unwrap_or_default()
1559 );
1560
1561 let version = match PythonVersion::from_str(&version_str) {
1562 Ok(version) => version,
1563 Err(e) => {
1564 debug!("Skipping entry {key}: Invalid version '{version_str}' - {e}");
1565 return None;
1566 }
1567 };
1568
1569 let url = Cow::Owned(entry.url);
1570 let sha256 = entry.sha256.map(Cow::Owned);
1571 let build = entry
1572 .build
1573 .map(|s| Box::leak(s.into_boxed_str()) as &'static str);
1574
1575 Some(ManagedPythonDownload {
1576 key: PythonInstallationKey::new_from_version(
1577 implementation,
1578 &version,
1579 Platform::new(os, arch, libc),
1580 variant,
1581 ),
1582 url,
1583 sha256,
1584 build,
1585 })
1586 })
1587 .sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
1588 .collect()
1589}
1590
1591impl Error {
1592 pub(crate) fn from_reqwest(
1593 url: DisplaySafeUrl,
1594 err: reqwest::Error,
1595 retries: Option<u32>,
1596 ) -> Self {
1597 let err = Self::NetworkError(url, WrappedReqwestError::from(err));
1598 if let Some(retries) = retries {
1599 Self::NetworkErrorWithRetries {
1600 err: Box::new(err),
1601 retries,
1602 }
1603 } else {
1604 err
1605 }
1606 }
1607
1608 pub(crate) fn from_reqwest_middleware(
1609 url: DisplaySafeUrl,
1610 err: reqwest_middleware::Error,
1611 ) -> Self {
1612 match err {
1613 reqwest_middleware::Error::Middleware(error) => {
1614 Self::NetworkMiddlewareError(url, error)
1615 }
1616 reqwest_middleware::Error::Reqwest(error) => {
1617 Self::NetworkError(url, WrappedReqwestError::from(error))
1618 }
1619 }
1620 }
1621}
1622
1623impl Display for ManagedPythonDownload {
1624 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1625 write!(f, "{}", self.key)
1626 }
1627}
1628
1629#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1630pub enum Direction {
1631 Download,
1632 Extract,
1633}
1634
1635impl Direction {
1636 fn as_str(&self) -> &str {
1637 match self {
1638 Self::Download => "download",
1639 Self::Extract => "extract",
1640 }
1641 }
1642}
1643
1644impl Display for Direction {
1645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1646 f.write_str(self.as_str())
1647 }
1648}
1649
1650pub trait Reporter: Send + Sync {
1651 fn on_request_start(
1652 &self,
1653 direction: Direction,
1654 name: &PythonInstallationKey,
1655 size: Option<u64>,
1656 ) -> usize;
1657 fn on_request_progress(&self, id: usize, inc: u64);
1658 fn on_request_complete(&self, direction: Direction, id: usize);
1659}
1660
1661struct ProgressReader<'a, R> {
1663 reader: R,
1664 index: usize,
1665 reporter: &'a dyn Reporter,
1666}
1667
1668impl<'a, R> ProgressReader<'a, R> {
1669 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1671 Self {
1672 reader,
1673 index,
1674 reporter,
1675 }
1676 }
1677}
1678
1679impl<R> AsyncRead for ProgressReader<'_, R>
1680where
1681 R: AsyncRead + Unpin,
1682{
1683 fn poll_read(
1684 mut self: Pin<&mut Self>,
1685 cx: &mut Context<'_>,
1686 buf: &mut ReadBuf<'_>,
1687 ) -> Poll<io::Result<()>> {
1688 Pin::new(&mut self.as_mut().reader)
1689 .poll_read(cx, buf)
1690 .map_ok(|()| {
1691 self.reporter
1692 .on_request_progress(self.index, buf.filled().len() as u64);
1693 })
1694 }
1695}
1696
1697async fn read_url(
1699 url: &DisplaySafeUrl,
1700 client: &BaseClient,
1701) -> Result<(impl AsyncRead + Unpin, Option<u64>), Error> {
1702 if url.scheme() == "file" {
1703 let path = url
1705 .to_file_path()
1706 .map_err(|()| Error::InvalidFileUrl(url.to_string()))?;
1707
1708 let size = fs_err::tokio::metadata(&path).await?.len();
1709 let reader = fs_err::tokio::File::open(&path).await?;
1710
1711 Ok((Either::Left(reader), Some(size)))
1712 } else {
1713 let response = client
1714 .for_host(url)
1715 .get(Url::from(url.clone()))
1716 .send()
1717 .await
1718 .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?;
1719
1720 let retry_count = response
1721 .extensions()
1722 .get::<reqwest_retry::RetryCount>()
1723 .map(|retries| retries.value());
1724
1725 let response = response
1727 .error_for_status()
1728 .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count))?;
1729
1730 let size = response.content_length();
1731 let stream = response
1732 .bytes_stream()
1733 .map_err(io::Error::other)
1734 .into_async_read();
1735
1736 Ok((Either::Right(stream.compat()), size))
1737 }
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742 use crate::PythonVariant;
1743 use crate::implementation::LenientImplementationName;
1744 use crate::installation::PythonInstallationKey;
1745 use uv_platform::{Arch, Libc, Os, Platform};
1746
1747 use super::*;
1748
1749 #[test]
1751 fn test_python_download_request_from_str_complete() {
1752 let request = PythonDownloadRequest::from_str("cpython-3.12.0-linux-x86_64-gnu")
1753 .expect("Test request should be parsed");
1754
1755 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1756 assert_eq!(
1757 request.version,
1758 Some(VersionRequest::from_str("3.12.0").unwrap())
1759 );
1760 assert_eq!(
1761 request.os,
1762 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1763 );
1764 assert_eq!(
1765 request.arch,
1766 Some(ArchRequest::Explicit(Arch::new(
1767 target_lexicon::Architecture::X86_64,
1768 None
1769 )))
1770 );
1771 assert_eq!(
1772 request.libc,
1773 Some(Libc::Some(target_lexicon::Environment::Gnu))
1774 );
1775 }
1776
1777 #[test]
1779 fn test_python_download_request_from_str_with_any() {
1780 let request = PythonDownloadRequest::from_str("any-3.11-any-x86_64-any")
1781 .expect("Test request should be parsed");
1782
1783 assert_eq!(request.implementation, None);
1784 assert_eq!(
1785 request.version,
1786 Some(VersionRequest::from_str("3.11").unwrap())
1787 );
1788 assert_eq!(request.os, None);
1789 assert_eq!(
1790 request.arch,
1791 Some(ArchRequest::Explicit(Arch::new(
1792 target_lexicon::Architecture::X86_64,
1793 None
1794 )))
1795 );
1796 assert_eq!(request.libc, None);
1797 }
1798
1799 #[test]
1801 fn test_python_download_request_from_str_missing_segment() {
1802 let request =
1803 PythonDownloadRequest::from_str("pypy-linux").expect("Test request should be parsed");
1804
1805 assert_eq!(request.implementation, Some(ImplementationName::PyPy));
1806 assert_eq!(request.version, None);
1807 assert_eq!(
1808 request.os,
1809 Some(Os::new(target_lexicon::OperatingSystem::Linux))
1810 );
1811 assert_eq!(request.arch, None);
1812 assert_eq!(request.libc, None);
1813 }
1814
1815 #[test]
1816 fn test_python_download_request_from_str_version_only() {
1817 let request =
1818 PythonDownloadRequest::from_str("3.10.5").expect("Test request should be parsed");
1819
1820 assert_eq!(request.implementation, None);
1821 assert_eq!(
1822 request.version,
1823 Some(VersionRequest::from_str("3.10.5").unwrap())
1824 );
1825 assert_eq!(request.os, None);
1826 assert_eq!(request.arch, None);
1827 assert_eq!(request.libc, None);
1828 }
1829
1830 #[test]
1831 fn test_python_download_request_from_str_implementation_only() {
1832 let request =
1833 PythonDownloadRequest::from_str("cpython").expect("Test request should be parsed");
1834
1835 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1836 assert_eq!(request.version, None);
1837 assert_eq!(request.os, None);
1838 assert_eq!(request.arch, None);
1839 assert_eq!(request.libc, None);
1840 }
1841
1842 #[test]
1844 fn test_python_download_request_from_str_os_arch() {
1845 let request = PythonDownloadRequest::from_str("windows-x86_64")
1846 .expect("Test request should be parsed");
1847
1848 assert_eq!(request.implementation, None);
1849 assert_eq!(request.version, None);
1850 assert_eq!(
1851 request.os,
1852 Some(Os::new(target_lexicon::OperatingSystem::Windows))
1853 );
1854 assert_eq!(
1855 request.arch,
1856 Some(ArchRequest::Explicit(Arch::new(
1857 target_lexicon::Architecture::X86_64,
1858 None
1859 )))
1860 );
1861 assert_eq!(request.libc, None);
1862 }
1863
1864 #[test]
1866 fn test_python_download_request_from_str_prerelease() {
1867 let request = PythonDownloadRequest::from_str("cpython-3.13.0rc1")
1868 .expect("Test request should be parsed");
1869
1870 assert_eq!(request.implementation, Some(ImplementationName::CPython));
1871 assert_eq!(
1872 request.version,
1873 Some(VersionRequest::from_str("3.13.0rc1").unwrap())
1874 );
1875 assert_eq!(request.os, None);
1876 assert_eq!(request.arch, None);
1877 assert_eq!(request.libc, None);
1878 }
1879
1880 #[test]
1882 fn test_python_download_request_from_str_too_many_parts() {
1883 let result = PythonDownloadRequest::from_str("cpython-3.12-linux-x86_64-gnu-extra");
1884
1885 assert!(matches!(result, Err(Error::TooManyParts(_))));
1886 }
1887
1888 #[test]
1890 fn test_python_download_request_from_str_empty() {
1891 let result = PythonDownloadRequest::from_str("");
1892
1893 assert!(matches!(result, Err(Error::EmptyRequest)), "{result:?}");
1894 }
1895
1896 #[test]
1898 fn test_python_download_request_from_str_all_any() {
1899 let request = PythonDownloadRequest::from_str("any-any-any-any-any")
1900 .expect("Test request should be parsed");
1901
1902 assert_eq!(request.implementation, None);
1903 assert_eq!(request.version, None);
1904 assert_eq!(request.os, None);
1905 assert_eq!(request.arch, None);
1906 assert_eq!(request.libc, None);
1907 }
1908
1909 #[test]
1911 fn test_python_download_request_from_str_case_insensitive_any() {
1912 let request = PythonDownloadRequest::from_str("ANY-3.11-Any-x86_64-aNy")
1913 .expect("Test request should be parsed");
1914
1915 assert_eq!(request.implementation, None);
1916 assert_eq!(
1917 request.version,
1918 Some(VersionRequest::from_str("3.11").unwrap())
1919 );
1920 assert_eq!(request.os, None);
1921 assert_eq!(
1922 request.arch,
1923 Some(ArchRequest::Explicit(Arch::new(
1924 target_lexicon::Architecture::X86_64,
1925 None
1926 )))
1927 );
1928 assert_eq!(request.libc, None);
1929 }
1930
1931 #[test]
1933 fn test_python_download_request_from_str_invalid_leading_segment() {
1934 let result = PythonDownloadRequest::from_str("foobar-3.14-windows");
1935
1936 assert!(
1937 matches!(result, Err(Error::ImplementationError(_))),
1938 "{result:?}"
1939 );
1940 }
1941
1942 #[test]
1944 fn test_python_download_request_from_str_out_of_order() {
1945 let result = PythonDownloadRequest::from_str("3.12-cpython");
1946
1947 assert!(
1948 matches!(result, Err(Error::InvalidRequestPlatform(_))),
1949 "{result:?}"
1950 );
1951 }
1952
1953 #[test]
1955 fn test_python_download_request_from_str_too_many_any() {
1956 let result = PythonDownloadRequest::from_str("any-any-any-any-any-any");
1957
1958 assert!(matches!(result, Err(Error::TooManyParts(_))));
1959 }
1960
1961 #[tokio::test]
1963 async fn test_python_download_request_build_filtering() {
1964 let request = PythonDownloadRequest::default()
1965 .with_version(VersionRequest::from_str("3.12").unwrap())
1966 .with_implementation(ImplementationName::CPython)
1967 .with_build("20240814".to_string());
1968
1969 let client = uv_client::BaseClientBuilder::default().build();
1970 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
1971
1972 let downloads: Vec<_> = download_list
1973 .iter_all()
1974 .filter(|d| request.satisfied_by_download(d))
1975 .collect();
1976
1977 assert!(
1978 !downloads.is_empty(),
1979 "Should find at least one matching download"
1980 );
1981 for download in downloads {
1982 assert_eq!(download.build(), Some("20240814"));
1983 }
1984 }
1985
1986 #[tokio::test]
1988 async fn test_python_download_request_invalid_build() {
1989 let request = PythonDownloadRequest::default()
1991 .with_version(VersionRequest::from_str("3.12").unwrap())
1992 .with_implementation(ImplementationName::CPython)
1993 .with_build("99999999".to_string());
1994
1995 let client = uv_client::BaseClientBuilder::default().build();
1996 let download_list = ManagedPythonDownloadList::new(&client, None).await.unwrap();
1997
1998 let downloads: Vec<_> = download_list
2000 .iter_all()
2001 .filter(|d| request.satisfied_by_download(d))
2002 .collect();
2003
2004 assert_eq!(downloads.len(), 0);
2005 }
2006
2007 #[test]
2008 fn upgrade_request_native_defaults() {
2009 let request = PythonDownloadRequest::default()
2010 .with_implementation(ImplementationName::CPython)
2011 .with_version(VersionRequest::MajorMinorPatch(
2012 3,
2013 13,
2014 1,
2015 PythonVariant::Default,
2016 ))
2017 .with_os(Os::from_str("linux").unwrap())
2018 .with_arch(Arch::from_str("x86_64").unwrap())
2019 .with_libc(Libc::from_str("gnu").unwrap())
2020 .with_prereleases(false);
2021
2022 let host = Platform::new(
2023 Os::from_str("linux").unwrap(),
2024 Arch::from_str("x86_64").unwrap(),
2025 Libc::from_str("gnu").unwrap(),
2026 );
2027
2028 assert_eq!(
2029 request
2030 .clone()
2031 .unset_defaults_for_host(&host)
2032 .without_patch()
2033 .simplified_display()
2034 .as_deref(),
2035 Some("3.13")
2036 );
2037 }
2038
2039 #[test]
2040 fn upgrade_request_preserves_variant() {
2041 let request = PythonDownloadRequest::default()
2042 .with_implementation(ImplementationName::CPython)
2043 .with_version(VersionRequest::MajorMinorPatch(
2044 3,
2045 13,
2046 0,
2047 PythonVariant::Freethreaded,
2048 ))
2049 .with_os(Os::from_str("linux").unwrap())
2050 .with_arch(Arch::from_str("x86_64").unwrap())
2051 .with_libc(Libc::from_str("gnu").unwrap())
2052 .with_prereleases(false);
2053
2054 let host = Platform::new(
2055 Os::from_str("linux").unwrap(),
2056 Arch::from_str("x86_64").unwrap(),
2057 Libc::from_str("gnu").unwrap(),
2058 );
2059
2060 assert_eq!(
2061 request
2062 .clone()
2063 .unset_defaults_for_host(&host)
2064 .without_patch()
2065 .simplified_display()
2066 .as_deref(),
2067 Some("3.13+freethreaded")
2068 );
2069 }
2070
2071 #[test]
2072 fn upgrade_request_preserves_non_default_platform() {
2073 let request = PythonDownloadRequest::default()
2074 .with_implementation(ImplementationName::CPython)
2075 .with_version(VersionRequest::MajorMinorPatch(
2076 3,
2077 12,
2078 4,
2079 PythonVariant::Default,
2080 ))
2081 .with_os(Os::from_str("linux").unwrap())
2082 .with_arch(Arch::from_str("aarch64").unwrap())
2083 .with_libc(Libc::from_str("gnu").unwrap())
2084 .with_prereleases(false);
2085
2086 let host = Platform::new(
2087 Os::from_str("linux").unwrap(),
2088 Arch::from_str("x86_64").unwrap(),
2089 Libc::from_str("gnu").unwrap(),
2090 );
2091
2092 assert_eq!(
2093 request
2094 .clone()
2095 .unset_defaults_for_host(&host)
2096 .without_patch()
2097 .simplified_display()
2098 .as_deref(),
2099 Some("3.12-aarch64")
2100 );
2101 }
2102
2103 #[test]
2104 fn upgrade_request_preserves_custom_implementation() {
2105 let request = PythonDownloadRequest::default()
2106 .with_implementation(ImplementationName::PyPy)
2107 .with_version(VersionRequest::MajorMinorPatch(
2108 3,
2109 10,
2110 5,
2111 PythonVariant::Default,
2112 ))
2113 .with_os(Os::from_str("linux").unwrap())
2114 .with_arch(Arch::from_str("x86_64").unwrap())
2115 .with_libc(Libc::from_str("gnu").unwrap())
2116 .with_prereleases(false);
2117
2118 let host = Platform::new(
2119 Os::from_str("linux").unwrap(),
2120 Arch::from_str("x86_64").unwrap(),
2121 Libc::from_str("gnu").unwrap(),
2122 );
2123
2124 assert_eq!(
2125 request
2126 .clone()
2127 .unset_defaults_for_host(&host)
2128 .without_patch()
2129 .simplified_display()
2130 .as_deref(),
2131 Some("pypy-3.10")
2132 );
2133 }
2134
2135 #[test]
2136 fn simplified_display_returns_none_when_empty() {
2137 let request = PythonDownloadRequest::default()
2138 .fill_platform()
2139 .expect("should populate defaults");
2140
2141 let host = Platform::from_env().expect("host platform");
2142
2143 assert_eq!(
2144 request.unset_defaults_for_host(&host).simplified_display(),
2145 None
2146 );
2147 }
2148
2149 #[test]
2150 fn simplified_display_omits_environment_arch() {
2151 let mut request = PythonDownloadRequest::default()
2152 .with_version(VersionRequest::MajorMinor(3, 12, PythonVariant::Default))
2153 .with_os(Os::from_str("linux").unwrap())
2154 .with_libc(Libc::from_str("gnu").unwrap());
2155
2156 request.arch = Some(ArchRequest::Environment(Arch::from_str("x86_64").unwrap()));
2157
2158 let host = Platform::new(
2159 Os::from_str("linux").unwrap(),
2160 Arch::from_str("aarch64").unwrap(),
2161 Libc::from_str("gnu").unwrap(),
2162 );
2163
2164 assert_eq!(
2165 request
2166 .unset_defaults_for_host(&host)
2167 .simplified_display()
2168 .as_deref(),
2169 Some("3.12")
2170 );
2171 }
2172
2173 #[test]
2175 fn test_managed_python_download_build_display() {
2176 let key = PythonInstallationKey::new(
2178 LenientImplementationName::Known(crate::implementation::ImplementationName::CPython),
2179 3,
2180 12,
2181 0,
2182 None,
2183 Platform::new(
2184 Os::from_str("linux").unwrap(),
2185 Arch::from_str("x86_64").unwrap(),
2186 Libc::from_str("gnu").unwrap(),
2187 ),
2188 crate::PythonVariant::default(),
2189 );
2190
2191 let download_with_build = ManagedPythonDownload {
2192 key,
2193 url: Cow::Borrowed("https://example.com/python.tar.gz"),
2194 sha256: Some(Cow::Borrowed("abc123")),
2195 build: Some("20240101"),
2196 };
2197
2198 assert_eq!(
2200 download_with_build.to_display_with_build().to_string(),
2201 "cpython-3.12.0-linux-x86_64-gnu+20240101"
2202 );
2203
2204 let download_without_build = ManagedPythonDownload {
2206 key: download_with_build.key.clone(),
2207 url: Cow::Borrowed("https://example.com/python.tar.gz"),
2208 sha256: Some(Cow::Borrowed("abc123")),
2209 build: None,
2210 };
2211
2212 assert_eq!(
2214 download_without_build.to_display_with_build().to_string(),
2215 "cpython-3.12.0-linux-x86_64-gnu"
2216 );
2217 }
2218}