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_prefix: PathBuf,
53 sys_base_executable: Option<PathBuf>,
54 sys_executable: PathBuf,
55 site_packages: Vec<PathBuf>,
56 stdlib: PathBuf,
57 extension_suffixes: Vec<Box<str>>,
58 standalone: bool,
59 tags: OnceLock<Tags>,
60 target: Option<Target>,
61 prefix: Option<Prefix>,
62 pointer_size: PointerSize,
63 gil_disabled: bool,
64 real_executable: PathBuf,
65 debug_enabled: bool,
66}
67
68impl Interpreter {
69 pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
71 let info = InterpreterInfo::query_cached(executable.as_ref(), cache)?;
72
73 debug_assert!(
74 info.sys_executable.is_absolute(),
75 "`sys.executable` is not an absolute Python; Python installation is broken: {}",
76 info.sys_executable.display()
77 );
78
79 Ok(Self {
80 platform: info.platform,
81 markers: Box::new(info.markers),
82 scheme: info.scheme,
83 virtualenv: info.virtualenv,
84 manylinux_compatible: info.manylinux_compatible,
85 sys_prefix: info.sys_prefix,
86 pointer_size: info.pointer_size,
87 gil_disabled: info.gil_disabled,
88 debug_enabled: info.debug_enabled,
89 sys_base_prefix: info.sys_base_prefix,
90 sys_base_executable: info.sys_base_executable,
91 sys_executable: info.sys_executable,
92 site_packages: info.site_packages,
93 stdlib: info.stdlib,
94 extension_suffixes: info.extension_suffixes,
95 standalone: info.standalone,
96 tags: OnceLock::new(),
97 target: None,
98 prefix: None,
99 real_executable: executable.as_ref().to_path_buf(),
100 })
101 }
102
103 #[must_use]
105 pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
106 Self {
107 scheme: virtualenv.scheme,
108 sys_base_executable: Some(virtualenv.base_executable),
109 sys_executable: virtualenv.executable,
110 sys_prefix: virtualenv.root,
111 target: None,
112 prefix: None,
113 site_packages: vec![],
114 ..self
115 }
116 }
117
118 pub fn with_target(self, target: Target) -> io::Result<Self> {
120 target.init()?;
121 Ok(Self {
122 target: Some(target),
123 ..self
124 })
125 }
126
127 pub fn with_prefix(self, prefix: Prefix) -> io::Result<Self> {
129 prefix.init(self.virtualenv())?;
130 Ok(Self {
131 prefix: Some(prefix),
132 ..self
133 })
134 }
135
136 pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
146 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
147 let base_python = std::path::absolute(base_executable)?;
148 Ok(base_python)
149 }
150
151 pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
162 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
163 let base_python = match find_base_python(
173 base_executable,
174 self.python_major(),
175 self.python_minor(),
176 self.variant().executable_suffix(),
177 ) {
178 Ok(path) => path,
179 Err(err) => {
180 warn!("Failed to find base Python executable: {err}");
181 canonicalize_executable(base_executable)?
182 }
183 };
184 Ok(base_python)
185 }
186
187 #[inline]
189 pub fn platform(&self) -> &Platform {
190 &self.platform
191 }
192
193 #[inline]
195 pub const fn markers(&self) -> &MarkerEnvironment {
196 &self.markers
197 }
198
199 pub fn resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
201 ResolverMarkerEnvironment::from(self.markers().clone())
202 }
203
204 pub fn key(&self) -> PythonInstallationKey {
206 PythonInstallationKey::new(
207 LenientImplementationName::from(self.implementation_name()),
208 self.python_major(),
209 self.python_minor(),
210 self.python_patch(),
211 self.python_version().pre(),
212 uv_platform::Platform::new(self.os(), self.arch(), self.libc()),
213 self.variant(),
214 )
215 }
216
217 pub fn variant(&self) -> PythonVariant {
218 if self.gil_disabled() {
219 if self.debug_enabled() {
220 PythonVariant::FreethreadedDebug
221 } else {
222 PythonVariant::Freethreaded
223 }
224 } else if self.debug_enabled() {
225 PythonVariant::Debug
226 } else {
227 PythonVariant::default()
228 }
229 }
230
231 pub fn arch(&self) -> Arch {
233 Arch::from(&self.platform().arch())
234 }
235
236 pub fn libc(&self) -> Libc {
238 Libc::from(self.platform().os())
239 }
240
241 pub fn os(&self) -> Os {
243 Os::from(self.platform().os())
244 }
245
246 pub fn tags(&self) -> Result<&Tags, TagsError> {
248 if self.tags.get().is_none() {
249 let tags = Tags::from_env(
250 self.platform(),
251 self.python_tuple(),
252 self.implementation_name(),
253 self.implementation_tuple(),
254 TagsOptions {
255 manylinux_compatible: self.manylinux_compatible,
256 gil_disabled: self.gil_disabled,
257 debug_enabled: self.debug_enabled,
258 is_cross: false,
259 },
260 )?;
261 self.tags.set(tags).expect("tags should not be set");
262 }
263 Ok(self.tags.get().expect("tags should be set"))
264 }
265
266 pub fn is_virtualenv(&self) -> bool {
270 self.sys_prefix != self.sys_base_prefix
272 }
273
274 fn is_target(&self) -> bool {
276 self.target.is_some()
277 }
278
279 fn is_prefix(&self) -> bool {
281 self.prefix.is_some()
282 }
283
284 pub fn is_managed(&self) -> bool {
288 if let Ok(test_managed) =
289 std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED)
290 {
291 return test_managed.split_ascii_whitespace().any(|item| {
294 let version = <PythonVersion as std::str::FromStr>::from_str(item).expect(
295 "`UV_INTERNAL__TEST_PYTHON_MANAGED` items should be valid Python versions",
296 );
297 if version.patch().is_some() {
298 version.version() == self.python_version()
299 } else {
300 (version.major(), version.minor()) == self.python_tuple()
301 }
302 });
303 }
304
305 let Ok(installations) = ManagedPythonInstallations::from_settings(None) else {
306 return false;
307 };
308 let Ok(root) = installations.absolute_root() else {
309 return false;
310 };
311 let sys_base_prefix = dunce::canonicalize(&self.sys_base_prefix)
312 .unwrap_or_else(|_| self.sys_base_prefix.clone());
313 let root = dunce::canonicalize(&root).unwrap_or(root);
314
315 let Ok(suffix) = sys_base_prefix.strip_prefix(&root) else {
316 return false;
317 };
318
319 let Some(first_component) = suffix.components().next() else {
320 return false;
321 };
322
323 let Some(name) = first_component.as_os_str().to_str() else {
324 return false;
325 };
326
327 PythonInstallationKey::from_str(name).is_ok()
328 }
329
330 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
335 if self.is_virtualenv() {
337 return None;
338 }
339
340 if self.is_target() || self.is_prefix() {
342 return None;
343 }
344
345 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
346 return None;
347 };
348
349 let mut ini = Ini::new_cs();
350 ini.set_multiline(true);
351
352 let Ok(mut sections) = ini.read(contents) else {
353 return Some(ExternallyManaged::default());
356 };
357
358 let Some(section) = sections.get_mut("externally-managed") else {
359 return Some(ExternallyManaged::default());
362 };
363
364 let Some(error) = section.remove("Error") else {
365 return Some(ExternallyManaged::default());
368 };
369
370 Some(ExternallyManaged { error })
371 }
372
373 #[inline]
375 pub fn python_full_version(&self) -> &StringVersion {
376 self.markers.python_full_version()
377 }
378
379 #[inline]
381 pub fn python_version(&self) -> &Version {
382 &self.markers.python_full_version().version
383 }
384
385 #[inline]
387 pub fn python_minor_version(&self) -> Version {
388 Version::new(self.python_version().release().iter().take(2).copied())
389 }
390
391 #[inline]
393 pub(crate) fn python_patch_version(&self) -> Version {
394 Version::new(self.python_version().release().iter().take(3).copied())
395 }
396
397 pub fn python_major(&self) -> u8 {
399 let major = self.markers.python_full_version().version.release()[0];
400 u8::try_from(major).expect("invalid major version")
401 }
402
403 pub fn python_minor(&self) -> u8 {
405 let minor = self.markers.python_full_version().version.release()[1];
406 u8::try_from(minor).expect("invalid minor version")
407 }
408
409 pub(crate) fn python_patch(&self) -> u8 {
411 let minor = self.markers.python_full_version().version.release()[2];
412 u8::try_from(minor).expect("invalid patch version")
413 }
414
415 pub fn python_tuple(&self) -> (u8, u8) {
417 (self.python_major(), self.python_minor())
418 }
419
420 fn implementation_major(&self) -> u8 {
422 let major = self.markers.implementation_version().version.release()[0];
423 u8::try_from(major).expect("invalid major version")
424 }
425
426 fn implementation_minor(&self) -> u8 {
428 let minor = self.markers.implementation_version().version.release()[1];
429 u8::try_from(minor).expect("invalid minor version")
430 }
431
432 pub fn implementation_tuple(&self) -> (u8, u8) {
434 (self.implementation_major(), self.implementation_minor())
435 }
436
437 pub fn implementation_name(&self) -> &str {
439 self.markers.implementation_name()
440 }
441
442 pub fn sys_base_prefix(&self) -> &Path {
444 &self.sys_base_prefix
445 }
446
447 pub fn sys_prefix(&self) -> &Path {
449 &self.sys_prefix
450 }
451
452 pub(crate) fn sys_base_executable(&self) -> Option<&Path> {
455 self.sys_base_executable.as_deref()
456 }
457
458 pub fn sys_executable(&self) -> &Path {
460 &self.sys_executable
461 }
462
463 pub fn extension_suffixes(&self) -> &[Box<str>] {
465 &self.extension_suffixes
466 }
467
468 pub fn real_executable(&self) -> &Path {
470 &self.real_executable
471 }
472
473 pub fn runtime_site_packages(&self) -> &[PathBuf] {
481 &self.site_packages
482 }
483
484 pub fn stdlib(&self) -> &Path {
486 &self.stdlib
487 }
488
489 pub fn purelib(&self) -> &Path {
491 &self.scheme.purelib
492 }
493
494 pub fn platlib(&self) -> &Path {
496 &self.scheme.platlib
497 }
498
499 pub fn scripts(&self) -> &Path {
501 &self.scheme.scripts
502 }
503
504 pub fn data(&self) -> &Path {
506 &self.scheme.data
507 }
508
509 pub fn include(&self) -> &Path {
511 &self.scheme.include
512 }
513
514 pub fn virtualenv(&self) -> &Scheme {
516 &self.virtualenv
517 }
518
519 pub fn manylinux_compatible(&self) -> bool {
521 self.manylinux_compatible
522 }
523
524 pub fn pointer_size(&self) -> PointerSize {
526 self.pointer_size
527 }
528
529 pub fn gil_disabled(&self) -> bool {
535 self.gil_disabled
536 }
537
538 pub fn debug_enabled(&self) -> bool {
541 self.debug_enabled
542 }
543
544 pub fn target(&self) -> Option<&Target> {
546 self.target.as_ref()
547 }
548
549 pub fn prefix(&self) -> Option<&Prefix> {
551 self.prefix.as_ref()
552 }
553
554 #[cfg(unix)]
563 pub fn is_standalone(&self) -> bool {
564 self.standalone
565 }
566
567 #[cfg(windows)]
571 pub fn is_standalone(&self) -> bool {
572 self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
573 }
574
575 pub fn layout(&self) -> Layout {
577 Layout {
578 python_version: self.python_tuple(),
579 sys_executable: self.sys_executable().to_path_buf(),
580 os_name: self.markers.os_name().to_string(),
581 scheme: if let Some(target) = self.target.as_ref() {
582 target.scheme()
583 } else if let Some(prefix) = self.prefix.as_ref() {
584 prefix.scheme(&self.virtualenv)
585 } else {
586 Scheme {
587 purelib: self.purelib().to_path_buf(),
588 platlib: self.platlib().to_path_buf(),
589 scripts: self.scripts().to_path_buf(),
590 data: self.data().to_path_buf(),
591 include: if self.is_virtualenv() {
592 self.sys_prefix.join("include").join("site").join(format!(
595 "python{}.{}",
596 self.python_major(),
597 self.python_minor()
598 ))
599 } else {
600 self.include().to_path_buf()
601 },
602 }
603 },
604 }
605 }
606
607 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
618 let target = self.target().map(Target::site_packages);
619
620 let prefix = self
621 .prefix()
622 .map(|prefix| prefix.site_packages(self.virtualenv()));
623
624 let interpreter = if target.is_none() && prefix.is_none() {
625 let purelib = self.purelib();
626 let platlib = self.platlib();
627 Some(std::iter::once(purelib).chain(
628 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
629 None
630 } else {
631 Some(platlib)
632 },
633 ))
634 } else {
635 None
636 };
637
638 target
639 .into_iter()
640 .flatten()
641 .map(Cow::Borrowed)
642 .chain(prefix.into_iter().flatten().map(Cow::Owned))
643 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
644 }
645
646 pub fn satisfies(&self, version: &PythonVersion) -> bool {
651 if version.patch().is_some() {
652 version.version() == self.python_version()
653 } else {
654 (version.major(), version.minor()) == self.python_tuple()
655 }
656 }
657
658 pub(crate) fn has_default_executable_name(&self) -> bool {
661 let Some(file_name) = self.sys_executable().file_name() else {
662 return false;
663 };
664 let Some(name) = file_name.to_str() else {
665 return false;
666 };
667 VersionRequest::Default
668 .executable_names(None)
669 .into_iter()
670 .any(|default_name| name == default_name.to_string())
671 }
672
673 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
675 if let Some(target) = self.target() {
676 LockedFile::acquire(
678 target.root().join(".lock"),
679 LockedFileMode::Exclusive,
680 target.root().user_display(),
681 )
682 .await
683 } else if let Some(prefix) = self.prefix() {
684 LockedFile::acquire(
686 prefix.root().join(".lock"),
687 LockedFileMode::Exclusive,
688 prefix.root().user_display(),
689 )
690 .await
691 } else if self.is_virtualenv() {
692 LockedFile::acquire(
694 self.sys_prefix.join(".lock"),
695 LockedFileMode::Exclusive,
696 self.sys_prefix.user_display(),
697 )
698 .await
699 } else {
700 LockedFile::acquire(
702 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
703 LockedFileMode::Exclusive,
704 self.sys_prefix.user_display(),
705 )
706 .await
707 }
708 }
709}
710
711pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
714 let path = path.as_ref();
715 debug_assert!(
716 path.is_absolute(),
717 "path must be absolute: {}",
718 path.display()
719 );
720
721 #[cfg(windows)]
722 {
723 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
724 Ok(dunce::canonicalize(launcher.python_path)?)
725 } else {
726 Ok(path.to_path_buf())
727 }
728 }
729
730 #[cfg(unix)]
731 fs_err::canonicalize(path)
732}
733
734#[derive(Debug, Default, Clone)]
738pub struct ExternallyManaged {
739 error: Option<String>,
740}
741
742impl ExternallyManaged {
743 pub fn into_error(self) -> Option<String> {
745 self.error
746 }
747}
748
749#[derive(Debug, Error)]
750pub struct UnexpectedResponseError {
751 #[source]
752 pub(super) err: serde_json::Error,
753 pub(super) stdout: String,
754 pub(super) stderr: String,
755 pub(super) path: PathBuf,
756}
757
758impl Display for UnexpectedResponseError {
759 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
760 write!(
761 f,
762 "Querying Python at `{}` returned an invalid response: {}",
763 self.path.display(),
764 self.err
765 )?;
766
767 let mut non_empty = false;
768
769 if !self.stdout.trim().is_empty() {
770 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
771 non_empty = true;
772 }
773
774 if !self.stderr.trim().is_empty() {
775 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
776 non_empty = true;
777 }
778
779 if non_empty {
780 writeln!(f)?;
781 }
782
783 Ok(())
784 }
785}
786
787#[derive(Debug, Error)]
788pub struct StatusCodeError {
789 pub(super) code: ExitStatus,
790 pub(super) stdout: String,
791 pub(super) stderr: String,
792 pub(super) path: PathBuf,
793}
794
795impl Display for StatusCodeError {
796 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
797 write!(
798 f,
799 "Querying Python at `{}` failed with exit status {}",
800 self.path.display(),
801 self.code
802 )?;
803
804 let mut non_empty = false;
805
806 if !self.stdout.trim().is_empty() {
807 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
808 non_empty = true;
809 }
810
811 if !self.stderr.trim().is_empty() {
812 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
813 non_empty = true;
814 }
815
816 if non_empty {
817 writeln!(f)?;
818 }
819
820 Ok(())
821 }
822}
823
824#[derive(Debug, Error)]
825pub enum Error {
826 #[error("Failed to query Python interpreter")]
827 Io(#[from] io::Error),
828 #[error(transparent)]
829 BrokenLink(BrokenLink),
830 #[error("Python interpreter not found at `{0}`")]
831 NotFound(PathBuf),
832 #[error("Failed to query Python interpreter at `{path}`")]
833 SpawnFailed {
834 path: PathBuf,
835 #[source]
836 err: io::Error,
837 },
838 #[cfg(windows)]
839 #[error("Failed to query Python interpreter at `{path}`")]
840 CorruptWindowsPackage {
841 path: PathBuf,
842 #[source]
843 err: io::Error,
844 },
845 #[error("Failed to query Python interpreter at `{path}`")]
846 PermissionDenied {
847 path: PathBuf,
848 #[source]
849 err: io::Error,
850 },
851 #[error("{0}")]
852 UnexpectedResponse(UnexpectedResponseError),
853 #[error("{0}")]
854 StatusCode(StatusCodeError),
855 #[error("Can't use Python at `{path}`")]
856 QueryScript {
857 #[source]
858 err: InterpreterInfoError,
859 path: PathBuf,
860 },
861 #[error("Failed to write to cache")]
862 Encode(#[from] rmp_serde::encode::Error),
863}
864
865impl uv_errors::Hint for Error {
866 fn hints(&self) -> uv_errors::Hints<'_> {
867 match self {
868 Self::BrokenLink(err) => err.hints(),
869 _ => uv_errors::Hints::none(),
870 }
871 }
872}
873
874#[derive(Debug, Error)]
875pub struct BrokenLink {
876 pub path: PathBuf,
877 pub unix: bool,
880 pub venv: bool,
882}
883
884impl Display for BrokenLink {
885 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
886 if self.unix {
887 write!(
888 f,
889 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
890 self.path.user_display()
891 )
892 } else {
893 write!(
894 f,
895 "Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
896 self.path.user_display()
897 )
898 }
899 }
900}
901
902impl uv_errors::Hint for BrokenLink {
903 fn hints(&self) -> uv_errors::Hints<'_> {
904 if self.venv {
905 uv_errors::Hints::from(format!(
906 "Consider recreating the environment (e.g., with `{}`)",
907 "uv venv".green()
908 ))
909 } else {
910 uv_errors::Hints::none()
911 }
912 }
913}
914
915#[derive(Debug, Deserialize, Serialize)]
916#[serde(tag = "result", rename_all = "lowercase")]
917enum InterpreterInfoResult {
918 Error(InterpreterInfoError),
919 Success(Box<InterpreterInfo>),
920}
921
922#[derive(Debug, Error, Deserialize, Serialize)]
923#[serde(tag = "kind", rename_all = "snake_case")]
924pub enum InterpreterInfoError {
925 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
926 LibcNotFound,
927 #[error(
928 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
929 )]
930 BrokenMacVer,
931 #[error("Unknown operating system: `{operating_system}`")]
932 UnknownOperatingSystem { operating_system: String },
933 #[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
934 UnsupportedPythonVersion { python_version: String },
935 #[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
936 UnsupportedPython,
937 #[error(
938 "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`."
939 )]
940 MissingRequiredDistutils {
941 python_major: usize,
942 python_minor: usize,
943 },
944 #[error("Only Pyodide is support for Emscripten Python")]
945 EmscriptenNotPyodide,
946}
947
948#[expect(clippy::struct_excessive_bools)]
949#[derive(Debug, Deserialize, Serialize, Clone)]
950struct InterpreterInfo {
951 platform: Platform,
952 markers: MarkerEnvironment,
953 scheme: Scheme,
954 virtualenv: Scheme,
955 manylinux_compatible: bool,
956 sys_prefix: PathBuf,
957 sys_base_exec_prefix: PathBuf,
958 sys_base_prefix: PathBuf,
959 sys_base_executable: Option<PathBuf>,
960 sys_executable: PathBuf,
961 sys_path: Vec<PathBuf>,
962 site_packages: Vec<PathBuf>,
963 stdlib: PathBuf,
964 extension_suffixes: Vec<Box<str>>,
965 standalone: bool,
966 pointer_size: PointerSize,
967 gil_disabled: bool,
968 debug_enabled: bool,
969}
970
971impl InterpreterInfo {
972 pub(crate) fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
974 let tempdir = tempfile::tempdir_in(cache.root())?;
975 Self::setup_python_query_files(tempdir.path())?;
976
977 let script = format!(
981 r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
982 tempdir.path().escape_for_python()
983 );
984 let mut command = Command::new(interpreter);
985 command
986 .arg("-I") .arg("-B") .arg("-c")
989 .arg(script);
990
991 #[cfg(target_os = "macos")]
1000 command.env("SYSTEM_VERSION_COMPAT", "0");
1001
1002 let output = command.output().map_err(|err| {
1003 match err.kind() {
1004 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
1005 io::ErrorKind::PermissionDenied => {
1006 return Error::PermissionDenied {
1007 path: interpreter.to_path_buf(),
1008 err,
1009 };
1010 }
1011 _ => {}
1012 }
1013 #[cfg(windows)]
1014 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
1015 .raw_os_error()
1016 .and_then(|code| u32::try_from(code).ok())
1017 .map(WIN32_ERROR)
1018 {
1019 return Error::CorruptWindowsPackage {
1022 path: interpreter.to_path_buf(),
1023 err,
1024 };
1025 }
1026 Error::SpawnFailed {
1027 path: interpreter.to_path_buf(),
1028 err,
1029 }
1030 })?;
1031
1032 if !output.status.success() {
1033 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1034
1035 if python_home(interpreter).is_some_and(|home| !home.exists()) {
1041 return Err(Error::BrokenLink(BrokenLink {
1042 path: interpreter.to_path_buf(),
1043 unix: false,
1044 venv: uv_fs::is_virtualenv_executable(interpreter),
1045 }));
1046 }
1047
1048 if stderr.contains("Unknown option: -I") {
1050 return Err(Error::QueryScript {
1051 err: InterpreterInfoError::UnsupportedPython,
1052 path: interpreter.to_path_buf(),
1053 });
1054 }
1055
1056 return Err(Error::StatusCode(StatusCodeError {
1057 code: output.status,
1058 stderr,
1059 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1060 path: interpreter.to_path_buf(),
1061 }));
1062 }
1063
1064 let result: InterpreterInfoResult =
1065 serde_json::from_slice(&output.stdout).map_err(|err| {
1066 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1067
1068 if stderr.contains("Unknown option: -I") {
1070 Error::QueryScript {
1071 err: InterpreterInfoError::UnsupportedPython,
1072 path: interpreter.to_path_buf(),
1073 }
1074 } else {
1075 Error::UnexpectedResponse(UnexpectedResponseError {
1076 err,
1077 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1078 stderr,
1079 path: interpreter.to_path_buf(),
1080 })
1081 }
1082 })?;
1083
1084 match result {
1085 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1086 err,
1087 path: interpreter.to_path_buf(),
1088 }),
1089 InterpreterInfoResult::Success(data) => Ok(*data),
1090 }
1091 }
1092
1093 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1096 let python_dir = root.join("python");
1097 fs_err::create_dir(&python_dir)?;
1098 fs_err::write(
1099 python_dir.join("get_interpreter_info.py"),
1100 include_str!("../python/get_interpreter_info.py"),
1101 )?;
1102 fs_err::write(
1103 python_dir.join("__init__.py"),
1104 include_str!("../python/__init__.py"),
1105 )?;
1106 let packaging_dir = python_dir.join("packaging");
1107 fs_err::create_dir(&packaging_dir)?;
1108 fs_err::write(
1109 packaging_dir.join("__init__.py"),
1110 include_str!("../python/packaging/__init__.py"),
1111 )?;
1112 fs_err::write(
1113 packaging_dir.join("_elffile.py"),
1114 include_str!("../python/packaging/_elffile.py"),
1115 )?;
1116 fs_err::write(
1117 packaging_dir.join("_manylinux.py"),
1118 include_str!("../python/packaging/_manylinux.py"),
1119 )?;
1120 fs_err::write(
1121 packaging_dir.join("_musllinux.py"),
1122 include_str!("../python/packaging/_musllinux.py"),
1123 )?;
1124 Ok(())
1125 }
1126
1127 fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1133 let absolute = std::path::absolute(executable)?;
1134
1135 let handle_io_error = |err: io::Error| -> Error {
1139 if err.kind() == io::ErrorKind::NotFound {
1140 if absolute
1143 .symlink_metadata()
1144 .is_ok_and(|metadata| metadata.is_symlink())
1145 {
1146 Error::BrokenLink(BrokenLink {
1147 path: executable.to_path_buf(),
1148 unix: true,
1149 venv: uv_fs::is_virtualenv_executable(executable),
1150 })
1151 } else {
1152 Error::NotFound(executable.to_path_buf())
1153 }
1154 } else {
1155 err.into()
1156 }
1157 };
1158
1159 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1160
1161 let cache_entry = cache.entry(
1162 CacheBucket::Interpreter,
1163 cache_digest(&(
1166 ARCH,
1167 uv_platform::OsType::from_env()
1168 .map(|os_type| os_type.to_string())
1169 .unwrap_or_default(),
1170 uv_platform::OsRelease::from_env()
1171 .map(|os_release| os_release.to_string())
1172 .unwrap_or_default(),
1173 )),
1174 format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
1182 );
1183
1184 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1187
1188 if cache
1190 .freshness(&cache_entry, None, None)
1191 .is_ok_and(Freshness::is_fresh)
1192 {
1193 if let Ok(data) = fs::read(cache_entry.path()) {
1194 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1195 Ok(cached) => {
1196 if cached.timestamp == modified {
1197 trace!(
1198 "Found cached interpreter info for Python {}, skipping query of: {}",
1199 cached.data.markers.python_full_version(),
1200 executable.user_display()
1201 );
1202 return Ok(cached.data);
1203 }
1204
1205 trace!(
1206 "Ignoring stale interpreter markers for: {}",
1207 executable.user_display()
1208 );
1209 }
1210 Err(err) => {
1211 warn!(
1212 "Broken interpreter cache entry at {}, removing: {err}",
1213 cache_entry.path().user_display()
1214 );
1215 let _ = fs_err::remove_file(cache_entry.path());
1216 }
1217 }
1218 }
1219 }
1220
1221 trace!(
1223 "Querying interpreter executable at {}",
1224 executable.display()
1225 );
1226 let info = Self::query(executable, cache)?;
1227
1228 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1231 fs::create_dir_all(cache_entry.dir())?;
1232 write_atomic_sync(
1233 cache_entry.path(),
1234 rmp_serde::to_vec(&CachedByTimestamp {
1235 timestamp: modified,
1236 data: info.clone(),
1237 })?,
1238 )?;
1239 }
1240
1241 Ok(info)
1242 }
1243}
1244
1245fn find_base_python(
1271 executable: &Path,
1272 major: u8,
1273 minor: u8,
1274 suffix: &str,
1275) -> Result<PathBuf, io::Error> {
1276 fn is_root(path: &Path) -> bool {
1278 let mut components = path.components();
1279 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1280 }
1281
1282 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1286 if cfg!(windows) {
1287 dir.join("Lib").join("os.py").is_file()
1288 } else {
1289 dir.join("lib")
1290 .join(format!("python{major}.{minor}{suffix}"))
1291 .join("os.py")
1292 .is_file()
1293 }
1294 }
1295
1296 let mut executable = Cow::Borrowed(executable);
1297
1298 loop {
1299 debug!(
1300 "Assessing Python executable as base candidate: {}",
1301 executable.display()
1302 );
1303
1304 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1306 if is_prefix(prefix, major, minor, suffix) {
1307 return Ok(executable.into_owned());
1308 }
1309 }
1310
1311 let resolved = fs_err::read_link(&executable)?;
1313
1314 let resolved = if resolved.is_relative() {
1316 if let Some(parent) = executable.parent() {
1317 parent.join(resolved)
1318 } else {
1319 return Err(io::Error::other("Symlink has no parent directory"));
1320 }
1321 } else {
1322 resolved
1323 };
1324
1325 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1327
1328 executable = Cow::Owned(resolved);
1329 }
1330}
1331
1332fn python_home(interpreter: &Path) -> Option<PathBuf> {
1334 let venv_root = interpreter.parent()?.parent()?;
1335 let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
1336 pyvenv_cfg.home
1337}
1338
1339#[cfg(unix)]
1340#[cfg(test)]
1341mod tests {
1342 use std::str::FromStr;
1343
1344 use fs_err as fs;
1345 use indoc::{formatdoc, indoc};
1346 use tempfile::tempdir;
1347
1348 use uv_cache::Cache;
1349 use uv_pep440::Version;
1350
1351 use crate::Interpreter;
1352
1353 #[tokio::test]
1354 async fn test_cache_invalidation() {
1355 let mock_dir = tempdir().unwrap();
1356 let mocked_interpreter = mock_dir.path().join("python");
1357 let json = indoc! {r##"
1358 {
1359 "result": "success",
1360 "platform": {
1361 "os": {
1362 "name": "manylinux",
1363 "major": 2,
1364 "minor": 38
1365 },
1366 "arch": "x86_64"
1367 },
1368 "manylinux_compatible": false,
1369 "standalone": false,
1370 "markers": {
1371 "implementation_name": "cpython",
1372 "implementation_version": "3.12.0",
1373 "os_name": "posix",
1374 "platform_machine": "x86_64",
1375 "platform_python_implementation": "CPython",
1376 "platform_release": "6.5.0-13-generic",
1377 "platform_system": "Linux",
1378 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1379 "python_full_version": "3.12.0",
1380 "python_version": "3.12",
1381 "sys_platform": "linux"
1382 },
1383 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1384 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1385 "sys_prefix": "/home/ferris/projects/uv/.venv",
1386 "sys_executable": "/home/ferris/projects/uv/.venv/bin/python",
1387 "sys_path": [
1388 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1389 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1390 ],
1391 "site_packages": [
1392 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1393 ],
1394 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1395 "extension_suffixes": [".cpython-312-x86_64-linux-gnu.so", ".abi3.so", ".so"],
1396 "scheme": {
1397 "data": "/home/ferris/.pyenv/versions/3.12.0",
1398 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1399 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1400 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1401 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1402 },
1403 "virtualenv": {
1404 "data": "",
1405 "include": "include",
1406 "platlib": "lib/python3.12/site-packages",
1407 "purelib": "lib/python3.12/site-packages",
1408 "scripts": "bin"
1409 },
1410 "pointer_size": "64",
1411 "gil_disabled": true,
1412 "debug_enabled": false
1413 }
1414 "##};
1415
1416 let cache = Cache::temp().unwrap().init().await.unwrap();
1417
1418 fs::write(
1419 &mocked_interpreter,
1420 formatdoc! {r"
1421 #!/bin/sh
1422 echo '{json}'
1423 "},
1424 )
1425 .unwrap();
1426
1427 fs::set_permissions(
1428 &mocked_interpreter,
1429 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1430 )
1431 .unwrap();
1432 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1433 assert_eq!(
1434 interpreter.markers.python_version().version,
1435 Version::from_str("3.12").unwrap()
1436 );
1437 fs::write(
1438 &mocked_interpreter,
1439 formatdoc! {r"
1440 #!/bin/sh
1441 echo '{}'
1442 ", json.replace("3.12", "3.13")},
1443 )
1444 .unwrap();
1445 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1446 assert_eq!(
1447 interpreter.markers.python_version().version,
1448 Version::from_str("3.13").unwrap()
1449 );
1450 }
1451}