1use std::borrow::Cow;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4use std::str::FromStr;
5
6use indexmap::IndexMap;
7use ref_cast::RefCast;
8use reqwest_retry::policies::ExponentialBackoff;
9use tracing::{debug, info};
10use uv_warnings::warn_user;
11
12use uv_cache::Cache;
13use uv_client::{BaseClient, BaseClientBuilder};
14use uv_pep440::{Prerelease, Version};
15use uv_platform::{Arch, Libc, Os, Platform};
16use uv_preview::Preview;
17
18use crate::discovery::{
19 EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation,
20};
21use crate::downloads::{
22 DownloadResult, ManagedPythonDownload, ManagedPythonDownloadList, PythonDownloadRequest,
23 Reporter,
24};
25use crate::implementation::LenientImplementationName;
26use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
27use crate::{
28 Error, ImplementationName, Interpreter, PythonDownloads, PythonPreference, PythonSource,
29 PythonVariant, PythonVersion, downloads,
30};
31
32#[derive(Clone, Debug)]
34pub struct PythonInstallation {
35 pub(crate) source: PythonSource,
37 pub(crate) interpreter: Interpreter,
38}
39
40impl PythonInstallation {
41 pub(crate) fn from_tuple(tuple: (PythonSource, Interpreter)) -> Self {
43 let (source, interpreter) = tuple;
44 Self {
45 source,
46 interpreter,
47 }
48 }
49
50 pub fn find(
63 request: &PythonRequest,
64 environments: EnvironmentPreference,
65 preference: PythonPreference,
66 download_list: &ManagedPythonDownloadList,
67 cache: &Cache,
68 preview: Preview,
69 ) -> Result<Self, Error> {
70 let installation =
71 find_python_installation(request, environments, preference, cache, preview)??;
72 installation.warn_if_outdated_prerelease(request, download_list);
73 Ok(installation)
74 }
75
76 pub fn find_best(
79 request: &PythonRequest,
80 environments: EnvironmentPreference,
81 preference: PythonPreference,
82 download_list: &ManagedPythonDownloadList,
83 cache: &Cache,
84 preview: Preview,
85 ) -> Result<Self, Error> {
86 let installation =
87 find_best_python_installation(request, environments, preference, cache, preview)??;
88 installation.warn_if_outdated_prerelease(request, download_list);
89 Ok(installation)
90 }
91
92 pub async fn find_or_download(
96 request: Option<&PythonRequest>,
97 environments: EnvironmentPreference,
98 preference: PythonPreference,
99 python_downloads: PythonDownloads,
100 client_builder: &BaseClientBuilder<'_>,
101 cache: &Cache,
102 reporter: Option<&dyn Reporter>,
103 python_install_mirror: Option<&str>,
104 pypy_install_mirror: Option<&str>,
105 python_downloads_json_url: Option<&str>,
106 preview: Preview,
107 ) -> Result<Self, Error> {
108 let request = request.unwrap_or(&PythonRequest::Default);
109
110 let retry_policy = client_builder.retry_policy();
113 let client = client_builder.clone().retries(0).build();
114 let download_list =
115 ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?;
116
117 let err = match Self::find(
119 request,
120 environments,
121 preference,
122 &download_list,
123 cache,
124 preview,
125 ) {
126 Ok(installation) => return Ok(installation),
127 Err(err) => err,
128 };
129
130 match err {
131 Error::MissingPython(..) => {}
133 Error::Discovery(ref err) if !err.is_critical() => {}
135 _ => return Err(err),
137 }
138
139 let Some(download_request) = PythonDownloadRequest::from_request(request) else {
141 return Err(err);
142 };
143
144 let downloads_enabled = preference.allows_managed()
145 && python_downloads.is_automatic()
146 && client_builder.connectivity.is_online();
147
148 let download = download_request
149 .clone()
150 .fill()
151 .map(|request| download_list.find(&request));
152
153 let download = match download {
157 Ok(Ok(download)) => Some(download),
158 Ok(Err(downloads::Error::NoDownloadFound(_))) => {
160 if downloads_enabled {
161 debug!("No downloads are available for {request}");
162 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
163 return Err(err);
164 }
165 return Err(err.with_missing_python_hint(
166 "uv embeds available Python downloads and may require an update to install new versions. Consider retrying on a newer version of uv."
167 .to_string(),
168 ));
169 }
170 None
171 }
172 Err(err) | Ok(Err(err)) => {
173 if downloads_enabled {
174 return Err(err.into());
176 }
177 None
178 }
179 };
180
181 let Some(download) = download else {
182 debug_assert!(!downloads_enabled);
185 return Err(err);
186 };
187
188 if !downloads_enabled {
190 let for_request = match request {
191 PythonRequest::Default | PythonRequest::Any => String::new(),
192 _ => format!(" for {request}"),
193 };
194
195 match python_downloads {
196 PythonDownloads::Automatic => {}
197 PythonDownloads::Manual => {
198 return Err(err.with_missing_python_hint(format!(
199 "A managed Python download is available{for_request}, but Python downloads are set to 'manual', use `uv python install {}` to install the required version",
200 request.to_canonical_string(),
201 )));
202 }
203 PythonDownloads::Never => {
204 return Err(err.with_missing_python_hint(format!(
205 "A managed Python download is available{for_request}, but Python downloads are set to 'never'"
206 )));
207 }
208 }
209
210 match preference {
211 PythonPreference::OnlySystem => {
212 return Err(err.with_missing_python_hint(format!(
213 "A managed Python download is available{for_request}, but the Python preference is set to 'only system'"
214 )));
215 }
216 PythonPreference::Managed
217 | PythonPreference::OnlyManaged
218 | PythonPreference::System => {}
219 }
220
221 if !client_builder.connectivity.is_online() {
222 return Err(err.with_missing_python_hint(format!(
223 "A managed Python download is available{for_request}, but uv is set to offline mode"
224 )));
225 }
226
227 return Err(err);
228 }
229
230 let installation = Self::fetch(
231 download,
232 &client,
233 &retry_policy,
234 cache,
235 reporter,
236 python_install_mirror,
237 pypy_install_mirror,
238 preview,
239 )
240 .await?;
241
242 installation.warn_if_outdated_prerelease(request, &download_list);
243
244 Ok(installation)
245 }
246
247 pub async fn fetch(
249 download: &ManagedPythonDownload,
250 client: &BaseClient,
251 retry_policy: &ExponentialBackoff,
252 cache: &Cache,
253 reporter: Option<&dyn Reporter>,
254 python_install_mirror: Option<&str>,
255 pypy_install_mirror: Option<&str>,
256 preview: Preview,
257 ) -> Result<Self, Error> {
258 let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
259 let installations_dir = installations.root();
260 let scratch_dir = installations.scratch();
261 let _lock = installations.lock().await?;
262
263 info!("Fetching requested Python...");
264 let result = download
265 .fetch_with_retry(
266 client,
267 retry_policy,
268 installations_dir,
269 &scratch_dir,
270 false,
271 python_install_mirror,
272 pypy_install_mirror,
273 reporter,
274 )
275 .await?;
276
277 let path = match result {
278 DownloadResult::AlreadyAvailable(path) => path,
279 DownloadResult::Fetched(path) => path,
280 };
281
282 let installed = ManagedPythonInstallation::new(path, download);
283 installed.ensure_externally_managed()?;
284 installed.ensure_sysconfig_patched()?;
285 installed.ensure_canonical_executables()?;
286 installed.ensure_build_file()?;
287
288 let minor_version = installed.minor_version_key();
289 let highest_patch = installations
290 .find_all()?
291 .filter(|installation| installation.minor_version_key() == minor_version)
292 .filter_map(|installation| installation.version().patch())
293 .fold(0, std::cmp::max);
294 if installed
295 .version()
296 .patch()
297 .is_some_and(|p| p >= highest_patch)
298 {
299 installed.ensure_minor_version_link(preview)?;
300 }
301
302 if let Err(e) = installed.ensure_dylib_patched() {
303 e.warn_user(&installed);
304 }
305
306 Ok(Self {
307 source: PythonSource::Managed,
308 interpreter: Interpreter::query(installed.executable(false), cache)?,
309 })
310 }
311
312 pub fn from_interpreter(interpreter: Interpreter) -> Self {
314 Self {
315 source: PythonSource::ProvidedPath,
316 interpreter,
317 }
318 }
319
320 pub fn source(&self) -> &PythonSource {
322 &self.source
323 }
324
325 pub fn key(&self) -> PythonInstallationKey {
326 self.interpreter.key()
327 }
328
329 pub fn python_version(&self) -> &Version {
331 self.interpreter.python_version()
332 }
333
334 pub fn implementation(&self) -> LenientImplementationName {
336 LenientImplementationName::from(self.interpreter.implementation_name())
337 }
338
339 pub(crate) fn is_alternative_implementation(&self) -> bool {
343 !matches!(
344 self.implementation(),
345 LenientImplementationName::Known(ImplementationName::CPython)
346 ) || self.os().is_emscripten()
347 }
348
349 pub fn arch(&self) -> Arch {
351 self.interpreter.arch()
352 }
353
354 pub fn libc(&self) -> Libc {
356 self.interpreter.libc()
357 }
358
359 pub fn os(&self) -> Os {
361 self.interpreter.os()
362 }
363
364 pub fn interpreter(&self) -> &Interpreter {
366 &self.interpreter
367 }
368
369 pub fn into_interpreter(self) -> Interpreter {
371 self.interpreter
372 }
373
374 pub(crate) fn warn_if_outdated_prerelease(
377 &self,
378 request: &PythonRequest,
379 download_list: &ManagedPythonDownloadList,
380 ) {
381 if request.allows_prereleases() {
382 return;
383 }
384
385 let interpreter = self.interpreter();
386 let version = interpreter.python_version();
387
388 if version.pre().is_none() {
389 return;
390 }
391
392 if !interpreter.is_managed() {
393 return;
394 }
395
396 if !interpreter
401 .implementation_name()
402 .eq_ignore_ascii_case("cpython")
403 {
404 return;
405 }
406
407 let release = version.only_release();
408
409 let Ok(download_request) = PythonDownloadRequest::try_from(&interpreter.key()) else {
410 return;
411 };
412
413 let download_request = download_request.with_prereleases(false);
414
415 let has_stable_download = {
416 let mut downloads = download_list.iter_matching(&download_request);
417
418 downloads.any(|download| {
419 let download_version = download.key().version().into_version();
420 download_version.pre().is_none() && download_version.only_release() >= release
421 })
422 };
423
424 if !has_stable_download {
425 return;
426 }
427
428 if let Some(upgrade_request) = download_request
429 .unset_defaults()
430 .without_patch()
431 .simplified_display()
432 {
433 warn_user!(
434 "You're using a pre-release version of Python ({}) but a stable version is available. Use `uv python upgrade {}` to upgrade.",
435 version,
436 upgrade_request
437 );
438 } else {
439 warn_user!(
440 "You're using a pre-release version of Python ({}) but a stable version is available. Run `uv python upgrade` to update your managed interpreters.",
441 version,
442 );
443 }
444 }
445}
446
447#[derive(Error, Debug)]
448pub enum PythonInstallationKeyError {
449 #[error("Failed to parse Python installation key `{0}`: {1}")]
450 ParseError(String, String),
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Hash)]
454pub struct PythonInstallationKey {
455 pub(crate) implementation: LenientImplementationName,
456 pub(crate) major: u8,
457 pub(crate) minor: u8,
458 pub(crate) patch: u8,
459 pub(crate) prerelease: Option<Prerelease>,
460 pub(crate) platform: Platform,
461 pub(crate) variant: PythonVariant,
462}
463
464impl PythonInstallationKey {
465 pub fn new(
466 implementation: LenientImplementationName,
467 major: u8,
468 minor: u8,
469 patch: u8,
470 prerelease: Option<Prerelease>,
471 platform: Platform,
472 variant: PythonVariant,
473 ) -> Self {
474 Self {
475 implementation,
476 major,
477 minor,
478 patch,
479 prerelease,
480 platform,
481 variant,
482 }
483 }
484
485 pub fn new_from_version(
486 implementation: LenientImplementationName,
487 version: &PythonVersion,
488 platform: Platform,
489 variant: PythonVariant,
490 ) -> Self {
491 Self {
492 implementation,
493 major: version.major(),
494 minor: version.minor(),
495 patch: version.patch().unwrap_or_default(),
496 prerelease: version.pre(),
497 platform,
498 variant,
499 }
500 }
501
502 pub fn implementation(&self) -> Cow<'_, LenientImplementationName> {
503 if self.os().is_emscripten() {
504 Cow::Owned(LenientImplementationName::from(ImplementationName::Pyodide))
505 } else {
506 Cow::Borrowed(&self.implementation)
507 }
508 }
509
510 pub fn version(&self) -> PythonVersion {
511 PythonVersion::from_str(&format!(
512 "{}.{}.{}{}",
513 self.major,
514 self.minor,
515 self.patch,
516 self.prerelease
517 .map(|pre| pre.to_string())
518 .unwrap_or_default()
519 ))
520 .expect("Python installation keys must have valid Python versions")
521 }
522
523 pub fn sys_version(&self) -> String {
525 format!("{}.{}.{}", self.major, self.minor, self.patch)
526 }
527
528 pub fn major(&self) -> u8 {
529 self.major
530 }
531
532 pub fn minor(&self) -> u8 {
533 self.minor
534 }
535
536 pub fn prerelease(&self) -> Option<Prerelease> {
537 self.prerelease
538 }
539
540 pub fn platform(&self) -> &Platform {
541 &self.platform
542 }
543
544 pub fn arch(&self) -> &Arch {
545 &self.platform.arch
546 }
547
548 pub fn os(&self) -> &Os {
549 &self.platform.os
550 }
551
552 pub fn libc(&self) -> &Libc {
553 &self.platform.libc
554 }
555
556 pub fn variant(&self) -> &PythonVariant {
557 &self.variant
558 }
559
560 pub fn executable_name_minor(&self) -> String {
562 format!(
563 "python{maj}.{min}{var}{exe}",
564 maj = self.major,
565 min = self.minor,
566 var = self.variant.executable_suffix(),
567 exe = std::env::consts::EXE_SUFFIX
568 )
569 }
570
571 pub fn executable_name_major(&self) -> String {
573 format!(
574 "python{maj}{var}{exe}",
575 maj = self.major,
576 var = self.variant.executable_suffix(),
577 exe = std::env::consts::EXE_SUFFIX
578 )
579 }
580
581 pub fn executable_name(&self) -> String {
583 format!(
584 "python{var}{exe}",
585 var = self.variant.executable_suffix(),
586 exe = std::env::consts::EXE_SUFFIX
587 )
588 }
589}
590
591impl fmt::Display for PythonInstallationKey {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 let variant = match self.variant {
594 PythonVariant::Default => String::new(),
595 _ => format!("+{}", self.variant),
596 };
597 write!(
598 f,
599 "{}-{}.{}.{}{}{}-{}",
600 self.implementation(),
601 self.major,
602 self.minor,
603 self.patch,
604 self.prerelease
605 .map(|pre| pre.to_string())
606 .unwrap_or_default(),
607 variant,
608 self.platform
609 )
610 }
611}
612
613impl FromStr for PythonInstallationKey {
614 type Err = PythonInstallationKeyError;
615
616 fn from_str(key: &str) -> Result<Self, Self::Err> {
617 let parts = key.split('-').collect::<Vec<_>>();
618
619 if parts.len() != 5 {
621 return Err(PythonInstallationKeyError::ParseError(
622 key.to_string(),
623 format!(
624 "expected exactly 5 `-`-separated values, got {}",
625 parts.len()
626 ),
627 ));
628 }
629
630 let [implementation_str, version_str, os, arch, libc] = parts.as_slice() else {
631 unreachable!()
632 };
633
634 let implementation = LenientImplementationName::from(*implementation_str);
635
636 let (version, variant) = match version_str.split_once('+') {
637 Some((version, variant)) => {
638 let variant = PythonVariant::from_str(variant).map_err(|()| {
639 PythonInstallationKeyError::ParseError(
640 key.to_string(),
641 format!("invalid Python variant: {variant}"),
642 )
643 })?;
644 (version, variant)
645 }
646 None => (*version_str, PythonVariant::Default),
647 };
648
649 let version = PythonVersion::from_str(version).map_err(|err| {
650 PythonInstallationKeyError::ParseError(
651 key.to_string(),
652 format!("invalid Python version: {err}"),
653 )
654 })?;
655
656 let platform = Platform::from_parts(os, arch, libc).map_err(|err| {
657 PythonInstallationKeyError::ParseError(
658 key.to_string(),
659 format!("invalid platform: {err}"),
660 )
661 })?;
662
663 Ok(Self {
664 implementation,
665 major: version.major(),
666 minor: version.minor(),
667 patch: version.patch().unwrap_or_default(),
668 prerelease: version.pre(),
669 platform,
670 variant,
671 })
672 }
673}
674
675impl PartialOrd for PythonInstallationKey {
676 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
677 Some(self.cmp(other))
678 }
679}
680
681impl Ord for PythonInstallationKey {
682 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
683 self.implementation
684 .cmp(&other.implementation)
685 .then_with(|| self.version().cmp(&other.version()))
686 .then_with(|| self.platform.cmp(&other.platform).reverse())
688 .then_with(|| self.variant.cmp(&other.variant).reverse())
690 }
691}
692
693#[derive(Clone, Eq, Ord, PartialOrd, RefCast)]
695#[repr(transparent)]
696pub struct PythonInstallationMinorVersionKey(PythonInstallationKey);
697
698impl PythonInstallationMinorVersionKey {
699 #[inline]
701 pub fn ref_cast(key: &PythonInstallationKey) -> &Self {
702 RefCast::ref_cast(key)
703 }
704
705 #[inline]
709 pub fn highest_installations_by_minor_version_key<'a, I>(
710 installations: I,
711 ) -> IndexMap<Self, ManagedPythonInstallation>
712 where
713 I: IntoIterator<Item = &'a ManagedPythonInstallation>,
714 {
715 let mut minor_versions = IndexMap::default();
716 for installation in installations {
717 minor_versions
718 .entry(installation.minor_version_key().clone())
719 .and_modify(|high_installation: &mut ManagedPythonInstallation| {
720 if installation.key() >= high_installation.key() {
721 *high_installation = installation.clone();
722 }
723 })
724 .or_insert_with(|| installation.clone());
725 }
726 minor_versions
727 }
728}
729
730impl fmt::Display for PythonInstallationMinorVersionKey {
731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732 let variant = match self.0.variant {
735 PythonVariant::Default => String::new(),
736 _ => format!("+{}", self.0.variant),
737 };
738 write!(
739 f,
740 "{}-{}.{}{}-{}",
741 self.0.implementation, self.0.major, self.0.minor, variant, self.0.platform,
742 )
743 }
744}
745
746impl fmt::Debug for PythonInstallationMinorVersionKey {
747 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
748 f.debug_struct("PythonInstallationMinorVersionKey")
751 .field("implementation", &self.0.implementation)
752 .field("major", &self.0.major)
753 .field("minor", &self.0.minor)
754 .field("variant", &self.0.variant)
755 .field("os", &self.0.platform.os)
756 .field("arch", &self.0.platform.arch)
757 .field("libc", &self.0.platform.libc)
758 .finish()
759 }
760}
761
762impl PartialEq for PythonInstallationMinorVersionKey {
763 fn eq(&self, other: &Self) -> bool {
764 self.0.implementation == other.0.implementation
767 && self.0.major == other.0.major
768 && self.0.minor == other.0.minor
769 && self.0.platform == other.0.platform
770 && self.0.variant == other.0.variant
771 }
772}
773
774impl Hash for PythonInstallationMinorVersionKey {
775 fn hash<H: Hasher>(&self, state: &mut H) {
776 self.0.implementation.hash(state);
779 self.0.major.hash(state);
780 self.0.minor.hash(state);
781 self.0.platform.hash(state);
782 self.0.variant.hash(state);
783 }
784}
785
786impl From<PythonInstallationKey> for PythonInstallationMinorVersionKey {
787 fn from(key: PythonInstallationKey) -> Self {
788 Self(key)
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use uv_platform::ArchVariant;
796
797 #[test]
798 fn test_python_installation_key_from_str() {
799 let key = PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64-gnu").unwrap();
801 assert_eq!(
802 key.implementation,
803 LenientImplementationName::Known(ImplementationName::CPython)
804 );
805 assert_eq!(key.major, 3);
806 assert_eq!(key.minor, 12);
807 assert_eq!(key.patch, 0);
808 assert_eq!(
809 key.platform.os,
810 Os::new(target_lexicon::OperatingSystem::Linux)
811 );
812 assert_eq!(
813 key.platform.arch,
814 Arch::new(target_lexicon::Architecture::X86_64, None)
815 );
816 assert_eq!(
817 key.platform.libc,
818 Libc::Some(target_lexicon::Environment::Gnu)
819 );
820
821 let key = PythonInstallationKey::from_str("cpython-3.11.2-linux-x86_64_v3-musl").unwrap();
823 assert_eq!(
824 key.implementation,
825 LenientImplementationName::Known(ImplementationName::CPython)
826 );
827 assert_eq!(key.major, 3);
828 assert_eq!(key.minor, 11);
829 assert_eq!(key.patch, 2);
830 assert_eq!(
831 key.platform.os,
832 Os::new(target_lexicon::OperatingSystem::Linux)
833 );
834 assert_eq!(
835 key.platform.arch,
836 Arch::new(target_lexicon::Architecture::X86_64, Some(ArchVariant::V3))
837 );
838 assert_eq!(
839 key.platform.libc,
840 Libc::Some(target_lexicon::Environment::Musl)
841 );
842
843 let key = PythonInstallationKey::from_str("cpython-3.13.0+freethreaded-macos-aarch64-none")
845 .unwrap();
846 assert_eq!(
847 key.implementation,
848 LenientImplementationName::Known(ImplementationName::CPython)
849 );
850 assert_eq!(key.major, 3);
851 assert_eq!(key.minor, 13);
852 assert_eq!(key.patch, 0);
853 assert_eq!(key.variant, PythonVariant::Freethreaded);
854 assert_eq!(
855 key.platform.os,
856 Os::new(target_lexicon::OperatingSystem::Darwin(None))
857 );
858 assert_eq!(
859 key.platform.arch,
860 Arch::new(
861 target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
862 None
863 )
864 );
865 assert_eq!(key.platform.libc, Libc::None);
866
867 assert!(PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64").is_err());
869 assert!(PythonInstallationKey::from_str("cpython-3.12.0").is_err());
870 assert!(PythonInstallationKey::from_str("cpython").is_err());
871 }
872
873 #[test]
874 fn test_python_installation_key_display() {
875 let key = PythonInstallationKey {
876 implementation: LenientImplementationName::from("cpython"),
877 major: 3,
878 minor: 12,
879 patch: 0,
880 prerelease: None,
881 platform: Platform::from_str("linux-x86_64-gnu").unwrap(),
882 variant: PythonVariant::Default,
883 };
884 assert_eq!(key.to_string(), "cpython-3.12.0-linux-x86_64-gnu");
885
886 let key_with_variant = PythonInstallationKey {
887 implementation: LenientImplementationName::from("cpython"),
888 major: 3,
889 minor: 13,
890 patch: 0,
891 prerelease: None,
892 platform: Platform::from_str("macos-aarch64-none").unwrap(),
893 variant: PythonVariant::Freethreaded,
894 };
895 assert_eq!(
896 key_with_variant.to_string(),
897 "cpython-3.13.0+freethreaded-macos-aarch64-none"
898 );
899 }
900}