1use std::borrow::Cow;
2use std::env::consts::ARCH;
3use std::fmt::{Display, Formatter};
4use std::path::{Path, PathBuf};
5use std::process::{Command, ExitStatus};
6use std::str::FromStr;
7use std::sync::OnceLock;
8use std::{env, io};
9
10use configparser::ini::Ini;
11use fs_err as fs;
12use owo_colors::OwoColorize;
13use same_file::is_same_file;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use tracing::{debug, trace, warn};
17
18use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness};
19use uv_cache_info::Timestamp;
20use uv_cache_key::cache_digest;
21use uv_fs::{
22 LockedFile, LockedFileError, LockedFileMode, PythonExt, Simplified, write_atomic_sync,
23};
24use uv_install_wheel::Layout;
25use uv_pep440::Version;
26use uv_pep508::{MarkerEnvironment, StringVersion};
27use uv_platform::{Arch, Libc, Os};
28use uv_platform_tags::{Platform, Tags, TagsError};
29use uv_pypi_types::{ResolverMarkerEnvironment, Scheme};
30
31use crate::implementation::LenientImplementationName;
32use crate::managed::ManagedPythonInstallations;
33use crate::pointer_size::PointerSize;
34use crate::{
35 Prefix, PyVenvConfiguration, PythonInstallationKey, PythonVariant, PythonVersion, Target,
36 VersionRequest, VirtualEnvironment,
37};
38
39#[cfg(windows)]
40use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
41
42#[expect(clippy::struct_excessive_bools)]
44#[derive(Debug, Clone)]
45pub struct Interpreter {
46 platform: Platform,
47 markers: Box<MarkerEnvironment>,
48 scheme: Scheme,
49 virtualenv: Scheme,
50 manylinux_compatible: bool,
51 sys_prefix: PathBuf,
52 sys_base_exec_prefix: PathBuf,
53 sys_base_prefix: PathBuf,
54 sys_base_executable: Option<PathBuf>,
55 sys_executable: PathBuf,
56 sys_path: Vec<PathBuf>,
57 site_packages: Vec<PathBuf>,
58 stdlib: PathBuf,
59 standalone: bool,
60 tags: OnceLock<Tags>,
61 target: Option<Target>,
62 prefix: Option<Prefix>,
63 pointer_size: PointerSize,
64 gil_disabled: bool,
65 real_executable: PathBuf,
66 debug_enabled: bool,
67}
68
69impl Interpreter {
70 pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
72 let info = InterpreterInfo::query_cached(executable.as_ref(), cache)?;
73
74 debug_assert!(
75 info.sys_executable.is_absolute(),
76 "`sys.executable` is not an absolute Python; Python installation is broken: {}",
77 info.sys_executable.display()
78 );
79
80 Ok(Self {
81 platform: info.platform,
82 markers: Box::new(info.markers),
83 scheme: info.scheme,
84 virtualenv: info.virtualenv,
85 manylinux_compatible: info.manylinux_compatible,
86 sys_prefix: info.sys_prefix,
87 sys_base_exec_prefix: info.sys_base_exec_prefix,
88 pointer_size: info.pointer_size,
89 gil_disabled: info.gil_disabled,
90 debug_enabled: info.debug_enabled,
91 sys_base_prefix: info.sys_base_prefix,
92 sys_base_executable: info.sys_base_executable,
93 sys_executable: info.sys_executable,
94 sys_path: info.sys_path,
95 site_packages: info.site_packages,
96 stdlib: info.stdlib,
97 standalone: info.standalone,
98 tags: OnceLock::new(),
99 target: None,
100 prefix: None,
101 real_executable: executable.as_ref().to_path_buf(),
102 })
103 }
104
105 #[must_use]
107 pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
108 Self {
109 scheme: virtualenv.scheme,
110 sys_base_executable: Some(virtualenv.base_executable),
111 sys_executable: virtualenv.executable,
112 sys_prefix: virtualenv.root,
113 target: None,
114 prefix: None,
115 site_packages: vec![],
116 ..self
117 }
118 }
119
120 pub fn with_target(self, target: Target) -> io::Result<Self> {
122 target.init()?;
123 Ok(Self {
124 target: Some(target),
125 ..self
126 })
127 }
128
129 pub fn with_prefix(self, prefix: Prefix) -> io::Result<Self> {
131 prefix.init(self.virtualenv())?;
132 Ok(Self {
133 prefix: Some(prefix),
134 ..self
135 })
136 }
137
138 pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
148 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
149 let base_python = std::path::absolute(base_executable)?;
150 Ok(base_python)
151 }
152
153 pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
164 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
165 let base_python = match find_base_python(
175 base_executable,
176 self.python_major(),
177 self.python_minor(),
178 self.variant().executable_suffix(),
179 ) {
180 Ok(path) => path,
181 Err(err) => {
182 warn!("Failed to find base Python executable: {err}");
183 canonicalize_executable(base_executable)?
184 }
185 };
186 Ok(base_python)
187 }
188
189 #[inline]
191 pub fn platform(&self) -> &Platform {
192 &self.platform
193 }
194
195 #[inline]
197 pub const fn markers(&self) -> &MarkerEnvironment {
198 &self.markers
199 }
200
201 pub fn resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
203 ResolverMarkerEnvironment::from(self.markers().clone())
204 }
205
206 pub fn key(&self) -> PythonInstallationKey {
208 PythonInstallationKey::new(
209 LenientImplementationName::from(self.implementation_name()),
210 self.python_major(),
211 self.python_minor(),
212 self.python_patch(),
213 self.python_version().pre(),
214 uv_platform::Platform::new(self.os(), self.arch(), self.libc()),
215 self.variant(),
216 )
217 }
218
219 pub fn variant(&self) -> PythonVariant {
220 if self.gil_disabled() {
221 if self.debug_enabled() {
222 PythonVariant::FreethreadedDebug
223 } else {
224 PythonVariant::Freethreaded
225 }
226 } else if self.debug_enabled() {
227 PythonVariant::Debug
228 } else {
229 PythonVariant::default()
230 }
231 }
232
233 pub fn arch(&self) -> Arch {
235 Arch::from(&self.platform().arch())
236 }
237
238 pub fn libc(&self) -> Libc {
240 Libc::from(self.platform().os())
241 }
242
243 pub fn os(&self) -> Os {
245 Os::from(self.platform().os())
246 }
247
248 pub fn tags(&self) -> Result<&Tags, TagsError> {
250 if self.tags.get().is_none() {
251 let tags = Tags::from_env(
252 self.platform(),
253 self.python_tuple(),
254 self.implementation_name(),
255 self.implementation_tuple(),
256 self.manylinux_compatible,
257 self.gil_disabled,
258 false,
259 )?;
260 self.tags.set(tags).expect("tags should not be set");
261 }
262 Ok(self.tags.get().expect("tags should be set"))
263 }
264
265 pub fn is_virtualenv(&self) -> bool {
269 self.sys_prefix != self.sys_base_prefix
271 }
272
273 pub fn is_target(&self) -> bool {
275 self.target.is_some()
276 }
277
278 pub fn is_prefix(&self) -> bool {
280 self.prefix.is_some()
281 }
282
283 pub fn is_managed(&self) -> bool {
287 if let Ok(test_managed) =
288 std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED)
289 {
290 return test_managed.split_ascii_whitespace().any(|item| {
293 let version = <PythonVersion as std::str::FromStr>::from_str(item).expect(
294 "`UV_INTERNAL__TEST_PYTHON_MANAGED` items should be valid Python versions",
295 );
296 if version.patch().is_some() {
297 version.version() == self.python_version()
298 } else {
299 (version.major(), version.minor()) == self.python_tuple()
300 }
301 });
302 }
303
304 let Ok(installations) = ManagedPythonInstallations::from_settings(None) else {
305 return false;
306 };
307 let Ok(root) = installations.absolute_root() else {
308 return false;
309 };
310 let sys_base_prefix = dunce::canonicalize(&self.sys_base_prefix)
311 .unwrap_or_else(|_| self.sys_base_prefix.clone());
312 let root = dunce::canonicalize(&root).unwrap_or(root);
313
314 let Ok(suffix) = sys_base_prefix.strip_prefix(&root) else {
315 return false;
316 };
317
318 let Some(first_component) = suffix.components().next() else {
319 return false;
320 };
321
322 let Some(name) = first_component.as_os_str().to_str() else {
323 return false;
324 };
325
326 PythonInstallationKey::from_str(name).is_ok()
327 }
328
329 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
334 if self.is_virtualenv() {
336 return None;
337 }
338
339 if self.is_target() || self.is_prefix() {
341 return None;
342 }
343
344 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
345 return None;
346 };
347
348 let mut ini = Ini::new_cs();
349 ini.set_multiline(true);
350
351 let Ok(mut sections) = ini.read(contents) else {
352 return Some(ExternallyManaged::default());
355 };
356
357 let Some(section) = sections.get_mut("externally-managed") else {
358 return Some(ExternallyManaged::default());
361 };
362
363 let Some(error) = section.remove("Error") else {
364 return Some(ExternallyManaged::default());
367 };
368
369 Some(ExternallyManaged { error })
370 }
371
372 #[inline]
374 pub fn python_full_version(&self) -> &StringVersion {
375 self.markers.python_full_version()
376 }
377
378 #[inline]
380 pub fn python_version(&self) -> &Version {
381 &self.markers.python_full_version().version
382 }
383
384 #[inline]
386 pub fn python_minor_version(&self) -> Version {
387 Version::new(self.python_version().release().iter().take(2).copied())
388 }
389
390 #[inline]
392 pub fn python_patch_version(&self) -> Version {
393 Version::new(self.python_version().release().iter().take(3).copied())
394 }
395
396 pub fn python_major(&self) -> u8 {
398 let major = self.markers.python_full_version().version.release()[0];
399 u8::try_from(major).expect("invalid major version")
400 }
401
402 pub fn python_minor(&self) -> u8 {
404 let minor = self.markers.python_full_version().version.release()[1];
405 u8::try_from(minor).expect("invalid minor version")
406 }
407
408 pub fn python_patch(&self) -> u8 {
410 let minor = self.markers.python_full_version().version.release()[2];
411 u8::try_from(minor).expect("invalid patch version")
412 }
413
414 pub fn python_tuple(&self) -> (u8, u8) {
416 (self.python_major(), self.python_minor())
417 }
418
419 pub fn implementation_major(&self) -> u8 {
421 let major = self.markers.implementation_version().version.release()[0];
422 u8::try_from(major).expect("invalid major version")
423 }
424
425 pub fn implementation_minor(&self) -> u8 {
427 let minor = self.markers.implementation_version().version.release()[1];
428 u8::try_from(minor).expect("invalid minor version")
429 }
430
431 pub fn implementation_tuple(&self) -> (u8, u8) {
433 (self.implementation_major(), self.implementation_minor())
434 }
435
436 pub fn implementation_name(&self) -> &str {
438 self.markers.implementation_name()
439 }
440
441 pub fn sys_base_exec_prefix(&self) -> &Path {
443 &self.sys_base_exec_prefix
444 }
445
446 pub fn sys_base_prefix(&self) -> &Path {
448 &self.sys_base_prefix
449 }
450
451 pub fn sys_prefix(&self) -> &Path {
453 &self.sys_prefix
454 }
455
456 pub fn sys_base_executable(&self) -> Option<&Path> {
459 self.sys_base_executable.as_deref()
460 }
461
462 pub fn sys_executable(&self) -> &Path {
464 &self.sys_executable
465 }
466
467 pub fn real_executable(&self) -> &Path {
469 &self.real_executable
470 }
471
472 pub fn sys_path(&self) -> &[PathBuf] {
474 &self.sys_path
475 }
476
477 pub fn runtime_site_packages(&self) -> &[PathBuf] {
485 &self.site_packages
486 }
487
488 pub fn stdlib(&self) -> &Path {
490 &self.stdlib
491 }
492
493 pub fn purelib(&self) -> &Path {
495 &self.scheme.purelib
496 }
497
498 pub fn platlib(&self) -> &Path {
500 &self.scheme.platlib
501 }
502
503 pub fn scripts(&self) -> &Path {
505 &self.scheme.scripts
506 }
507
508 pub fn data(&self) -> &Path {
510 &self.scheme.data
511 }
512
513 pub fn include(&self) -> &Path {
515 &self.scheme.include
516 }
517
518 pub fn virtualenv(&self) -> &Scheme {
520 &self.virtualenv
521 }
522
523 pub fn manylinux_compatible(&self) -> bool {
525 self.manylinux_compatible
526 }
527
528 pub fn pointer_size(&self) -> PointerSize {
530 self.pointer_size
531 }
532
533 pub fn gil_disabled(&self) -> bool {
539 self.gil_disabled
540 }
541
542 pub fn debug_enabled(&self) -> bool {
545 self.debug_enabled
546 }
547
548 pub fn target(&self) -> Option<&Target> {
550 self.target.as_ref()
551 }
552
553 pub fn prefix(&self) -> Option<&Prefix> {
555 self.prefix.as_ref()
556 }
557
558 #[cfg(unix)]
567 pub fn is_standalone(&self) -> bool {
568 self.standalone
569 }
570
571 #[cfg(windows)]
575 pub fn is_standalone(&self) -> bool {
576 self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
577 }
578
579 pub fn layout(&self) -> Layout {
581 Layout {
582 python_version: self.python_tuple(),
583 sys_executable: self.sys_executable().to_path_buf(),
584 os_name: self.markers.os_name().to_string(),
585 scheme: if let Some(target) = self.target.as_ref() {
586 target.scheme()
587 } else if let Some(prefix) = self.prefix.as_ref() {
588 prefix.scheme(&self.virtualenv)
589 } else {
590 Scheme {
591 purelib: self.purelib().to_path_buf(),
592 platlib: self.platlib().to_path_buf(),
593 scripts: self.scripts().to_path_buf(),
594 data: self.data().to_path_buf(),
595 include: if self.is_virtualenv() {
596 self.sys_prefix.join("include").join("site").join(format!(
599 "python{}.{}",
600 self.python_major(),
601 self.python_minor()
602 ))
603 } else {
604 self.include().to_path_buf()
605 },
606 }
607 },
608 }
609 }
610
611 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
622 let target = self.target().map(Target::site_packages);
623
624 let prefix = self
625 .prefix()
626 .map(|prefix| prefix.site_packages(self.virtualenv()));
627
628 let interpreter = if target.is_none() && prefix.is_none() {
629 let purelib = self.purelib();
630 let platlib = self.platlib();
631 Some(std::iter::once(purelib).chain(
632 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
633 None
634 } else {
635 Some(platlib)
636 },
637 ))
638 } else {
639 None
640 };
641
642 target
643 .into_iter()
644 .flatten()
645 .map(Cow::Borrowed)
646 .chain(prefix.into_iter().flatten().map(Cow::Owned))
647 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
648 }
649
650 pub fn satisfies(&self, version: &PythonVersion) -> bool {
655 if version.patch().is_some() {
656 version.version() == self.python_version()
657 } else {
658 (version.major(), version.minor()) == self.python_tuple()
659 }
660 }
661
662 pub(crate) fn has_default_executable_name(&self) -> bool {
665 let Some(file_name) = self.sys_executable().file_name() else {
666 return false;
667 };
668 let Some(name) = file_name.to_str() else {
669 return false;
670 };
671 VersionRequest::Default
672 .executable_names(None)
673 .into_iter()
674 .any(|default_name| name == default_name.to_string())
675 }
676
677 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
679 if let Some(target) = self.target() {
680 LockedFile::acquire(
682 target.root().join(".lock"),
683 LockedFileMode::Exclusive,
684 target.root().user_display(),
685 )
686 .await
687 } else if let Some(prefix) = self.prefix() {
688 LockedFile::acquire(
690 prefix.root().join(".lock"),
691 LockedFileMode::Exclusive,
692 prefix.root().user_display(),
693 )
694 .await
695 } else if self.is_virtualenv() {
696 LockedFile::acquire(
698 self.sys_prefix.join(".lock"),
699 LockedFileMode::Exclusive,
700 self.sys_prefix.user_display(),
701 )
702 .await
703 } else {
704 LockedFile::acquire(
706 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
707 LockedFileMode::Exclusive,
708 self.sys_prefix.user_display(),
709 )
710 .await
711 }
712 }
713}
714
715pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
718 let path = path.as_ref();
719 debug_assert!(
720 path.is_absolute(),
721 "path must be absolute: {}",
722 path.display()
723 );
724
725 #[cfg(windows)]
726 {
727 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
728 Ok(dunce::canonicalize(launcher.python_path)?)
729 } else {
730 Ok(path.to_path_buf())
731 }
732 }
733
734 #[cfg(unix)]
735 fs_err::canonicalize(path)
736}
737
738#[derive(Debug, Default, Clone)]
742pub struct ExternallyManaged {
743 error: Option<String>,
744}
745
746impl ExternallyManaged {
747 pub fn into_error(self) -> Option<String> {
749 self.error
750 }
751}
752
753#[derive(Debug, Error)]
754pub struct UnexpectedResponseError {
755 #[source]
756 pub(super) err: serde_json::Error,
757 pub(super) stdout: String,
758 pub(super) stderr: String,
759 pub(super) path: PathBuf,
760}
761
762impl Display for UnexpectedResponseError {
763 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
764 write!(
765 f,
766 "Querying Python at `{}` returned an invalid response: {}",
767 self.path.display(),
768 self.err
769 )?;
770
771 let mut non_empty = false;
772
773 if !self.stdout.trim().is_empty() {
774 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
775 non_empty = true;
776 }
777
778 if !self.stderr.trim().is_empty() {
779 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
780 non_empty = true;
781 }
782
783 if non_empty {
784 writeln!(f)?;
785 }
786
787 Ok(())
788 }
789}
790
791#[derive(Debug, Error)]
792pub struct StatusCodeError {
793 pub(super) code: ExitStatus,
794 pub(super) stdout: String,
795 pub(super) stderr: String,
796 pub(super) path: PathBuf,
797}
798
799impl Display for StatusCodeError {
800 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
801 write!(
802 f,
803 "Querying Python at `{}` failed with exit status {}",
804 self.path.display(),
805 self.code
806 )?;
807
808 let mut non_empty = false;
809
810 if !self.stdout.trim().is_empty() {
811 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
812 non_empty = true;
813 }
814
815 if !self.stderr.trim().is_empty() {
816 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
817 non_empty = true;
818 }
819
820 if non_empty {
821 writeln!(f)?;
822 }
823
824 Ok(())
825 }
826}
827
828#[derive(Debug, Error)]
829pub enum Error {
830 #[error("Failed to query Python interpreter")]
831 Io(#[from] io::Error),
832 #[error(transparent)]
833 BrokenLink(BrokenLink),
834 #[error("Python interpreter not found at `{0}`")]
835 NotFound(PathBuf),
836 #[error("Failed to query Python interpreter at `{path}`")]
837 SpawnFailed {
838 path: PathBuf,
839 #[source]
840 err: io::Error,
841 },
842 #[cfg(windows)]
843 #[error("Failed to query Python interpreter at `{path}`")]
844 CorruptWindowsPackage {
845 path: PathBuf,
846 #[source]
847 err: io::Error,
848 },
849 #[error("Failed to query Python interpreter at `{path}`")]
850 PermissionDenied {
851 path: PathBuf,
852 #[source]
853 err: io::Error,
854 },
855 #[error("{0}")]
856 UnexpectedResponse(UnexpectedResponseError),
857 #[error("{0}")]
858 StatusCode(StatusCodeError),
859 #[error("Can't use Python at `{path}`")]
860 QueryScript {
861 #[source]
862 err: InterpreterInfoError,
863 path: PathBuf,
864 },
865 #[error("Failed to write to cache")]
866 Encode(#[from] rmp_serde::encode::Error),
867}
868
869#[derive(Debug, Error)]
870pub struct BrokenLink {
871 pub path: PathBuf,
872 pub unix: bool,
875 pub venv: bool,
877}
878
879impl Display for BrokenLink {
880 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
881 if self.unix {
882 write!(
883 f,
884 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
885 self.path.user_display()
886 )?;
887 } else {
888 write!(
889 f,
890 "Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
891 self.path.user_display()
892 )?;
893 }
894 if self.venv {
895 write!(
896 f,
897 "\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
898 "hint".bold().cyan(),
899 ":".bold(),
900 "uv venv".green()
901 )?;
902 }
903 Ok(())
904 }
905}
906
907#[derive(Debug, Deserialize, Serialize)]
908#[serde(tag = "result", rename_all = "lowercase")]
909enum InterpreterInfoResult {
910 Error(InterpreterInfoError),
911 Success(Box<InterpreterInfo>),
912}
913
914#[derive(Debug, Error, Deserialize, Serialize)]
915#[serde(tag = "kind", rename_all = "snake_case")]
916pub enum InterpreterInfoError {
917 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
918 LibcNotFound,
919 #[error(
920 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
921 )]
922 BrokenMacVer,
923 #[error("Unknown operating system: `{operating_system}`")]
924 UnknownOperatingSystem { operating_system: String },
925 #[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
926 UnsupportedPythonVersion { python_version: String },
927 #[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
928 UnsupportedPython,
929 #[error(
930 "Python installation is missing `distutils`, which is required for packaging on older Python versions. Your system may package it separately, e.g., as `python{python_major}-distutils` or `python{python_major}.{python_minor}-distutils`."
931 )]
932 MissingRequiredDistutils {
933 python_major: usize,
934 python_minor: usize,
935 },
936 #[error("Only Pyodide is support for Emscripten Python")]
937 EmscriptenNotPyodide,
938}
939
940#[expect(clippy::struct_excessive_bools)]
941#[derive(Debug, Deserialize, Serialize, Clone)]
942struct InterpreterInfo {
943 platform: Platform,
944 markers: MarkerEnvironment,
945 scheme: Scheme,
946 virtualenv: Scheme,
947 manylinux_compatible: bool,
948 sys_prefix: PathBuf,
949 sys_base_exec_prefix: PathBuf,
950 sys_base_prefix: PathBuf,
951 sys_base_executable: Option<PathBuf>,
952 sys_executable: PathBuf,
953 sys_path: Vec<PathBuf>,
954 site_packages: Vec<PathBuf>,
955 stdlib: PathBuf,
956 standalone: bool,
957 pointer_size: PointerSize,
958 gil_disabled: bool,
959 debug_enabled: bool,
960}
961
962impl InterpreterInfo {
963 pub(crate) fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
965 let tempdir = tempfile::tempdir_in(cache.root())?;
966 Self::setup_python_query_files(tempdir.path())?;
967
968 let script = format!(
972 r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
973 tempdir.path().escape_for_python()
974 );
975 let mut command = Command::new(interpreter);
976 command
977 .arg("-I") .arg("-B") .arg("-c")
980 .arg(script);
981
982 #[cfg(target_os = "macos")]
991 command.env("SYSTEM_VERSION_COMPAT", "0");
992
993 let output = command.output().map_err(|err| {
994 match err.kind() {
995 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
996 io::ErrorKind::PermissionDenied => {
997 return Error::PermissionDenied {
998 path: interpreter.to_path_buf(),
999 err,
1000 };
1001 }
1002 _ => {}
1003 }
1004 #[cfg(windows)]
1005 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
1006 .raw_os_error()
1007 .and_then(|code| u32::try_from(code).ok())
1008 .map(WIN32_ERROR)
1009 {
1010 return Error::CorruptWindowsPackage {
1013 path: interpreter.to_path_buf(),
1014 err,
1015 };
1016 }
1017 Error::SpawnFailed {
1018 path: interpreter.to_path_buf(),
1019 err,
1020 }
1021 })?;
1022
1023 if !output.status.success() {
1024 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1025
1026 if python_home(interpreter).is_some_and(|home| !home.exists()) {
1032 return Err(Error::BrokenLink(BrokenLink {
1033 path: interpreter.to_path_buf(),
1034 unix: false,
1035 venv: uv_fs::is_virtualenv_executable(interpreter),
1036 }));
1037 }
1038
1039 if stderr.contains("Unknown option: -I") {
1041 return Err(Error::QueryScript {
1042 err: InterpreterInfoError::UnsupportedPython,
1043 path: interpreter.to_path_buf(),
1044 });
1045 }
1046
1047 return Err(Error::StatusCode(StatusCodeError {
1048 code: output.status,
1049 stderr,
1050 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1051 path: interpreter.to_path_buf(),
1052 }));
1053 }
1054
1055 let result: InterpreterInfoResult =
1056 serde_json::from_slice(&output.stdout).map_err(|err| {
1057 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1058
1059 if stderr.contains("Unknown option: -I") {
1061 Error::QueryScript {
1062 err: InterpreterInfoError::UnsupportedPython,
1063 path: interpreter.to_path_buf(),
1064 }
1065 } else {
1066 Error::UnexpectedResponse(UnexpectedResponseError {
1067 err,
1068 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1069 stderr,
1070 path: interpreter.to_path_buf(),
1071 })
1072 }
1073 })?;
1074
1075 match result {
1076 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1077 err,
1078 path: interpreter.to_path_buf(),
1079 }),
1080 InterpreterInfoResult::Success(data) => Ok(*data),
1081 }
1082 }
1083
1084 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1087 let python_dir = root.join("python");
1088 fs_err::create_dir(&python_dir)?;
1089 fs_err::write(
1090 python_dir.join("get_interpreter_info.py"),
1091 include_str!("../python/get_interpreter_info.py"),
1092 )?;
1093 fs_err::write(
1094 python_dir.join("__init__.py"),
1095 include_str!("../python/__init__.py"),
1096 )?;
1097 let packaging_dir = python_dir.join("packaging");
1098 fs_err::create_dir(&packaging_dir)?;
1099 fs_err::write(
1100 packaging_dir.join("__init__.py"),
1101 include_str!("../python/packaging/__init__.py"),
1102 )?;
1103 fs_err::write(
1104 packaging_dir.join("_elffile.py"),
1105 include_str!("../python/packaging/_elffile.py"),
1106 )?;
1107 fs_err::write(
1108 packaging_dir.join("_manylinux.py"),
1109 include_str!("../python/packaging/_manylinux.py"),
1110 )?;
1111 fs_err::write(
1112 packaging_dir.join("_musllinux.py"),
1113 include_str!("../python/packaging/_musllinux.py"),
1114 )?;
1115 Ok(())
1116 }
1117
1118 pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1124 let absolute = std::path::absolute(executable)?;
1125
1126 let handle_io_error = |err: io::Error| -> Error {
1130 if err.kind() == io::ErrorKind::NotFound {
1131 if absolute
1134 .symlink_metadata()
1135 .is_ok_and(|metadata| metadata.is_symlink())
1136 {
1137 Error::BrokenLink(BrokenLink {
1138 path: executable.to_path_buf(),
1139 unix: true,
1140 venv: uv_fs::is_virtualenv_executable(executable),
1141 })
1142 } else {
1143 Error::NotFound(executable.to_path_buf())
1144 }
1145 } else {
1146 err.into()
1147 }
1148 };
1149
1150 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1151
1152 let cache_entry = cache.entry(
1153 CacheBucket::Interpreter,
1154 cache_digest(&(
1157 ARCH,
1158 uv_platform::host::OsType::from_env()
1159 .map(|os_type| os_type.to_string())
1160 .unwrap_or_default(),
1161 uv_platform::host::OsRelease::from_env()
1162 .map(|os_release| os_release.to_string())
1163 .unwrap_or_default(),
1164 )),
1165 format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
1173 );
1174
1175 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1178
1179 if cache
1181 .freshness(&cache_entry, None, None)
1182 .is_ok_and(Freshness::is_fresh)
1183 {
1184 if let Ok(data) = fs::read(cache_entry.path()) {
1185 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1186 Ok(cached) => {
1187 if cached.timestamp == modified {
1188 trace!(
1189 "Found cached interpreter info for Python {}, skipping query of: {}",
1190 cached.data.markers.python_full_version(),
1191 executable.user_display()
1192 );
1193 return Ok(cached.data);
1194 }
1195
1196 trace!(
1197 "Ignoring stale interpreter markers for: {}",
1198 executable.user_display()
1199 );
1200 }
1201 Err(err) => {
1202 warn!(
1203 "Broken interpreter cache entry at {}, removing: {err}",
1204 cache_entry.path().user_display()
1205 );
1206 let _ = fs_err::remove_file(cache_entry.path());
1207 }
1208 }
1209 }
1210 }
1211
1212 trace!(
1214 "Querying interpreter executable at {}",
1215 executable.display()
1216 );
1217 let info = Self::query(executable, cache)?;
1218
1219 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1222 fs::create_dir_all(cache_entry.dir())?;
1223 write_atomic_sync(
1224 cache_entry.path(),
1225 rmp_serde::to_vec(&CachedByTimestamp {
1226 timestamp: modified,
1227 data: info.clone(),
1228 })?,
1229 )?;
1230 }
1231
1232 Ok(info)
1233 }
1234}
1235
1236fn find_base_python(
1262 executable: &Path,
1263 major: u8,
1264 minor: u8,
1265 suffix: &str,
1266) -> Result<PathBuf, io::Error> {
1267 fn is_root(path: &Path) -> bool {
1269 let mut components = path.components();
1270 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1271 }
1272
1273 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1277 if cfg!(windows) {
1278 dir.join("Lib").join("os.py").is_file()
1279 } else {
1280 dir.join("lib")
1281 .join(format!("python{major}.{minor}{suffix}"))
1282 .join("os.py")
1283 .is_file()
1284 }
1285 }
1286
1287 let mut executable = Cow::Borrowed(executable);
1288
1289 loop {
1290 debug!(
1291 "Assessing Python executable as base candidate: {}",
1292 executable.display()
1293 );
1294
1295 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1297 if is_prefix(prefix, major, minor, suffix) {
1298 return Ok(executable.into_owned());
1299 }
1300 }
1301
1302 let resolved = fs_err::read_link(&executable)?;
1304
1305 let resolved = if resolved.is_relative() {
1307 if let Some(parent) = executable.parent() {
1308 parent.join(resolved)
1309 } else {
1310 return Err(io::Error::other("Symlink has no parent directory"));
1311 }
1312 } else {
1313 resolved
1314 };
1315
1316 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1318
1319 executable = Cow::Owned(resolved);
1320 }
1321}
1322
1323fn python_home(interpreter: &Path) -> Option<PathBuf> {
1325 let venv_root = interpreter.parent()?.parent()?;
1326 let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
1327 pyvenv_cfg.home
1328}
1329
1330#[cfg(unix)]
1331#[cfg(test)]
1332mod tests {
1333 use std::str::FromStr;
1334
1335 use fs_err as fs;
1336 use indoc::{formatdoc, indoc};
1337 use tempfile::tempdir;
1338
1339 use uv_cache::Cache;
1340 use uv_pep440::Version;
1341
1342 use crate::Interpreter;
1343
1344 #[tokio::test]
1345 async fn test_cache_invalidation() {
1346 let mock_dir = tempdir().unwrap();
1347 let mocked_interpreter = mock_dir.path().join("python");
1348 let json = indoc! {r##"
1349 {
1350 "result": "success",
1351 "platform": {
1352 "os": {
1353 "name": "manylinux",
1354 "major": 2,
1355 "minor": 38
1356 },
1357 "arch": "x86_64"
1358 },
1359 "manylinux_compatible": false,
1360 "standalone": false,
1361 "markers": {
1362 "implementation_name": "cpython",
1363 "implementation_version": "3.12.0",
1364 "os_name": "posix",
1365 "platform_machine": "x86_64",
1366 "platform_python_implementation": "CPython",
1367 "platform_release": "6.5.0-13-generic",
1368 "platform_system": "Linux",
1369 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1370 "python_full_version": "3.12.0",
1371 "python_version": "3.12",
1372 "sys_platform": "linux"
1373 },
1374 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1375 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1376 "sys_prefix": "/home/ferris/projects/uv/.venv",
1377 "sys_executable": "/home/ferris/projects/uv/.venv/bin/python",
1378 "sys_path": [
1379 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1380 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1381 ],
1382 "site_packages": [
1383 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1384 ],
1385 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1386 "scheme": {
1387 "data": "/home/ferris/.pyenv/versions/3.12.0",
1388 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1389 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1390 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1391 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1392 },
1393 "virtualenv": {
1394 "data": "",
1395 "include": "include",
1396 "platlib": "lib/python3.12/site-packages",
1397 "purelib": "lib/python3.12/site-packages",
1398 "scripts": "bin"
1399 },
1400 "pointer_size": "64",
1401 "gil_disabled": true,
1402 "debug_enabled": false
1403 }
1404 "##};
1405
1406 let cache = Cache::temp().unwrap().init().await.unwrap();
1407
1408 fs::write(
1409 &mocked_interpreter,
1410 formatdoc! {r"
1411 #!/bin/sh
1412 echo '{json}'
1413 "},
1414 )
1415 .unwrap();
1416
1417 fs::set_permissions(
1418 &mocked_interpreter,
1419 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1420 )
1421 .unwrap();
1422 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1423 assert_eq!(
1424 interpreter.markers.python_version().version,
1425 Version::from_str("3.12").unwrap()
1426 );
1427 fs::write(
1428 &mocked_interpreter,
1429 formatdoc! {r"
1430 #!/bin/sh
1431 echo '{}'
1432 ", json.replace("3.12", "3.13")},
1433 )
1434 .unwrap();
1435 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1436 assert_eq!(
1437 interpreter.markers.python_version().version,
1438 Version::from_str("3.13").unwrap()
1439 );
1440 }
1441}