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, CacheEntry, 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};
30use uv_static::EnvVars;
31
32use crate::implementation::LenientImplementationName;
33use crate::managed::ManagedPythonInstallations;
34use crate::pointer_size::PointerSize;
35use crate::{
36 Prefix, PyVenvConfiguration, PythonInstallationKey, PythonVariant, PythonVersion, Target,
37 VersionRequest, VirtualEnvironment,
38};
39
40#[cfg(windows)]
41use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
42
43#[expect(clippy::struct_excessive_bools)]
45#[derive(Debug, Clone)]
46pub struct Interpreter {
47 platform: Platform,
48 markers: Box<MarkerEnvironment>,
49 scheme: Scheme,
50 virtualenv: Scheme,
51 manylinux_compatible: bool,
52 sys_prefix: PathBuf,
53 sys_base_prefix: PathBuf,
54 sys_base_executable: Option<PathBuf>,
55 sys_executable: PathBuf,
56 site_packages: Vec<PathBuf>,
57 stdlib: PathBuf,
58 extension_suffixes: Vec<Box<str>>,
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 executable = executable.as_ref();
73 let info = InterpreterInfo::query_cached(executable, cache)?;
74
75 debug_assert!(
76 info.sys_executable.is_absolute(),
77 "`sys.executable` is not an absolute Python; Python installation is broken: {}",
78 info.sys_executable.display()
79 );
80
81 Ok(Self {
82 platform: info.platform,
83 markers: Box::new(info.markers),
84 scheme: info.scheme,
85 virtualenv: info.virtualenv,
86 manylinux_compatible: info.manylinux_compatible,
87 sys_prefix: info.sys_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 site_packages: info.site_packages,
95 stdlib: info.stdlib,
96 extension_suffixes: info.extension_suffixes,
97 standalone: info.standalone,
98 tags: OnceLock::new(),
99 target: None,
100 prefix: None,
101 real_executable: executable.to_path_buf(),
102 })
103 }
104
105 pub fn clear_cache(executable: impl AsRef<Path>, cache: &Cache) -> Result<(), Error> {
107 let absolute = std::path::absolute(executable.as_ref())?;
108 let canonical = canonicalize_executable(&absolute)?;
109 let cache_entry = InterpreterInfo::cache_entry(&absolute, &canonical, cache);
110
111 match fs::remove_file(cache_entry.path()) {
112 Ok(()) => Ok(()),
113 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
114 Err(err) => Err(err.into()),
115 }
116 }
117
118 #[must_use]
120 pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
121 Self {
122 scheme: virtualenv.scheme,
123 sys_base_executable: Some(virtualenv.base_executable),
124 sys_executable: virtualenv.executable,
125 sys_prefix: virtualenv.root,
126 target: None,
127 prefix: None,
128 site_packages: vec![],
129 ..self
130 }
131 }
132
133 pub(crate) fn with_target(self, target: Target) -> io::Result<Self> {
135 target.init()?;
136 Ok(Self {
137 target: Some(target),
138 ..self
139 })
140 }
141
142 pub(crate) fn with_prefix(self, prefix: Prefix) -> io::Result<Self> {
144 prefix.init(self.virtualenv())?;
145 Ok(Self {
146 prefix: Some(prefix),
147 ..self
148 })
149 }
150
151 pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
161 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
162 let base_python = std::path::absolute(base_executable)?;
163 Ok(base_python)
164 }
165
166 pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
177 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
178 let base_python = match find_base_python(
188 base_executable,
189 self.python_major(),
190 self.python_minor(),
191 self.variant().executable_suffix(),
192 ) {
193 Ok(path) => path,
194 Err(err) => {
195 warn!("Failed to find base Python executable: {err}");
196 canonicalize_executable(base_executable)?
197 }
198 };
199 Ok(base_python)
200 }
201
202 #[inline]
204 pub fn platform(&self) -> &Platform {
205 &self.platform
206 }
207
208 #[inline]
210 pub const fn markers(&self) -> &MarkerEnvironment {
211 &self.markers
212 }
213
214 pub fn to_resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
216 ResolverMarkerEnvironment::from(self.markers().clone())
217 }
218
219 pub fn key(&self) -> PythonInstallationKey {
221 PythonInstallationKey::new(
222 LenientImplementationName::from(self.implementation_name()),
223 self.python_major(),
224 self.python_minor(),
225 self.python_patch(),
226 self.python_version().pre(),
227 uv_platform::Platform::new(self.os(), self.arch(), self.libc()),
228 self.variant(),
229 )
230 }
231
232 pub fn variant(&self) -> PythonVariant {
233 if self.gil_disabled() {
234 if self.debug_enabled() {
235 PythonVariant::FreethreadedDebug
236 } else {
237 PythonVariant::Freethreaded
238 }
239 } else if self.debug_enabled() {
240 PythonVariant::Debug
241 } else {
242 PythonVariant::default()
243 }
244 }
245
246 pub(crate) fn arch(&self) -> Arch {
248 Arch::from(&self.platform().arch())
249 }
250
251 pub(crate) fn libc(&self) -> Libc {
253 Libc::from(self.platform().os())
254 }
255
256 pub(crate) fn os(&self) -> Os {
258 Os::from(self.platform().os())
259 }
260
261 pub fn tags(&self) -> Result<&Tags, TagsError> {
263 if self.tags.get().is_none() {
264 let tags = Tags::from_env(
265 self.platform().clone(),
266 self.python_tuple(),
267 self.implementation_name(),
268 self.implementation_tuple(),
269 TagsOptions {
270 manylinux_compatible: self.manylinux_compatible,
271 gil_disabled: self.gil_disabled,
272 debug_enabled: self.debug_enabled,
273 is_cross: false,
274 },
275 )?;
276 self.tags.set(tags).expect("tags should not be set");
277 }
278 Ok(self.tags.get().expect("tags should be set"))
279 }
280
281 pub fn is_virtualenv(&self) -> bool {
285 self.sys_prefix != self.sys_base_prefix
287 }
288
289 fn is_target(&self) -> bool {
291 self.target.is_some()
292 }
293
294 fn is_prefix(&self) -> bool {
296 self.prefix.is_some()
297 }
298
299 pub(crate) fn is_managed(&self) -> bool {
303 if let Ok(test_managed) =
304 std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED)
305 {
306 return test_managed.split_ascii_whitespace().any(|item| {
309 let version = <PythonVersion as std::str::FromStr>::from_str(item).expect(
310 "`UV_INTERNAL__TEST_PYTHON_MANAGED` items should be valid Python versions",
311 );
312 if version.patch().is_some() {
313 version.version() == self.python_version()
314 } else {
315 (version.major(), version.minor()) == self.python_tuple()
316 }
317 });
318 }
319
320 let Ok(installations) = ManagedPythonInstallations::from_settings(None) else {
321 return false;
322 };
323 let Ok(root) = installations.absolute_root() else {
324 return false;
325 };
326 let sys_base_prefix = dunce::canonicalize(&self.sys_base_prefix)
327 .unwrap_or_else(|_| self.sys_base_prefix.clone());
328 let root = dunce::canonicalize(&root).unwrap_or(root);
329
330 let Ok(suffix) = sys_base_prefix.strip_prefix(&root) else {
331 return false;
332 };
333
334 let Some(first_component) = suffix.components().next() else {
335 return false;
336 };
337
338 let Some(name) = first_component.as_os_str().to_str() else {
339 return false;
340 };
341
342 PythonInstallationKey::from_str(name).is_ok()
343 }
344
345 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
350 if self.is_virtualenv() {
352 return None;
353 }
354
355 if self.is_target() || self.is_prefix() {
357 return None;
358 }
359
360 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
361 return None;
362 };
363
364 let mut ini = Ini::new_cs();
365 ini.set_multiline(true);
366
367 let Ok(mut sections) = ini.read(contents) else {
368 return Some(ExternallyManaged::default());
371 };
372
373 let Some(section) = sections.get_mut("externally-managed") else {
374 return Some(ExternallyManaged::default());
377 };
378
379 let Some(error) = section.remove("Error") else {
380 return Some(ExternallyManaged::default());
383 };
384
385 Some(ExternallyManaged { error })
386 }
387
388 #[inline]
390 pub fn python_full_version(&self) -> &StringVersion {
391 self.markers.python_full_version()
392 }
393
394 #[inline]
396 pub fn python_version(&self) -> &Version {
397 &self.markers.python_full_version().version
398 }
399
400 #[inline]
402 pub fn python_minor_version(&self) -> Version {
403 Version::new(self.python_version().release().iter().take(2).copied())
404 }
405
406 #[inline]
408 pub(crate) fn python_patch_version(&self) -> Version {
409 Version::new(self.python_version().release().iter().take(3).copied())
410 }
411
412 pub fn python_major(&self) -> u8 {
414 let major = self.markers.python_full_version().version.release()[0];
415 u8::try_from(major).expect("invalid major version")
416 }
417
418 pub fn python_minor(&self) -> u8 {
420 let minor = self.markers.python_full_version().version.release()[1];
421 u8::try_from(minor).expect("invalid minor version")
422 }
423
424 pub(crate) fn python_patch(&self) -> u8 {
426 let minor = self.markers.python_full_version().version.release()[2];
427 u8::try_from(minor).expect("invalid patch version")
428 }
429
430 pub fn python_tuple(&self) -> (u8, u8) {
432 (self.python_major(), self.python_minor())
433 }
434
435 fn implementation_major(&self) -> u8 {
437 let major = self.markers.implementation_version().version.release()[0];
438 u8::try_from(major).expect("invalid major version")
439 }
440
441 fn implementation_minor(&self) -> u8 {
443 let minor = self.markers.implementation_version().version.release()[1];
444 u8::try_from(minor).expect("invalid minor version")
445 }
446
447 pub fn implementation_tuple(&self) -> (u8, u8) {
449 (self.implementation_major(), self.implementation_minor())
450 }
451
452 pub fn implementation_name(&self) -> &str {
454 self.markers.implementation_name()
455 }
456
457 pub fn sys_base_prefix(&self) -> &Path {
459 &self.sys_base_prefix
460 }
461
462 pub fn sys_prefix(&self) -> &Path {
464 &self.sys_prefix
465 }
466
467 pub(crate) fn sys_base_executable(&self) -> Option<&Path> {
470 self.sys_base_executable.as_deref()
471 }
472
473 pub fn sys_executable(&self) -> &Path {
475 &self.sys_executable
476 }
477
478 pub fn extension_suffixes(&self) -> &[Box<str>] {
480 &self.extension_suffixes
481 }
482
483 pub fn real_executable(&self) -> &Path {
485 &self.real_executable
486 }
487
488 pub fn runtime_site_packages(&self) -> &[PathBuf] {
496 &self.site_packages
497 }
498
499 pub fn stdlib(&self) -> &Path {
501 &self.stdlib
502 }
503
504 fn purelib(&self) -> &Path {
506 &self.scheme.purelib
507 }
508
509 fn platlib(&self) -> &Path {
511 &self.scheme.platlib
512 }
513
514 pub fn scripts(&self) -> &Path {
516 &self.scheme.scripts
517 }
518
519 fn data(&self) -> &Path {
521 &self.scheme.data
522 }
523
524 fn include(&self) -> &Path {
526 &self.scheme.include
527 }
528
529 pub fn virtualenv(&self) -> &Scheme {
531 &self.virtualenv
532 }
533
534 pub fn manylinux_compatible(&self) -> bool {
536 self.manylinux_compatible
537 }
538
539 pub fn pointer_size(&self) -> PointerSize {
541 self.pointer_size
542 }
543
544 pub fn gil_disabled(&self) -> bool {
550 self.gil_disabled
551 }
552
553 pub fn debug_enabled(&self) -> bool {
556 self.debug_enabled
557 }
558
559 fn target(&self) -> Option<&Target> {
561 self.target.as_ref()
562 }
563
564 fn prefix(&self) -> Option<&Prefix> {
566 self.prefix.as_ref()
567 }
568
569 #[cfg(unix)]
578 pub fn is_standalone(&self) -> bool {
579 self.standalone
580 }
581
582 #[cfg(windows)]
586 pub fn is_standalone(&self) -> bool {
587 self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
588 }
589
590 pub fn layout(&self) -> Layout {
592 Layout {
593 python_version: self.python_tuple(),
594 sys_executable: self.sys_executable().to_path_buf(),
595 os_name: self.markers.os_name().to_string(),
596 scheme: if let Some(target) = self.target.as_ref() {
597 target.scheme()
598 } else if let Some(prefix) = self.prefix.as_ref() {
599 prefix.scheme(&self.virtualenv)
600 } else {
601 Scheme {
602 purelib: self.purelib().to_path_buf(),
603 platlib: self.platlib().to_path_buf(),
604 scripts: self.scripts().to_path_buf(),
605 data: self.data().to_path_buf(),
606 include: if self.is_virtualenv() {
607 self.sys_prefix.join("include").join("site").join(format!(
610 "python{}.{}",
611 self.python_major(),
612 self.python_minor()
613 ))
614 } else {
615 self.include().to_path_buf()
616 },
617 }
618 },
619 }
620 }
621
622 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
633 let target = self.target().map(Target::site_packages);
634
635 let prefix = self
636 .prefix()
637 .map(|prefix| prefix.site_packages(self.virtualenv()));
638
639 let interpreter = if target.is_none() && prefix.is_none() {
640 let purelib = self.purelib();
641 let platlib = self.platlib();
642 Some(std::iter::once(purelib).chain(
643 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
644 None
645 } else {
646 Some(platlib)
647 },
648 ))
649 } else {
650 None
651 };
652
653 target
654 .into_iter()
655 .flatten()
656 .map(Cow::Borrowed)
657 .chain(prefix.into_iter().flatten().map(Cow::Owned))
658 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
659 }
660
661 pub(crate) fn has_default_executable_name(&self) -> bool {
664 let Some(file_name) = self.sys_executable().file_name() else {
665 return false;
666 };
667 let Some(name) = file_name.to_str() else {
668 return false;
669 };
670 VersionRequest::Default
671 .executable_names(None)
672 .into_iter()
673 .any(|default_name| name == default_name.to_string())
674 }
675
676 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
678 if let Some(target) = self.target() {
679 LockedFile::acquire(
681 target.root().join(".lock"),
682 LockedFileMode::Exclusive,
683 target.root().user_display(),
684 )
685 .await
686 } else if let Some(prefix) = self.prefix() {
687 LockedFile::acquire(
689 prefix.root().join(".lock"),
690 LockedFileMode::Exclusive,
691 prefix.root().user_display(),
692 )
693 .await
694 } else if self.is_virtualenv() {
695 LockedFile::acquire(
697 self.sys_prefix.join(".lock"),
698 LockedFileMode::Exclusive,
699 self.sys_prefix.user_display(),
700 )
701 .await
702 } else {
703 LockedFile::acquire(
705 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
706 LockedFileMode::Exclusive,
707 self.sys_prefix.user_display(),
708 )
709 .await
710 }
711 }
712}
713
714pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
717 let path = path.as_ref();
718 debug_assert!(
719 path.is_absolute(),
720 "path must be absolute: {}",
721 path.display()
722 );
723
724 #[cfg(windows)]
725 {
726 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
727 Ok(dunce::canonicalize(launcher.python_path)?)
728 } else {
729 Ok(path.to_path_buf())
730 }
731 }
732
733 #[cfg(unix)]
734 fs_err::canonicalize(path)
735}
736
737#[derive(Debug, Default, Clone)]
741pub struct ExternallyManaged {
742 error: Option<String>,
743}
744
745impl ExternallyManaged {
746 pub fn into_error(self) -> Option<String> {
748 self.error
749 }
750}
751
752#[derive(Debug, Error)]
753pub struct UnexpectedResponseError {
754 #[source]
755 pub(super) err: serde_json::Error,
756 pub(super) stdout: String,
757 pub(super) stderr: String,
758 pub(super) path: PathBuf,
759}
760
761impl Display for UnexpectedResponseError {
762 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
763 write!(
764 f,
765 "Querying Python at `{}` returned an invalid response: {}",
766 self.path.display(),
767 self.err
768 )?;
769
770 let mut non_empty = false;
771
772 if !self.stdout.trim().is_empty() {
773 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
774 non_empty = true;
775 }
776
777 if !self.stderr.trim().is_empty() {
778 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
779 non_empty = true;
780 }
781
782 if non_empty {
783 writeln!(f)?;
784 }
785
786 Ok(())
787 }
788}
789
790#[derive(Debug, Error)]
791pub struct StatusCodeError {
792 pub(super) code: ExitStatus,
793 pub(super) stdout: String,
794 pub(super) stderr: String,
795 pub(super) path: PathBuf,
796}
797
798impl Display for StatusCodeError {
799 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
800 write!(
801 f,
802 "Querying Python at `{}` failed with exit status {}",
803 self.path.display(),
804 self.code
805 )?;
806
807 let mut non_empty = false;
808
809 if !self.stdout.trim().is_empty() {
810 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
811 non_empty = true;
812 }
813
814 if !self.stderr.trim().is_empty() {
815 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
816 non_empty = true;
817 }
818
819 if non_empty {
820 writeln!(f)?;
821 }
822
823 Ok(())
824 }
825}
826
827#[derive(Debug, Error)]
828pub enum Error {
829 #[error("Failed to query Python interpreter")]
830 Io(#[from] io::Error),
831 #[error(transparent)]
832 BrokenLink(BrokenLink),
833 #[error("Python interpreter not found at `{0}`")]
834 NotFound(PathBuf),
835 #[error("Failed to query Python interpreter at `{path}`")]
836 SpawnFailed {
837 path: PathBuf,
838 #[source]
839 err: io::Error,
840 },
841 #[cfg(windows)]
842 #[error("Failed to query Python interpreter at `{path}`")]
843 CorruptWindowsPackage {
844 path: PathBuf,
845 #[source]
846 err: io::Error,
847 },
848 #[error("Failed to query Python interpreter at `{path}`")]
849 PermissionDenied {
850 path: PathBuf,
851 #[source]
852 err: io::Error,
853 },
854 #[error("{0}")]
855 UnexpectedResponse(UnexpectedResponseError),
856 #[error("{0}")]
857 StatusCode(StatusCodeError),
858 #[error("Can't use Python at `{path}`")]
859 QueryScript {
860 #[source]
861 err: InterpreterInfoError,
862 path: PathBuf,
863 },
864 #[error("Failed to write to cache")]
865 Encode(#[from] rmp_serde::encode::Error),
866}
867
868impl uv_errors::Hint for Error {
869 fn hints(&self) -> uv_errors::Hints<'_> {
870 match self {
871 Self::BrokenLink(err) => err.hints(),
872 _ => uv_errors::Hints::none(),
873 }
874 }
875}
876
877#[derive(Debug, Error)]
878pub struct BrokenLink {
879 pub path: PathBuf,
880 pub unix: bool,
883 pub venv: bool,
885}
886
887impl Display for BrokenLink {
888 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
889 if self.unix {
890 write!(
891 f,
892 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
893 self.path.user_display()
894 )
895 } else {
896 write!(
897 f,
898 "Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
899 self.path.user_display()
900 )
901 }
902 }
903}
904
905impl uv_errors::Hint for BrokenLink {
906 fn hints(&self) -> uv_errors::Hints<'_> {
907 if self.venv {
908 uv_errors::Hints::from(format!(
909 "Consider recreating the environment (e.g., with `{}`)",
910 "uv venv".green()
911 ))
912 } else {
913 uv_errors::Hints::none()
914 }
915 }
916}
917
918#[derive(Debug, Deserialize, Serialize)]
919#[serde(tag = "result", rename_all = "lowercase")]
920enum InterpreterInfoResult {
921 Error(InterpreterInfoError),
922 Success(Box<InterpreterInfo>),
923}
924
925#[derive(Debug, Error, Deserialize, Serialize)]
926#[serde(tag = "kind", rename_all = "snake_case")]
927pub enum InterpreterInfoError {
928 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
929 LibcNotFound,
930 #[error(
931 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
932 )]
933 BrokenMacVer,
934 #[error("Unknown operating system: `{operating_system}`")]
935 UnknownOperatingSystem { operating_system: String },
936 #[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
937 UnsupportedPythonVersion { python_version: String },
938 #[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
939 UnsupportedPython,
940 #[error(
941 "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`."
942 )]
943 MissingRequiredDistutils {
944 python_major: usize,
945 python_minor: usize,
946 },
947 #[error("Only Pyodide is supported for Emscripten Python")]
948 EmscriptenNotPyodide,
949}
950
951#[expect(clippy::struct_excessive_bools)]
952#[derive(Debug, Deserialize, Serialize, Clone)]
953struct InterpreterInfo {
954 platform: Platform,
955 markers: MarkerEnvironment,
956 scheme: Scheme,
957 virtualenv: Scheme,
958 manylinux_compatible: bool,
959 sys_prefix: PathBuf,
960 sys_base_exec_prefix: PathBuf,
961 sys_base_prefix: PathBuf,
962 sys_base_executable: Option<PathBuf>,
963 sys_executable: PathBuf,
964 sys_path: Vec<PathBuf>,
965 site_packages: Vec<PathBuf>,
966 stdlib: PathBuf,
967 extension_suffixes: Vec<Box<str>>,
968 standalone: bool,
969 pointer_size: PointerSize,
970 gil_disabled: bool,
971 debug_enabled: bool,
972}
973
974impl InterpreterInfo {
975 fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
977 let tempdir = tempfile::tempdir_in(cache.root())?;
978 Self::setup_python_query_files(tempdir.path())?;
979
980 let script = format!(
988 r"import sys; sys.path = [{}] + sys.path; from python.get_interpreter_info import main; main()",
989 tempdir.path().escape_for_python()
990 );
991 let mut command = Command::new(interpreter);
992 command
993 .arg("-I") .arg("-B") .arg("-c")
996 .arg(script);
997
998 #[cfg(target_os = "macos")]
1007 command.env("SYSTEM_VERSION_COMPAT", "0");
1008
1009 let output = command.output().map_err(|err| {
1010 match err.kind() {
1011 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
1012 io::ErrorKind::PermissionDenied => {
1013 return Error::PermissionDenied {
1014 path: interpreter.to_path_buf(),
1015 err,
1016 };
1017 }
1018 _ => {}
1019 }
1020 #[cfg(windows)]
1021 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
1022 .raw_os_error()
1023 .and_then(|code| u32::try_from(code).ok())
1024 .map(WIN32_ERROR)
1025 {
1026 return Error::CorruptWindowsPackage {
1029 path: interpreter.to_path_buf(),
1030 err,
1031 };
1032 }
1033 Error::SpawnFailed {
1034 path: interpreter.to_path_buf(),
1035 err,
1036 }
1037 })?;
1038
1039 if !output.status.success() {
1040 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1041
1042 if python_home(interpreter).is_some_and(|home| !home.exists()) {
1048 return Err(Error::BrokenLink(BrokenLink {
1049 path: interpreter.to_path_buf(),
1050 unix: false,
1051 venv: uv_fs::is_virtualenv_executable(interpreter),
1052 }));
1053 }
1054
1055 if stderr.contains("Unknown option: -I") {
1057 return Err(Error::QueryScript {
1058 err: InterpreterInfoError::UnsupportedPython,
1059 path: interpreter.to_path_buf(),
1060 });
1061 }
1062
1063 return Err(Error::StatusCode(StatusCodeError {
1064 code: output.status,
1065 stderr,
1066 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1067 path: interpreter.to_path_buf(),
1068 }));
1069 }
1070
1071 let result: InterpreterInfoResult =
1072 serde_json::from_slice(&output.stdout).map_err(|err| {
1073 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1074
1075 if stderr.contains("Unknown option: -I") {
1077 Error::QueryScript {
1078 err: InterpreterInfoError::UnsupportedPython,
1079 path: interpreter.to_path_buf(),
1080 }
1081 } else {
1082 Error::UnexpectedResponse(UnexpectedResponseError {
1083 err,
1084 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1085 stderr,
1086 path: interpreter.to_path_buf(),
1087 })
1088 }
1089 })?;
1090
1091 match result {
1092 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1093 err,
1094 path: interpreter.to_path_buf(),
1095 }),
1096 InterpreterInfoResult::Success(data) => Ok(*data),
1097 }
1098 }
1099
1100 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1103 let python_dir = root.join("python");
1104 fs_err::create_dir(&python_dir)?;
1105 fs_err::write(
1106 python_dir.join("get_interpreter_info.py"),
1107 include_str!("../python/get_interpreter_info.py"),
1108 )?;
1109 fs_err::write(
1110 python_dir.join("__init__.py"),
1111 include_str!("../python/__init__.py"),
1112 )?;
1113 let packaging_dir = python_dir.join("packaging");
1114 fs_err::create_dir(&packaging_dir)?;
1115 fs_err::write(
1116 packaging_dir.join("__init__.py"),
1117 include_str!("../python/packaging/__init__.py"),
1118 )?;
1119 fs_err::write(
1120 packaging_dir.join("_elffile.py"),
1121 include_str!("../python/packaging/_elffile.py"),
1122 )?;
1123 fs_err::write(
1124 packaging_dir.join("_manylinux.py"),
1125 include_str!("../python/packaging/_manylinux.py"),
1126 )?;
1127 fs_err::write(
1128 packaging_dir.join("_musllinux.py"),
1129 include_str!("../python/packaging/_musllinux.py"),
1130 )?;
1131 Ok(())
1132 }
1133
1134 fn cache_entry(absolute: &Path, canonical: &Path, cache: &Cache) -> CacheEntry {
1136 let python_executable = env::var_os(EnvVars::PYTHONEXECUTABLE).map(PathBuf::from);
1137 let pyvenv_launcher = env::var_os(EnvVars::PYVENV_LAUNCHER).map(PathBuf::from);
1138
1139 cache.entry(
1140 CacheBucket::Interpreter,
1141 cache_digest(&(
1144 ARCH,
1145 uv_platform::OsType::from_env()
1146 .map(|os_type| os_type.to_string())
1147 .unwrap_or_default(),
1148 uv_platform::OsRelease::from_env()
1149 .map(|os_release| os_release.to_string())
1150 .unwrap_or_default(),
1151 )),
1152 format!(
1163 "{}.msgpack",
1164 cache_digest(&(absolute, canonical, &python_executable, &pyvenv_launcher))
1165 ),
1166 )
1167 }
1168
1169 fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1175 let absolute = std::path::absolute(executable)?;
1176
1177 let handle_io_error = |err: io::Error| -> Error {
1181 if err.kind() == io::ErrorKind::NotFound {
1182 if absolute
1185 .symlink_metadata()
1186 .is_ok_and(|metadata| metadata.is_symlink())
1187 {
1188 Error::BrokenLink(BrokenLink {
1189 path: executable.to_path_buf(),
1190 unix: true,
1191 venv: uv_fs::is_virtualenv_executable(executable),
1192 })
1193 } else {
1194 Error::NotFound(executable.to_path_buf())
1195 }
1196 } else {
1197 err.into()
1198 }
1199 };
1200
1201 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1202 let cache_entry = Self::cache_entry(&absolute, &canonical, cache);
1203
1204 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1207
1208 if cache
1210 .freshness(&cache_entry, None, None)
1211 .is_ok_and(Freshness::is_fresh)
1212 {
1213 if let Ok(data) = fs::read(cache_entry.path()) {
1214 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1215 Ok(cached) => {
1216 if cached.timestamp == modified {
1217 trace!(
1218 "Found cached interpreter info for Python {}, skipping query of: {}",
1219 cached.data.markers.python_full_version(),
1220 executable.user_display()
1221 );
1222 return Ok(cached.data);
1223 }
1224
1225 trace!(
1226 "Ignoring stale interpreter markers for: {}",
1227 executable.user_display()
1228 );
1229 }
1230 Err(err) => {
1231 warn!(
1232 "Broken interpreter cache entry at {}, removing: {err}",
1233 cache_entry.path().user_display()
1234 );
1235 let _ = fs_err::remove_file(cache_entry.path());
1236 }
1237 }
1238 }
1239 }
1240
1241 trace!(
1243 "Querying interpreter executable at {}",
1244 executable.display()
1245 );
1246 let info = Self::query(executable, cache)?;
1247
1248 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1251 fs::create_dir_all(cache_entry.dir())?;
1252 write_atomic_sync(
1253 cache_entry.path(),
1254 rmp_serde::to_vec(&CachedByTimestamp {
1255 timestamp: modified,
1256 data: info.clone(),
1257 })?,
1258 )?;
1259 }
1260
1261 Ok(info)
1262 }
1263}
1264
1265fn find_base_python(
1291 executable: &Path,
1292 major: u8,
1293 minor: u8,
1294 suffix: &str,
1295) -> Result<PathBuf, io::Error> {
1296 fn is_root(path: &Path) -> bool {
1298 let mut components = path.components();
1299 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1300 }
1301
1302 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1306 if cfg!(windows) {
1307 dir.join("Lib").join("os.py").is_file()
1308 } else {
1309 dir.join("lib")
1310 .join(format!("python{major}.{minor}{suffix}"))
1311 .join("os.py")
1312 .is_file()
1313 }
1314 }
1315
1316 let mut executable = Cow::Borrowed(executable);
1317
1318 loop {
1319 debug!(
1320 "Assessing Python executable as base candidate: {}",
1321 executable.display()
1322 );
1323
1324 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1326 if is_prefix(prefix, major, minor, suffix) {
1327 return Ok(executable.into_owned());
1328 }
1329 }
1330
1331 let resolved = fs_err::read_link(&executable)?;
1333
1334 let resolved = if resolved.is_relative() {
1336 if let Some(parent) = executable.parent() {
1337 parent.join(resolved)
1338 } else {
1339 return Err(io::Error::other("Symlink has no parent directory"));
1340 }
1341 } else {
1342 resolved
1343 };
1344
1345 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1347
1348 executable = Cow::Owned(resolved);
1349 }
1350}
1351
1352fn python_home(interpreter: &Path) -> Option<PathBuf> {
1354 let venv_root = interpreter.parent()?.parent()?;
1355 let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
1356 pyvenv_cfg.home
1357}
1358
1359#[cfg(unix)]
1360#[cfg(test)]
1361mod tests {
1362 use std::str::FromStr;
1363
1364 use anyhow::Result;
1365 use fs_err as fs;
1366 use indoc::{formatdoc, indoc};
1367 use serde_json::Value;
1368 use tempfile::tempdir;
1369
1370 use uv_cache::{Cache, CacheBucket};
1371 use uv_cache_info::Timestamp;
1372 use uv_pep440::Version;
1373
1374 use crate::Interpreter;
1375
1376 fn mocked_interpreter_response() -> &'static str {
1377 indoc! {r##"
1378 {
1379 "result": "success",
1380 "platform": {
1381 "os": {
1382 "name": "manylinux",
1383 "major": 2,
1384 "minor": 38
1385 },
1386 "arch": "x86_64"
1387 },
1388 "manylinux_compatible": false,
1389 "standalone": false,
1390 "markers": {
1391 "implementation_name": "cpython",
1392 "implementation_version": "3.12.0",
1393 "os_name": "posix",
1394 "platform_machine": "x86_64",
1395 "platform_python_implementation": "CPython",
1396 "platform_release": "6.5.0-13-generic",
1397 "platform_system": "Linux",
1398 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1399 "python_full_version": "3.12.0",
1400 "python_version": "3.12",
1401 "sys_platform": "linux"
1402 },
1403 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1404 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1405 "sys_prefix": "/home/ferris/projects/uv/.venv",
1406 "sys_executable": "{sys_executable}",
1407 "sys_path": [
1408 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1409 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1410 ],
1411 "site_packages": [
1412 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1413 ],
1414 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1415 "extension_suffixes": [".cpython-312-x86_64-linux-gnu.so", ".abi3.so", ".so"],
1416 "scheme": {
1417 "data": "/home/ferris/.pyenv/versions/3.12.0",
1418 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1419 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1420 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1421 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1422 },
1423 "virtualenv": {
1424 "data": "",
1425 "include": "include",
1426 "platlib": "lib/python3.12/site-packages",
1427 "purelib": "lib/python3.12/site-packages",
1428 "scripts": "bin"
1429 },
1430 "pointer_size": "64",
1431 "gil_disabled": true,
1432 "debug_enabled": false
1433 }
1434 "##}
1435 }
1436
1437 #[tokio::test]
1438 async fn test_cache_invalidation() {
1439 let mock_dir = tempdir().unwrap();
1440 let mocked_interpreter = mock_dir.path().join("python");
1441 let query_log = mock_dir.path().join("queries");
1442 let json = mocked_interpreter_response().replace(
1443 "{sys_executable}",
1444 &mocked_interpreter.display().to_string(),
1445 );
1446
1447 let cache = Cache::temp().unwrap().init().await.unwrap();
1448
1449 fs::write(
1450 &mocked_interpreter,
1451 formatdoc! {r"
1452 #!/bin/sh
1453 echo queried >> '{}'
1454 echo '{json}'
1455 ", query_log.display()},
1456 )
1457 .unwrap();
1458
1459 fs::set_permissions(
1460 &mocked_interpreter,
1461 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1462 )
1463 .unwrap();
1464 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1465 assert_eq!(
1466 interpreter.markers.python_version().version,
1467 Version::from_str("3.12").unwrap()
1468 );
1469 assert!(cache.bucket(CacheBucket::Interpreter).is_dir());
1470 assert_eq!(fs::read_to_string(&query_log).unwrap(), "queried\n");
1471
1472 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1473 assert_eq!(
1474 interpreter.markers.python_version().version,
1475 Version::from_str("3.12").unwrap()
1476 );
1477 assert_eq!(fs::read_to_string(&query_log).unwrap(), "queried\n");
1478
1479 let timestamp = Timestamp::from_path(&mocked_interpreter).unwrap();
1480 fs::write(
1481 &mocked_interpreter,
1482 formatdoc! {r"
1483 #!/bin/sh
1484 echo queried >> '{}'
1485 echo '{}'
1486 ", query_log.display(), json.replace("3.12", "3.13")},
1487 )
1488 .unwrap();
1489 assert_ne!(
1490 Timestamp::from_path(&mocked_interpreter).unwrap(),
1491 timestamp
1492 );
1493 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1494 assert_eq!(
1495 interpreter.markers.python_version().version,
1496 Version::from_str("3.13").unwrap()
1497 );
1498 assert_eq!(
1499 fs::read_to_string(&query_log).unwrap(),
1500 "queried\nqueried\n"
1501 );
1502 }
1503
1504 #[tokio::test]
1505 async fn test_cache_eviction_with_unchanged_executable() -> Result<()> {
1506 let mock_dir = tempdir()?;
1507 let mocked_interpreter = mock_dir.path().join("python");
1508 let response_file = mock_dir.path().join("response.json");
1509 let query_count = mock_dir.path().join("queries");
1510
1511 let mut response = serde_json::from_str::<Value>(mocked_interpreter_response())?;
1512 response["sys_executable"] = serde_json::to_value(&mocked_interpreter)?;
1513 fs::write(&response_file, serde_json::to_vec(&response)?)?;
1514 fs::write(
1515 &mocked_interpreter,
1516 formatdoc! {r#"
1517 #!/bin/sh
1518 printf '.' >> "{}"
1519 cat "{}"
1520 "#, query_count.display(), response_file.display()},
1521 )?;
1522 fs::set_permissions(
1523 &mocked_interpreter,
1524 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1525 )?;
1526
1527 let cache = Cache::temp()?.init().await?;
1528 let original_version = Version::from_str("3.12.0")?;
1529 let updated_version = Version::from_str("3.12.13")?;
1530
1531 assert_eq!(
1532 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1533 &original_version
1534 );
1535
1536 response["markers"]["implementation_version"] = "3.12.13".into();
1537 response["markers"]["python_full_version"] = "3.12.13".into();
1538 fs::write(&response_file, serde_json::to_vec(&response)?)?;
1539
1540 assert_eq!(
1541 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1542 &original_version,
1543 "an unchanged executable should retain its cached interpreter metadata"
1544 );
1545
1546 Interpreter::clear_cache(&mocked_interpreter, &cache)?;
1547 assert_eq!(
1548 fs::read_to_string(&query_count)?,
1549 ".",
1550 "clearing cached metadata should not query the interpreter"
1551 );
1552 assert_eq!(
1553 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1554 &updated_version,
1555 "clearing the cache should force the next query to run the interpreter"
1556 );
1557 assert_eq!(
1558 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1559 &updated_version,
1560 "the next query should persist the updated interpreter metadata"
1561 );
1562 assert_eq!(
1563 fs::read_to_string(&query_count)?,
1564 "..",
1565 "the updated interpreter metadata should be cached again"
1566 );
1567
1568 Ok(())
1569 }
1570}