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, TagsOptions};
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 TagsOptions {
257 manylinux_compatible: self.manylinux_compatible,
258 gil_disabled: self.gil_disabled,
259 debug_enabled: self.debug_enabled,
260 is_cross: false,
261 },
262 )?;
263 self.tags.set(tags).expect("tags should not be set");
264 }
265 Ok(self.tags.get().expect("tags should be set"))
266 }
267
268 pub fn is_virtualenv(&self) -> bool {
272 self.sys_prefix != self.sys_base_prefix
274 }
275
276 pub fn is_target(&self) -> bool {
278 self.target.is_some()
279 }
280
281 pub fn is_prefix(&self) -> bool {
283 self.prefix.is_some()
284 }
285
286 pub fn is_managed(&self) -> bool {
290 if let Ok(test_managed) =
291 std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED)
292 {
293 return test_managed.split_ascii_whitespace().any(|item| {
296 let version = <PythonVersion as std::str::FromStr>::from_str(item).expect(
297 "`UV_INTERNAL__TEST_PYTHON_MANAGED` items should be valid Python versions",
298 );
299 if version.patch().is_some() {
300 version.version() == self.python_version()
301 } else {
302 (version.major(), version.minor()) == self.python_tuple()
303 }
304 });
305 }
306
307 let Ok(installations) = ManagedPythonInstallations::from_settings(None) else {
308 return false;
309 };
310 let Ok(root) = installations.absolute_root() else {
311 return false;
312 };
313 let sys_base_prefix = dunce::canonicalize(&self.sys_base_prefix)
314 .unwrap_or_else(|_| self.sys_base_prefix.clone());
315 let root = dunce::canonicalize(&root).unwrap_or(root);
316
317 let Ok(suffix) = sys_base_prefix.strip_prefix(&root) else {
318 return false;
319 };
320
321 let Some(first_component) = suffix.components().next() else {
322 return false;
323 };
324
325 let Some(name) = first_component.as_os_str().to_str() else {
326 return false;
327 };
328
329 PythonInstallationKey::from_str(name).is_ok()
330 }
331
332 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
337 if self.is_virtualenv() {
339 return None;
340 }
341
342 if self.is_target() || self.is_prefix() {
344 return None;
345 }
346
347 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
348 return None;
349 };
350
351 let mut ini = Ini::new_cs();
352 ini.set_multiline(true);
353
354 let Ok(mut sections) = ini.read(contents) else {
355 return Some(ExternallyManaged::default());
358 };
359
360 let Some(section) = sections.get_mut("externally-managed") else {
361 return Some(ExternallyManaged::default());
364 };
365
366 let Some(error) = section.remove("Error") else {
367 return Some(ExternallyManaged::default());
370 };
371
372 Some(ExternallyManaged { error })
373 }
374
375 #[inline]
377 pub fn python_full_version(&self) -> &StringVersion {
378 self.markers.python_full_version()
379 }
380
381 #[inline]
383 pub fn python_version(&self) -> &Version {
384 &self.markers.python_full_version().version
385 }
386
387 #[inline]
389 pub fn python_minor_version(&self) -> Version {
390 Version::new(self.python_version().release().iter().take(2).copied())
391 }
392
393 #[inline]
395 pub fn python_patch_version(&self) -> Version {
396 Version::new(self.python_version().release().iter().take(3).copied())
397 }
398
399 pub fn python_major(&self) -> u8 {
401 let major = self.markers.python_full_version().version.release()[0];
402 u8::try_from(major).expect("invalid major version")
403 }
404
405 pub fn python_minor(&self) -> u8 {
407 let minor = self.markers.python_full_version().version.release()[1];
408 u8::try_from(minor).expect("invalid minor version")
409 }
410
411 pub fn python_patch(&self) -> u8 {
413 let minor = self.markers.python_full_version().version.release()[2];
414 u8::try_from(minor).expect("invalid patch version")
415 }
416
417 pub fn python_tuple(&self) -> (u8, u8) {
419 (self.python_major(), self.python_minor())
420 }
421
422 pub fn implementation_major(&self) -> u8 {
424 let major = self.markers.implementation_version().version.release()[0];
425 u8::try_from(major).expect("invalid major version")
426 }
427
428 pub fn implementation_minor(&self) -> u8 {
430 let minor = self.markers.implementation_version().version.release()[1];
431 u8::try_from(minor).expect("invalid minor version")
432 }
433
434 pub fn implementation_tuple(&self) -> (u8, u8) {
436 (self.implementation_major(), self.implementation_minor())
437 }
438
439 pub fn implementation_name(&self) -> &str {
441 self.markers.implementation_name()
442 }
443
444 pub fn sys_base_exec_prefix(&self) -> &Path {
446 &self.sys_base_exec_prefix
447 }
448
449 pub fn sys_base_prefix(&self) -> &Path {
451 &self.sys_base_prefix
452 }
453
454 pub fn sys_prefix(&self) -> &Path {
456 &self.sys_prefix
457 }
458
459 pub fn sys_base_executable(&self) -> Option<&Path> {
462 self.sys_base_executable.as_deref()
463 }
464
465 pub fn sys_executable(&self) -> &Path {
467 &self.sys_executable
468 }
469
470 pub fn real_executable(&self) -> &Path {
472 &self.real_executable
473 }
474
475 pub fn sys_path(&self) -> &[PathBuf] {
477 &self.sys_path
478 }
479
480 pub fn runtime_site_packages(&self) -> &[PathBuf] {
488 &self.site_packages
489 }
490
491 pub fn stdlib(&self) -> &Path {
493 &self.stdlib
494 }
495
496 pub fn purelib(&self) -> &Path {
498 &self.scheme.purelib
499 }
500
501 pub fn platlib(&self) -> &Path {
503 &self.scheme.platlib
504 }
505
506 pub fn scripts(&self) -> &Path {
508 &self.scheme.scripts
509 }
510
511 pub fn data(&self) -> &Path {
513 &self.scheme.data
514 }
515
516 pub fn include(&self) -> &Path {
518 &self.scheme.include
519 }
520
521 pub fn virtualenv(&self) -> &Scheme {
523 &self.virtualenv
524 }
525
526 pub fn manylinux_compatible(&self) -> bool {
528 self.manylinux_compatible
529 }
530
531 pub fn pointer_size(&self) -> PointerSize {
533 self.pointer_size
534 }
535
536 pub fn gil_disabled(&self) -> bool {
542 self.gil_disabled
543 }
544
545 pub fn debug_enabled(&self) -> bool {
548 self.debug_enabled
549 }
550
551 pub fn target(&self) -> Option<&Target> {
553 self.target.as_ref()
554 }
555
556 pub fn prefix(&self) -> Option<&Prefix> {
558 self.prefix.as_ref()
559 }
560
561 #[cfg(unix)]
570 pub fn is_standalone(&self) -> bool {
571 self.standalone
572 }
573
574 #[cfg(windows)]
578 pub fn is_standalone(&self) -> bool {
579 self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
580 }
581
582 pub fn layout(&self) -> Layout {
584 Layout {
585 python_version: self.python_tuple(),
586 sys_executable: self.sys_executable().to_path_buf(),
587 os_name: self.markers.os_name().to_string(),
588 scheme: if let Some(target) = self.target.as_ref() {
589 target.scheme()
590 } else if let Some(prefix) = self.prefix.as_ref() {
591 prefix.scheme(&self.virtualenv)
592 } else {
593 Scheme {
594 purelib: self.purelib().to_path_buf(),
595 platlib: self.platlib().to_path_buf(),
596 scripts: self.scripts().to_path_buf(),
597 data: self.data().to_path_buf(),
598 include: if self.is_virtualenv() {
599 self.sys_prefix.join("include").join("site").join(format!(
602 "python{}.{}",
603 self.python_major(),
604 self.python_minor()
605 ))
606 } else {
607 self.include().to_path_buf()
608 },
609 }
610 },
611 }
612 }
613
614 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
625 let target = self.target().map(Target::site_packages);
626
627 let prefix = self
628 .prefix()
629 .map(|prefix| prefix.site_packages(self.virtualenv()));
630
631 let interpreter = if target.is_none() && prefix.is_none() {
632 let purelib = self.purelib();
633 let platlib = self.platlib();
634 Some(std::iter::once(purelib).chain(
635 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
636 None
637 } else {
638 Some(platlib)
639 },
640 ))
641 } else {
642 None
643 };
644
645 target
646 .into_iter()
647 .flatten()
648 .map(Cow::Borrowed)
649 .chain(prefix.into_iter().flatten().map(Cow::Owned))
650 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
651 }
652
653 pub fn satisfies(&self, version: &PythonVersion) -> bool {
658 if version.patch().is_some() {
659 version.version() == self.python_version()
660 } else {
661 (version.major(), version.minor()) == self.python_tuple()
662 }
663 }
664
665 pub(crate) fn has_default_executable_name(&self) -> bool {
668 let Some(file_name) = self.sys_executable().file_name() else {
669 return false;
670 };
671 let Some(name) = file_name.to_str() else {
672 return false;
673 };
674 VersionRequest::Default
675 .executable_names(None)
676 .into_iter()
677 .any(|default_name| name == default_name.to_string())
678 }
679
680 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
682 if let Some(target) = self.target() {
683 LockedFile::acquire(
685 target.root().join(".lock"),
686 LockedFileMode::Exclusive,
687 target.root().user_display(),
688 )
689 .await
690 } else if let Some(prefix) = self.prefix() {
691 LockedFile::acquire(
693 prefix.root().join(".lock"),
694 LockedFileMode::Exclusive,
695 prefix.root().user_display(),
696 )
697 .await
698 } else if self.is_virtualenv() {
699 LockedFile::acquire(
701 self.sys_prefix.join(".lock"),
702 LockedFileMode::Exclusive,
703 self.sys_prefix.user_display(),
704 )
705 .await
706 } else {
707 LockedFile::acquire(
709 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
710 LockedFileMode::Exclusive,
711 self.sys_prefix.user_display(),
712 )
713 .await
714 }
715 }
716}
717
718pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
721 let path = path.as_ref();
722 debug_assert!(
723 path.is_absolute(),
724 "path must be absolute: {}",
725 path.display()
726 );
727
728 #[cfg(windows)]
729 {
730 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
731 Ok(dunce::canonicalize(launcher.python_path)?)
732 } else {
733 Ok(path.to_path_buf())
734 }
735 }
736
737 #[cfg(unix)]
738 fs_err::canonicalize(path)
739}
740
741#[derive(Debug, Default, Clone)]
745pub struct ExternallyManaged {
746 error: Option<String>,
747}
748
749impl ExternallyManaged {
750 pub fn into_error(self) -> Option<String> {
752 self.error
753 }
754}
755
756#[derive(Debug, Error)]
757pub struct UnexpectedResponseError {
758 #[source]
759 pub(super) err: serde_json::Error,
760 pub(super) stdout: String,
761 pub(super) stderr: String,
762 pub(super) path: PathBuf,
763}
764
765impl Display for UnexpectedResponseError {
766 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
767 write!(
768 f,
769 "Querying Python at `{}` returned an invalid response: {}",
770 self.path.display(),
771 self.err
772 )?;
773
774 let mut non_empty = false;
775
776 if !self.stdout.trim().is_empty() {
777 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
778 non_empty = true;
779 }
780
781 if !self.stderr.trim().is_empty() {
782 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
783 non_empty = true;
784 }
785
786 if non_empty {
787 writeln!(f)?;
788 }
789
790 Ok(())
791 }
792}
793
794#[derive(Debug, Error)]
795pub struct StatusCodeError {
796 pub(super) code: ExitStatus,
797 pub(super) stdout: String,
798 pub(super) stderr: String,
799 pub(super) path: PathBuf,
800}
801
802impl Display for StatusCodeError {
803 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
804 write!(
805 f,
806 "Querying Python at `{}` failed with exit status {}",
807 self.path.display(),
808 self.code
809 )?;
810
811 let mut non_empty = false;
812
813 if !self.stdout.trim().is_empty() {
814 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
815 non_empty = true;
816 }
817
818 if !self.stderr.trim().is_empty() {
819 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
820 non_empty = true;
821 }
822
823 if non_empty {
824 writeln!(f)?;
825 }
826
827 Ok(())
828 }
829}
830
831#[derive(Debug, Error)]
832pub enum Error {
833 #[error("Failed to query Python interpreter")]
834 Io(#[from] io::Error),
835 #[error(transparent)]
836 BrokenLink(BrokenLink),
837 #[error("Python interpreter not found at `{0}`")]
838 NotFound(PathBuf),
839 #[error("Failed to query Python interpreter at `{path}`")]
840 SpawnFailed {
841 path: PathBuf,
842 #[source]
843 err: io::Error,
844 },
845 #[cfg(windows)]
846 #[error("Failed to query Python interpreter at `{path}`")]
847 CorruptWindowsPackage {
848 path: PathBuf,
849 #[source]
850 err: io::Error,
851 },
852 #[error("Failed to query Python interpreter at `{path}`")]
853 PermissionDenied {
854 path: PathBuf,
855 #[source]
856 err: io::Error,
857 },
858 #[error("{0}")]
859 UnexpectedResponse(UnexpectedResponseError),
860 #[error("{0}")]
861 StatusCode(StatusCodeError),
862 #[error("Can't use Python at `{path}`")]
863 QueryScript {
864 #[source]
865 err: InterpreterInfoError,
866 path: PathBuf,
867 },
868 #[error("Failed to write to cache")]
869 Encode(#[from] rmp_serde::encode::Error),
870}
871
872#[derive(Debug, Error)]
873pub struct BrokenLink {
874 pub path: PathBuf,
875 pub unix: bool,
878 pub venv: bool,
880}
881
882impl Display for BrokenLink {
883 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
884 if self.unix {
885 write!(
886 f,
887 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
888 self.path.user_display()
889 )?;
890 } else {
891 write!(
892 f,
893 "Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
894 self.path.user_display()
895 )?;
896 }
897 if self.venv {
898 write!(
899 f,
900 "\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
901 "hint".bold().cyan(),
902 ":".bold(),
903 "uv venv".green()
904 )?;
905 }
906 Ok(())
907 }
908}
909
910#[derive(Debug, Deserialize, Serialize)]
911#[serde(tag = "result", rename_all = "lowercase")]
912enum InterpreterInfoResult {
913 Error(InterpreterInfoError),
914 Success(Box<InterpreterInfo>),
915}
916
917#[derive(Debug, Error, Deserialize, Serialize)]
918#[serde(tag = "kind", rename_all = "snake_case")]
919pub enum InterpreterInfoError {
920 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
921 LibcNotFound,
922 #[error(
923 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
924 )]
925 BrokenMacVer,
926 #[error("Unknown operating system: `{operating_system}`")]
927 UnknownOperatingSystem { operating_system: String },
928 #[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
929 UnsupportedPythonVersion { python_version: String },
930 #[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
931 UnsupportedPython,
932 #[error(
933 "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`."
934 )]
935 MissingRequiredDistutils {
936 python_major: usize,
937 python_minor: usize,
938 },
939 #[error("Only Pyodide is support for Emscripten Python")]
940 EmscriptenNotPyodide,
941}
942
943#[expect(clippy::struct_excessive_bools)]
944#[derive(Debug, Deserialize, Serialize, Clone)]
945struct InterpreterInfo {
946 platform: Platform,
947 markers: MarkerEnvironment,
948 scheme: Scheme,
949 virtualenv: Scheme,
950 manylinux_compatible: bool,
951 sys_prefix: PathBuf,
952 sys_base_exec_prefix: PathBuf,
953 sys_base_prefix: PathBuf,
954 sys_base_executable: Option<PathBuf>,
955 sys_executable: PathBuf,
956 sys_path: Vec<PathBuf>,
957 site_packages: Vec<PathBuf>,
958 stdlib: PathBuf,
959 standalone: bool,
960 pointer_size: PointerSize,
961 gil_disabled: bool,
962 debug_enabled: bool,
963}
964
965impl InterpreterInfo {
966 pub(crate) fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
968 let tempdir = tempfile::tempdir_in(cache.root())?;
969 Self::setup_python_query_files(tempdir.path())?;
970
971 let script = format!(
975 r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
976 tempdir.path().escape_for_python()
977 );
978 let mut command = Command::new(interpreter);
979 command
980 .arg("-I") .arg("-B") .arg("-c")
983 .arg(script);
984
985 #[cfg(target_os = "macos")]
994 command.env("SYSTEM_VERSION_COMPAT", "0");
995
996 let output = command.output().map_err(|err| {
997 match err.kind() {
998 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
999 io::ErrorKind::PermissionDenied => {
1000 return Error::PermissionDenied {
1001 path: interpreter.to_path_buf(),
1002 err,
1003 };
1004 }
1005 _ => {}
1006 }
1007 #[cfg(windows)]
1008 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
1009 .raw_os_error()
1010 .and_then(|code| u32::try_from(code).ok())
1011 .map(WIN32_ERROR)
1012 {
1013 return Error::CorruptWindowsPackage {
1016 path: interpreter.to_path_buf(),
1017 err,
1018 };
1019 }
1020 Error::SpawnFailed {
1021 path: interpreter.to_path_buf(),
1022 err,
1023 }
1024 })?;
1025
1026 if !output.status.success() {
1027 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1028
1029 if python_home(interpreter).is_some_and(|home| !home.exists()) {
1035 return Err(Error::BrokenLink(BrokenLink {
1036 path: interpreter.to_path_buf(),
1037 unix: false,
1038 venv: uv_fs::is_virtualenv_executable(interpreter),
1039 }));
1040 }
1041
1042 if stderr.contains("Unknown option: -I") {
1044 return Err(Error::QueryScript {
1045 err: InterpreterInfoError::UnsupportedPython,
1046 path: interpreter.to_path_buf(),
1047 });
1048 }
1049
1050 return Err(Error::StatusCode(StatusCodeError {
1051 code: output.status,
1052 stderr,
1053 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1054 path: interpreter.to_path_buf(),
1055 }));
1056 }
1057
1058 let result: InterpreterInfoResult =
1059 serde_json::from_slice(&output.stdout).map_err(|err| {
1060 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1061
1062 if stderr.contains("Unknown option: -I") {
1064 Error::QueryScript {
1065 err: InterpreterInfoError::UnsupportedPython,
1066 path: interpreter.to_path_buf(),
1067 }
1068 } else {
1069 Error::UnexpectedResponse(UnexpectedResponseError {
1070 err,
1071 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1072 stderr,
1073 path: interpreter.to_path_buf(),
1074 })
1075 }
1076 })?;
1077
1078 match result {
1079 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1080 err,
1081 path: interpreter.to_path_buf(),
1082 }),
1083 InterpreterInfoResult::Success(data) => Ok(*data),
1084 }
1085 }
1086
1087 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1090 let python_dir = root.join("python");
1091 fs_err::create_dir(&python_dir)?;
1092 fs_err::write(
1093 python_dir.join("get_interpreter_info.py"),
1094 include_str!("../python/get_interpreter_info.py"),
1095 )?;
1096 fs_err::write(
1097 python_dir.join("__init__.py"),
1098 include_str!("../python/__init__.py"),
1099 )?;
1100 let packaging_dir = python_dir.join("packaging");
1101 fs_err::create_dir(&packaging_dir)?;
1102 fs_err::write(
1103 packaging_dir.join("__init__.py"),
1104 include_str!("../python/packaging/__init__.py"),
1105 )?;
1106 fs_err::write(
1107 packaging_dir.join("_elffile.py"),
1108 include_str!("../python/packaging/_elffile.py"),
1109 )?;
1110 fs_err::write(
1111 packaging_dir.join("_manylinux.py"),
1112 include_str!("../python/packaging/_manylinux.py"),
1113 )?;
1114 fs_err::write(
1115 packaging_dir.join("_musllinux.py"),
1116 include_str!("../python/packaging/_musllinux.py"),
1117 )?;
1118 Ok(())
1119 }
1120
1121 pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1127 let absolute = std::path::absolute(executable)?;
1128
1129 let handle_io_error = |err: io::Error| -> Error {
1133 if err.kind() == io::ErrorKind::NotFound {
1134 if absolute
1137 .symlink_metadata()
1138 .is_ok_and(|metadata| metadata.is_symlink())
1139 {
1140 Error::BrokenLink(BrokenLink {
1141 path: executable.to_path_buf(),
1142 unix: true,
1143 venv: uv_fs::is_virtualenv_executable(executable),
1144 })
1145 } else {
1146 Error::NotFound(executable.to_path_buf())
1147 }
1148 } else {
1149 err.into()
1150 }
1151 };
1152
1153 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1154
1155 let cache_entry = cache.entry(
1156 CacheBucket::Interpreter,
1157 cache_digest(&(
1160 ARCH,
1161 uv_platform::host::OsType::from_env()
1162 .map(|os_type| os_type.to_string())
1163 .unwrap_or_default(),
1164 uv_platform::host::OsRelease::from_env()
1165 .map(|os_release| os_release.to_string())
1166 .unwrap_or_default(),
1167 )),
1168 format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
1176 );
1177
1178 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1181
1182 if cache
1184 .freshness(&cache_entry, None, None)
1185 .is_ok_and(Freshness::is_fresh)
1186 {
1187 if let Ok(data) = fs::read(cache_entry.path()) {
1188 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1189 Ok(cached) => {
1190 if cached.timestamp == modified {
1191 trace!(
1192 "Found cached interpreter info for Python {}, skipping query of: {}",
1193 cached.data.markers.python_full_version(),
1194 executable.user_display()
1195 );
1196 return Ok(cached.data);
1197 }
1198
1199 trace!(
1200 "Ignoring stale interpreter markers for: {}",
1201 executable.user_display()
1202 );
1203 }
1204 Err(err) => {
1205 warn!(
1206 "Broken interpreter cache entry at {}, removing: {err}",
1207 cache_entry.path().user_display()
1208 );
1209 let _ = fs_err::remove_file(cache_entry.path());
1210 }
1211 }
1212 }
1213 }
1214
1215 trace!(
1217 "Querying interpreter executable at {}",
1218 executable.display()
1219 );
1220 let info = Self::query(executable, cache)?;
1221
1222 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1225 fs::create_dir_all(cache_entry.dir())?;
1226 write_atomic_sync(
1227 cache_entry.path(),
1228 rmp_serde::to_vec(&CachedByTimestamp {
1229 timestamp: modified,
1230 data: info.clone(),
1231 })?,
1232 )?;
1233 }
1234
1235 Ok(info)
1236 }
1237}
1238
1239fn find_base_python(
1265 executable: &Path,
1266 major: u8,
1267 minor: u8,
1268 suffix: &str,
1269) -> Result<PathBuf, io::Error> {
1270 fn is_root(path: &Path) -> bool {
1272 let mut components = path.components();
1273 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1274 }
1275
1276 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1280 if cfg!(windows) {
1281 dir.join("Lib").join("os.py").is_file()
1282 } else {
1283 dir.join("lib")
1284 .join(format!("python{major}.{minor}{suffix}"))
1285 .join("os.py")
1286 .is_file()
1287 }
1288 }
1289
1290 let mut executable = Cow::Borrowed(executable);
1291
1292 loop {
1293 debug!(
1294 "Assessing Python executable as base candidate: {}",
1295 executable.display()
1296 );
1297
1298 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1300 if is_prefix(prefix, major, minor, suffix) {
1301 return Ok(executable.into_owned());
1302 }
1303 }
1304
1305 let resolved = fs_err::read_link(&executable)?;
1307
1308 let resolved = if resolved.is_relative() {
1310 if let Some(parent) = executable.parent() {
1311 parent.join(resolved)
1312 } else {
1313 return Err(io::Error::other("Symlink has no parent directory"));
1314 }
1315 } else {
1316 resolved
1317 };
1318
1319 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1321
1322 executable = Cow::Owned(resolved);
1323 }
1324}
1325
1326fn python_home(interpreter: &Path) -> Option<PathBuf> {
1328 let venv_root = interpreter.parent()?.parent()?;
1329 let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
1330 pyvenv_cfg.home
1331}
1332
1333#[cfg(unix)]
1334#[cfg(test)]
1335mod tests {
1336 use std::str::FromStr;
1337
1338 use fs_err as fs;
1339 use indoc::{formatdoc, indoc};
1340 use tempfile::tempdir;
1341
1342 use uv_cache::Cache;
1343 use uv_pep440::Version;
1344
1345 use crate::Interpreter;
1346
1347 #[tokio::test]
1348 async fn test_cache_invalidation() {
1349 let mock_dir = tempdir().unwrap();
1350 let mocked_interpreter = mock_dir.path().join("python");
1351 let json = indoc! {r##"
1352 {
1353 "result": "success",
1354 "platform": {
1355 "os": {
1356 "name": "manylinux",
1357 "major": 2,
1358 "minor": 38
1359 },
1360 "arch": "x86_64"
1361 },
1362 "manylinux_compatible": false,
1363 "standalone": false,
1364 "markers": {
1365 "implementation_name": "cpython",
1366 "implementation_version": "3.12.0",
1367 "os_name": "posix",
1368 "platform_machine": "x86_64",
1369 "platform_python_implementation": "CPython",
1370 "platform_release": "6.5.0-13-generic",
1371 "platform_system": "Linux",
1372 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1373 "python_full_version": "3.12.0",
1374 "python_version": "3.12",
1375 "sys_platform": "linux"
1376 },
1377 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1378 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1379 "sys_prefix": "/home/ferris/projects/uv/.venv",
1380 "sys_executable": "/home/ferris/projects/uv/.venv/bin/python",
1381 "sys_path": [
1382 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1383 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1384 ],
1385 "site_packages": [
1386 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1387 ],
1388 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1389 "scheme": {
1390 "data": "/home/ferris/.pyenv/versions/3.12.0",
1391 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1392 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1393 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1394 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1395 },
1396 "virtualenv": {
1397 "data": "",
1398 "include": "include",
1399 "platlib": "lib/python3.12/site-packages",
1400 "purelib": "lib/python3.12/site-packages",
1401 "scripts": "bin"
1402 },
1403 "pointer_size": "64",
1404 "gil_disabled": true,
1405 "debug_enabled": false
1406 }
1407 "##};
1408
1409 let cache = Cache::temp().unwrap().init().await.unwrap();
1410
1411 fs::write(
1412 &mocked_interpreter,
1413 formatdoc! {r"
1414 #!/bin/sh
1415 echo '{json}'
1416 "},
1417 )
1418 .unwrap();
1419
1420 fs::set_permissions(
1421 &mocked_interpreter,
1422 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1423 )
1424 .unwrap();
1425 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1426 assert_eq!(
1427 interpreter.markers.python_version().version,
1428 Version::from_str("3.12").unwrap()
1429 );
1430 fs::write(
1431 &mocked_interpreter,
1432 formatdoc! {r"
1433 #!/bin/sh
1434 echo '{}'
1435 ", json.replace("3.12", "3.13")},
1436 )
1437 .unwrap();
1438 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1439 assert_eq!(
1440 interpreter.markers.python_version().version,
1441 Version::from_str("3.13").unwrap()
1442 );
1443 }
1444}