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 return Err(err);
163 }
164 None
165 }
166 Err(err) | Ok(Err(err)) => {
167 if downloads_enabled {
168 return Err(err.into());
170 }
171 None
172 }
173 };
174
175 let Some(download) = download else {
176 debug_assert!(!downloads_enabled);
179 return Err(err);
180 };
181
182 if !downloads_enabled {
184 let for_request = match request {
185 PythonRequest::Default | PythonRequest::Any => String::new(),
186 _ => format!(" for {request}"),
187 };
188
189 match python_downloads {
190 PythonDownloads::Automatic => {}
191 PythonDownloads::Manual => {
192 return Err(err.with_missing_python_hint(format!(
193 "A managed Python download is available{for_request}, but Python downloads are set to 'manual', use `uv python install {}` to install the required version",
194 request.to_canonical_string(),
195 )));
196 }
197 PythonDownloads::Never => {
198 return Err(err.with_missing_python_hint(format!(
199 "A managed Python download is available{for_request}, but Python downloads are set to 'never'"
200 )));
201 }
202 }
203
204 match preference {
205 PythonPreference::OnlySystem => {
206 return Err(err.with_missing_python_hint(format!(
207 "A managed Python download is available{for_request}, but the Python preference is set to 'only system'"
208 )));
209 }
210 PythonPreference::Managed
211 | PythonPreference::OnlyManaged
212 | PythonPreference::System => {}
213 }
214
215 if !client_builder.connectivity.is_online() {
216 return Err(err.with_missing_python_hint(format!(
217 "A managed Python download is available{for_request}, but uv is set to offline mode"
218 )));
219 }
220
221 return Err(err);
222 }
223
224 let installation = Self::fetch(
225 download,
226 &client,
227 &retry_policy,
228 cache,
229 reporter,
230 python_install_mirror,
231 pypy_install_mirror,
232 preview,
233 )
234 .await?;
235
236 installation.warn_if_outdated_prerelease(request, &download_list);
237
238 Ok(installation)
239 }
240
241 pub async fn fetch(
243 download: &ManagedPythonDownload,
244 client: &BaseClient,
245 retry_policy: &ExponentialBackoff,
246 cache: &Cache,
247 reporter: Option<&dyn Reporter>,
248 python_install_mirror: Option<&str>,
249 pypy_install_mirror: Option<&str>,
250 preview: Preview,
251 ) -> Result<Self, Error> {
252 let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
253 let installations_dir = installations.root();
254 let scratch_dir = installations.scratch();
255 let _lock = installations.lock().await?;
256
257 info!("Fetching requested Python...");
258 let result = download
259 .fetch_with_retry(
260 client,
261 retry_policy,
262 installations_dir,
263 &scratch_dir,
264 false,
265 python_install_mirror,
266 pypy_install_mirror,
267 reporter,
268 )
269 .await?;
270
271 let path = match result {
272 DownloadResult::AlreadyAvailable(path) => path,
273 DownloadResult::Fetched(path) => path,
274 };
275
276 let installed = ManagedPythonInstallation::new(path, download);
277 installed.ensure_externally_managed()?;
278 installed.ensure_sysconfig_patched()?;
279 installed.ensure_canonical_executables()?;
280 installed.ensure_build_file()?;
281
282 let minor_version = installed.minor_version_key();
283 let highest_patch = installations
284 .find_all()?
285 .filter(|installation| installation.minor_version_key() == minor_version)
286 .filter_map(|installation| installation.version().patch())
287 .fold(0, std::cmp::max);
288 if installed
289 .version()
290 .patch()
291 .is_some_and(|p| p >= highest_patch)
292 {
293 installed.ensure_minor_version_link(preview)?;
294 }
295
296 if let Err(e) = installed.ensure_dylib_patched() {
297 e.warn_user(&installed);
298 }
299
300 Ok(Self {
301 source: PythonSource::Managed,
302 interpreter: Interpreter::query(installed.executable(false), cache)?,
303 })
304 }
305
306 pub fn from_interpreter(interpreter: Interpreter) -> Self {
308 Self {
309 source: PythonSource::ProvidedPath,
310 interpreter,
311 }
312 }
313
314 pub fn source(&self) -> &PythonSource {
316 &self.source
317 }
318
319 pub fn key(&self) -> PythonInstallationKey {
320 self.interpreter.key()
321 }
322
323 pub fn python_version(&self) -> &Version {
325 self.interpreter.python_version()
326 }
327
328 pub fn implementation(&self) -> LenientImplementationName {
330 LenientImplementationName::from(self.interpreter.implementation_name())
331 }
332
333 pub(crate) fn is_alternative_implementation(&self) -> bool {
337 !matches!(
338 self.implementation(),
339 LenientImplementationName::Known(ImplementationName::CPython)
340 ) || self.os().is_emscripten()
341 }
342
343 pub fn arch(&self) -> Arch {
345 self.interpreter.arch()
346 }
347
348 pub fn libc(&self) -> Libc {
350 self.interpreter.libc()
351 }
352
353 pub fn os(&self) -> Os {
355 self.interpreter.os()
356 }
357
358 pub fn interpreter(&self) -> &Interpreter {
360 &self.interpreter
361 }
362
363 pub fn into_interpreter(self) -> Interpreter {
365 self.interpreter
366 }
367
368 pub(crate) fn warn_if_outdated_prerelease(
371 &self,
372 request: &PythonRequest,
373 download_list: &ManagedPythonDownloadList,
374 ) {
375 if request.allows_prereleases() {
376 return;
377 }
378
379 let interpreter = self.interpreter();
380 let version = interpreter.python_version();
381
382 if version.pre().is_none() {
383 return;
384 }
385
386 if !interpreter.is_managed() {
387 return;
388 }
389
390 if !interpreter
395 .implementation_name()
396 .eq_ignore_ascii_case("cpython")
397 {
398 return;
399 }
400
401 let release = version.only_release();
402
403 let Ok(download_request) = PythonDownloadRequest::try_from(&interpreter.key()) else {
404 return;
405 };
406
407 let download_request = download_request.with_prereleases(false);
408
409 let has_stable_download = {
410 let mut downloads = download_list.iter_matching(&download_request);
411
412 downloads.any(|download| {
413 let download_version = download.key().version().into_version();
414 download_version.pre().is_none() && download_version.only_release() >= release
415 })
416 };
417
418 if !has_stable_download {
419 return;
420 }
421
422 if let Some(upgrade_request) = download_request
423 .unset_defaults()
424 .without_patch()
425 .simplified_display()
426 {
427 warn_user!(
428 "You're using a pre-release version of Python ({}) but a stable version is available. Use `uv python upgrade {}` to upgrade.",
429 version,
430 upgrade_request
431 );
432 } else {
433 warn_user!(
434 "You're using a pre-release version of Python ({}) but a stable version is available. Run `uv python upgrade` to update your managed interpreters.",
435 version,
436 );
437 }
438 }
439}
440
441#[derive(Error, Debug)]
442pub enum PythonInstallationKeyError {
443 #[error("Failed to parse Python installation key `{0}`: {1}")]
444 ParseError(String, String),
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Hash)]
448pub struct PythonInstallationKey {
449 pub(crate) implementation: LenientImplementationName,
450 pub(crate) major: u8,
451 pub(crate) minor: u8,
452 pub(crate) patch: u8,
453 pub(crate) prerelease: Option<Prerelease>,
454 pub(crate) platform: Platform,
455 pub(crate) variant: PythonVariant,
456}
457
458impl PythonInstallationKey {
459 pub fn new(
460 implementation: LenientImplementationName,
461 major: u8,
462 minor: u8,
463 patch: u8,
464 prerelease: Option<Prerelease>,
465 platform: Platform,
466 variant: PythonVariant,
467 ) -> Self {
468 Self {
469 implementation,
470 major,
471 minor,
472 patch,
473 prerelease,
474 platform,
475 variant,
476 }
477 }
478
479 pub fn new_from_version(
480 implementation: LenientImplementationName,
481 version: &PythonVersion,
482 platform: Platform,
483 variant: PythonVariant,
484 ) -> Self {
485 Self {
486 implementation,
487 major: version.major(),
488 minor: version.minor(),
489 patch: version.patch().unwrap_or_default(),
490 prerelease: version.pre(),
491 platform,
492 variant,
493 }
494 }
495
496 pub fn implementation(&self) -> Cow<'_, LenientImplementationName> {
497 if self.os().is_emscripten() {
498 Cow::Owned(LenientImplementationName::from(ImplementationName::Pyodide))
499 } else {
500 Cow::Borrowed(&self.implementation)
501 }
502 }
503
504 pub fn version(&self) -> PythonVersion {
505 PythonVersion::from_str(&format!(
506 "{}.{}.{}{}",
507 self.major,
508 self.minor,
509 self.patch,
510 self.prerelease
511 .map(|pre| pre.to_string())
512 .unwrap_or_default()
513 ))
514 .expect("Python installation keys must have valid Python versions")
515 }
516
517 pub fn sys_version(&self) -> String {
519 format!("{}.{}.{}", self.major, self.minor, self.patch)
520 }
521
522 pub fn major(&self) -> u8 {
523 self.major
524 }
525
526 pub fn minor(&self) -> u8 {
527 self.minor
528 }
529
530 pub fn prerelease(&self) -> Option<Prerelease> {
531 self.prerelease
532 }
533
534 pub fn platform(&self) -> &Platform {
535 &self.platform
536 }
537
538 pub fn arch(&self) -> &Arch {
539 &self.platform.arch
540 }
541
542 pub fn os(&self) -> &Os {
543 &self.platform.os
544 }
545
546 pub fn libc(&self) -> &Libc {
547 &self.platform.libc
548 }
549
550 pub fn variant(&self) -> &PythonVariant {
551 &self.variant
552 }
553
554 pub fn executable_name_minor(&self) -> String {
556 format!(
557 "python{maj}.{min}{var}{exe}",
558 maj = self.major,
559 min = self.minor,
560 var = self.variant.executable_suffix(),
561 exe = std::env::consts::EXE_SUFFIX
562 )
563 }
564
565 pub fn executable_name_major(&self) -> String {
567 format!(
568 "python{maj}{var}{exe}",
569 maj = self.major,
570 var = self.variant.executable_suffix(),
571 exe = std::env::consts::EXE_SUFFIX
572 )
573 }
574
575 pub fn executable_name(&self) -> String {
577 format!(
578 "python{var}{exe}",
579 var = self.variant.executable_suffix(),
580 exe = std::env::consts::EXE_SUFFIX
581 )
582 }
583}
584
585impl fmt::Display for PythonInstallationKey {
586 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587 let variant = match self.variant {
588 PythonVariant::Default => String::new(),
589 _ => format!("+{}", self.variant),
590 };
591 write!(
592 f,
593 "{}-{}.{}.{}{}{}-{}",
594 self.implementation(),
595 self.major,
596 self.minor,
597 self.patch,
598 self.prerelease
599 .map(|pre| pre.to_string())
600 .unwrap_or_default(),
601 variant,
602 self.platform
603 )
604 }
605}
606
607impl FromStr for PythonInstallationKey {
608 type Err = PythonInstallationKeyError;
609
610 fn from_str(key: &str) -> Result<Self, Self::Err> {
611 let parts = key.split('-').collect::<Vec<_>>();
612
613 if parts.len() != 5 {
615 return Err(PythonInstallationKeyError::ParseError(
616 key.to_string(),
617 format!(
618 "expected exactly 5 `-`-separated values, got {}",
619 parts.len()
620 ),
621 ));
622 }
623
624 let [implementation_str, version_str, os, arch, libc] = parts.as_slice() else {
625 unreachable!()
626 };
627
628 let implementation = LenientImplementationName::from(*implementation_str);
629
630 let (version, variant) = match version_str.split_once('+') {
631 Some((version, variant)) => {
632 let variant = PythonVariant::from_str(variant).map_err(|()| {
633 PythonInstallationKeyError::ParseError(
634 key.to_string(),
635 format!("invalid Python variant: {variant}"),
636 )
637 })?;
638 (version, variant)
639 }
640 None => (*version_str, PythonVariant::Default),
641 };
642
643 let version = PythonVersion::from_str(version).map_err(|err| {
644 PythonInstallationKeyError::ParseError(
645 key.to_string(),
646 format!("invalid Python version: {err}"),
647 )
648 })?;
649
650 let platform = Platform::from_parts(os, arch, libc).map_err(|err| {
651 PythonInstallationKeyError::ParseError(
652 key.to_string(),
653 format!("invalid platform: {err}"),
654 )
655 })?;
656
657 Ok(Self {
658 implementation,
659 major: version.major(),
660 minor: version.minor(),
661 patch: version.patch().unwrap_or_default(),
662 prerelease: version.pre(),
663 platform,
664 variant,
665 })
666 }
667}
668
669impl PartialOrd for PythonInstallationKey {
670 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
671 Some(self.cmp(other))
672 }
673}
674
675impl Ord for PythonInstallationKey {
676 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
677 self.implementation
678 .cmp(&other.implementation)
679 .then_with(|| self.version().cmp(&other.version()))
680 .then_with(|| self.platform.cmp(&other.platform).reverse())
682 .then_with(|| self.variant.cmp(&other.variant).reverse())
684 }
685}
686
687#[derive(Clone, Eq, Ord, PartialOrd, RefCast)]
689#[repr(transparent)]
690pub struct PythonInstallationMinorVersionKey(PythonInstallationKey);
691
692impl PythonInstallationMinorVersionKey {
693 #[inline]
695 pub fn ref_cast(key: &PythonInstallationKey) -> &Self {
696 RefCast::ref_cast(key)
697 }
698
699 #[inline]
703 pub fn highest_installations_by_minor_version_key<'a, I>(
704 installations: I,
705 ) -> IndexMap<Self, ManagedPythonInstallation>
706 where
707 I: IntoIterator<Item = &'a ManagedPythonInstallation>,
708 {
709 let mut minor_versions = IndexMap::default();
710 for installation in installations {
711 minor_versions
712 .entry(installation.minor_version_key().clone())
713 .and_modify(|high_installation: &mut ManagedPythonInstallation| {
714 if installation.key() >= high_installation.key() {
715 *high_installation = installation.clone();
716 }
717 })
718 .or_insert_with(|| installation.clone());
719 }
720 minor_versions
721 }
722}
723
724impl fmt::Display for PythonInstallationMinorVersionKey {
725 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
726 let variant = match self.0.variant {
729 PythonVariant::Default => String::new(),
730 _ => format!("+{}", self.0.variant),
731 };
732 write!(
733 f,
734 "{}-{}.{}{}-{}",
735 self.0.implementation, self.0.major, self.0.minor, variant, self.0.platform,
736 )
737 }
738}
739
740impl fmt::Debug for PythonInstallationMinorVersionKey {
741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742 f.debug_struct("PythonInstallationMinorVersionKey")
745 .field("implementation", &self.0.implementation)
746 .field("major", &self.0.major)
747 .field("minor", &self.0.minor)
748 .field("variant", &self.0.variant)
749 .field("os", &self.0.platform.os)
750 .field("arch", &self.0.platform.arch)
751 .field("libc", &self.0.platform.libc)
752 .finish()
753 }
754}
755
756impl PartialEq for PythonInstallationMinorVersionKey {
757 fn eq(&self, other: &Self) -> bool {
758 self.0.implementation == other.0.implementation
761 && self.0.major == other.0.major
762 && self.0.minor == other.0.minor
763 && self.0.platform == other.0.platform
764 && self.0.variant == other.0.variant
765 }
766}
767
768impl Hash for PythonInstallationMinorVersionKey {
769 fn hash<H: Hasher>(&self, state: &mut H) {
770 self.0.implementation.hash(state);
773 self.0.major.hash(state);
774 self.0.minor.hash(state);
775 self.0.platform.hash(state);
776 self.0.variant.hash(state);
777 }
778}
779
780impl From<PythonInstallationKey> for PythonInstallationMinorVersionKey {
781 fn from(key: PythonInstallationKey) -> Self {
782 Self(key)
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use uv_platform::ArchVariant;
790
791 #[test]
792 fn test_python_installation_key_from_str() {
793 let key = PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64-gnu").unwrap();
795 assert_eq!(
796 key.implementation,
797 LenientImplementationName::Known(ImplementationName::CPython)
798 );
799 assert_eq!(key.major, 3);
800 assert_eq!(key.minor, 12);
801 assert_eq!(key.patch, 0);
802 assert_eq!(
803 key.platform.os,
804 Os::new(target_lexicon::OperatingSystem::Linux)
805 );
806 assert_eq!(
807 key.platform.arch,
808 Arch::new(target_lexicon::Architecture::X86_64, None)
809 );
810 assert_eq!(
811 key.platform.libc,
812 Libc::Some(target_lexicon::Environment::Gnu)
813 );
814
815 let key = PythonInstallationKey::from_str("cpython-3.11.2-linux-x86_64_v3-musl").unwrap();
817 assert_eq!(
818 key.implementation,
819 LenientImplementationName::Known(ImplementationName::CPython)
820 );
821 assert_eq!(key.major, 3);
822 assert_eq!(key.minor, 11);
823 assert_eq!(key.patch, 2);
824 assert_eq!(
825 key.platform.os,
826 Os::new(target_lexicon::OperatingSystem::Linux)
827 );
828 assert_eq!(
829 key.platform.arch,
830 Arch::new(target_lexicon::Architecture::X86_64, Some(ArchVariant::V3))
831 );
832 assert_eq!(
833 key.platform.libc,
834 Libc::Some(target_lexicon::Environment::Musl)
835 );
836
837 let key = PythonInstallationKey::from_str("cpython-3.13.0+freethreaded-macos-aarch64-none")
839 .unwrap();
840 assert_eq!(
841 key.implementation,
842 LenientImplementationName::Known(ImplementationName::CPython)
843 );
844 assert_eq!(key.major, 3);
845 assert_eq!(key.minor, 13);
846 assert_eq!(key.patch, 0);
847 assert_eq!(key.variant, PythonVariant::Freethreaded);
848 assert_eq!(
849 key.platform.os,
850 Os::new(target_lexicon::OperatingSystem::Darwin(None))
851 );
852 assert_eq!(
853 key.platform.arch,
854 Arch::new(
855 target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
856 None
857 )
858 );
859 assert_eq!(key.platform.libc, Libc::None);
860
861 assert!(PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64").is_err());
863 assert!(PythonInstallationKey::from_str("cpython-3.12.0").is_err());
864 assert!(PythonInstallationKey::from_str("cpython").is_err());
865 }
866
867 #[test]
868 fn test_python_installation_key_display() {
869 let key = PythonInstallationKey {
870 implementation: LenientImplementationName::from("cpython"),
871 major: 3,
872 minor: 12,
873 patch: 0,
874 prerelease: None,
875 platform: Platform::from_str("linux-x86_64-gnu").unwrap(),
876 variant: PythonVariant::Default,
877 };
878 assert_eq!(key.to_string(), "cpython-3.12.0-linux-x86_64-gnu");
879
880 let key_with_variant = PythonInstallationKey {
881 implementation: LenientImplementationName::from("cpython"),
882 major: 3,
883 minor: 13,
884 patch: 0,
885 prerelease: None,
886 platform: Platform::from_str("macos-aarch64-none").unwrap(),
887 variant: PythonVariant::Freethreaded,
888 };
889 assert_eq!(
890 key_with_variant.to_string(),
891 "cpython-3.13.0+freethreaded-macos-aarch64-none"
892 );
893 }
894}