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::{LockedFile, PythonExt, Simplified, write_atomic_sync};
22use uv_install_wheel::Layout;
23use uv_pep440::Version;
24use uv_pep508::{MarkerEnvironment, StringVersion};
25use uv_platform::{Arch, Libc, Os};
26use uv_platform_tags::{Platform, Tags, TagsError};
27use uv_pypi_types::{ResolverMarkerEnvironment, Scheme};
28
29use crate::implementation::LenientImplementationName;
30use crate::managed::ManagedPythonInstallations;
31use crate::pointer_size::PointerSize;
32use crate::{
33 Prefix, PythonInstallationKey, PythonVariant, PythonVersion, Target, VersionRequest,
34 VirtualEnvironment,
35};
36
37#[cfg(windows)]
38use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
39
40#[allow(clippy::struct_excessive_bools)]
42#[derive(Debug, Clone)]
43pub struct Interpreter {
44 platform: Platform,
45 markers: Box<MarkerEnvironment>,
46 scheme: Scheme,
47 virtualenv: Scheme,
48 manylinux_compatible: bool,
49 sys_prefix: PathBuf,
50 sys_base_exec_prefix: PathBuf,
51 sys_base_prefix: PathBuf,
52 sys_base_executable: Option<PathBuf>,
53 sys_executable: PathBuf,
54 sys_path: Vec<PathBuf>,
55 site_packages: Vec<PathBuf>,
56 stdlib: PathBuf,
57 standalone: bool,
58 tags: OnceLock<Tags>,
59 target: Option<Target>,
60 prefix: Option<Prefix>,
61 pointer_size: PointerSize,
62 gil_disabled: bool,
63 real_executable: PathBuf,
64 debug_enabled: bool,
65}
66
67impl Interpreter {
68 pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
70 let info = InterpreterInfo::query_cached(executable.as_ref(), cache)?;
71
72 debug_assert!(
73 info.sys_executable.is_absolute(),
74 "`sys.executable` is not an absolute Python; Python installation is broken: {}",
75 info.sys_executable.display()
76 );
77
78 Ok(Self {
79 platform: info.platform,
80 markers: Box::new(info.markers),
81 scheme: info.scheme,
82 virtualenv: info.virtualenv,
83 manylinux_compatible: info.manylinux_compatible,
84 sys_prefix: info.sys_prefix,
85 sys_base_exec_prefix: info.sys_base_exec_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 sys_path: info.sys_path,
93 site_packages: info.site_packages,
94 stdlib: info.stdlib,
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 self.manylinux_compatible,
255 self.gil_disabled,
256 )?;
257 self.tags.set(tags).expect("tags should not be set");
258 }
259 Ok(self.tags.get().expect("tags should be set"))
260 }
261
262 pub fn is_virtualenv(&self) -> bool {
266 self.sys_prefix != self.sys_base_prefix
268 }
269
270 pub fn is_target(&self) -> bool {
272 self.target.is_some()
273 }
274
275 pub fn is_prefix(&self) -> bool {
277 self.prefix.is_some()
278 }
279
280 pub fn is_managed(&self) -> bool {
284 if let Ok(test_managed) =
285 std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED)
286 {
287 return test_managed.split_ascii_whitespace().any(|item| {
290 let version = <PythonVersion as std::str::FromStr>::from_str(item).expect(
291 "`UV_INTERNAL__TEST_PYTHON_MANAGED` items should be valid Python versions",
292 );
293 if version.patch().is_some() {
294 version.version() == self.python_version()
295 } else {
296 (version.major(), version.minor()) == self.python_tuple()
297 }
298 });
299 }
300
301 let Ok(installations) = ManagedPythonInstallations::from_settings(None) else {
302 return false;
303 };
304
305 let Ok(suffix) = self.sys_base_prefix.strip_prefix(installations.root()) else {
306 return false;
307 };
308
309 let Some(first_component) = suffix.components().next() else {
310 return false;
311 };
312
313 let Some(name) = first_component.as_os_str().to_str() else {
314 return false;
315 };
316
317 PythonInstallationKey::from_str(name).is_ok()
318 }
319
320 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
325 if self.is_virtualenv() {
327 return None;
328 }
329
330 if self.is_target() || self.is_prefix() {
332 return None;
333 }
334
335 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
336 return None;
337 };
338
339 let mut ini = Ini::new_cs();
340 ini.set_multiline(true);
341
342 let Ok(mut sections) = ini.read(contents) else {
343 return Some(ExternallyManaged::default());
346 };
347
348 let Some(section) = sections.get_mut("externally-managed") else {
349 return Some(ExternallyManaged::default());
352 };
353
354 let Some(error) = section.remove("Error") else {
355 return Some(ExternallyManaged::default());
358 };
359
360 Some(ExternallyManaged { error })
361 }
362
363 #[inline]
365 pub fn python_full_version(&self) -> &StringVersion {
366 self.markers.python_full_version()
367 }
368
369 #[inline]
371 pub fn python_version(&self) -> &Version {
372 &self.markers.python_full_version().version
373 }
374
375 #[inline]
377 pub fn python_minor_version(&self) -> Version {
378 Version::new(self.python_version().release().iter().take(2).copied())
379 }
380
381 #[inline]
383 pub fn python_patch_version(&self) -> Version {
384 Version::new(self.python_version().release().iter().take(3).copied())
385 }
386
387 pub fn python_major(&self) -> u8 {
389 let major = self.markers.python_full_version().version.release()[0];
390 u8::try_from(major).expect("invalid major version")
391 }
392
393 pub fn python_minor(&self) -> u8 {
395 let minor = self.markers.python_full_version().version.release()[1];
396 u8::try_from(minor).expect("invalid minor version")
397 }
398
399 pub fn python_patch(&self) -> u8 {
401 let minor = self.markers.python_full_version().version.release()[2];
402 u8::try_from(minor).expect("invalid patch version")
403 }
404
405 pub fn python_tuple(&self) -> (u8, u8) {
407 (self.python_major(), self.python_minor())
408 }
409
410 pub fn implementation_major(&self) -> u8 {
412 let major = self.markers.implementation_version().version.release()[0];
413 u8::try_from(major).expect("invalid major version")
414 }
415
416 pub fn implementation_minor(&self) -> u8 {
418 let minor = self.markers.implementation_version().version.release()[1];
419 u8::try_from(minor).expect("invalid minor version")
420 }
421
422 pub fn implementation_tuple(&self) -> (u8, u8) {
424 (self.implementation_major(), self.implementation_minor())
425 }
426
427 pub fn implementation_name(&self) -> &str {
429 self.markers.implementation_name()
430 }
431
432 pub fn sys_base_exec_prefix(&self) -> &Path {
434 &self.sys_base_exec_prefix
435 }
436
437 pub fn sys_base_prefix(&self) -> &Path {
439 &self.sys_base_prefix
440 }
441
442 pub fn sys_prefix(&self) -> &Path {
444 &self.sys_prefix
445 }
446
447 pub fn sys_base_executable(&self) -> Option<&Path> {
450 self.sys_base_executable.as_deref()
451 }
452
453 pub fn sys_executable(&self) -> &Path {
455 &self.sys_executable
456 }
457
458 pub fn real_executable(&self) -> &Path {
460 &self.real_executable
461 }
462
463 pub fn sys_path(&self) -> &[PathBuf] {
465 &self.sys_path
466 }
467
468 pub fn runtime_site_packages(&self) -> &[PathBuf] {
476 &self.site_packages
477 }
478
479 pub fn stdlib(&self) -> &Path {
481 &self.stdlib
482 }
483
484 pub fn purelib(&self) -> &Path {
486 &self.scheme.purelib
487 }
488
489 pub fn platlib(&self) -> &Path {
491 &self.scheme.platlib
492 }
493
494 pub fn scripts(&self) -> &Path {
496 &self.scheme.scripts
497 }
498
499 pub fn data(&self) -> &Path {
501 &self.scheme.data
502 }
503
504 pub fn include(&self) -> &Path {
506 &self.scheme.include
507 }
508
509 pub fn virtualenv(&self) -> &Scheme {
511 &self.virtualenv
512 }
513
514 pub fn manylinux_compatible(&self) -> bool {
516 self.manylinux_compatible
517 }
518
519 pub fn pointer_size(&self) -> PointerSize {
521 self.pointer_size
522 }
523
524 pub fn gil_disabled(&self) -> bool {
530 self.gil_disabled
531 }
532
533 pub fn debug_enabled(&self) -> bool {
536 self.debug_enabled
537 }
538
539 pub fn target(&self) -> Option<&Target> {
541 self.target.as_ref()
542 }
543
544 pub fn prefix(&self) -> Option<&Prefix> {
546 self.prefix.as_ref()
547 }
548
549 #[cfg(unix)]
558 pub fn is_standalone(&self) -> bool {
559 self.standalone
560 }
561
562 #[cfg(windows)]
566 pub fn is_standalone(&self) -> bool {
567 self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
568 }
569
570 pub fn layout(&self) -> Layout {
572 Layout {
573 python_version: self.python_tuple(),
574 sys_executable: self.sys_executable().to_path_buf(),
575 os_name: self.markers.os_name().to_string(),
576 scheme: if let Some(target) = self.target.as_ref() {
577 target.scheme()
578 } else if let Some(prefix) = self.prefix.as_ref() {
579 prefix.scheme(&self.virtualenv)
580 } else {
581 Scheme {
582 purelib: self.purelib().to_path_buf(),
583 platlib: self.platlib().to_path_buf(),
584 scripts: self.scripts().to_path_buf(),
585 data: self.data().to_path_buf(),
586 include: if self.is_virtualenv() {
587 self.sys_prefix.join("include").join("site").join(format!(
590 "python{}.{}",
591 self.python_major(),
592 self.python_minor()
593 ))
594 } else {
595 self.include().to_path_buf()
596 },
597 }
598 },
599 }
600 }
601
602 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
613 let target = self.target().map(Target::site_packages);
614
615 let prefix = self
616 .prefix()
617 .map(|prefix| prefix.site_packages(self.virtualenv()));
618
619 let interpreter = if target.is_none() && prefix.is_none() {
620 let purelib = self.purelib();
621 let platlib = self.platlib();
622 Some(std::iter::once(purelib).chain(
623 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
624 None
625 } else {
626 Some(platlib)
627 },
628 ))
629 } else {
630 None
631 };
632
633 target
634 .into_iter()
635 .flatten()
636 .map(Cow::Borrowed)
637 .chain(prefix.into_iter().flatten().map(Cow::Owned))
638 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
639 }
640
641 pub fn satisfies(&self, version: &PythonVersion) -> bool {
646 if version.patch().is_some() {
647 version.version() == self.python_version()
648 } else {
649 (version.major(), version.minor()) == self.python_tuple()
650 }
651 }
652
653 pub(crate) fn has_default_executable_name(&self) -> bool {
656 let Some(file_name) = self.sys_executable().file_name() else {
657 return false;
658 };
659 let Some(name) = file_name.to_str() else {
660 return false;
661 };
662 VersionRequest::Default
663 .executable_names(None)
664 .into_iter()
665 .any(|default_name| name == default_name.to_string())
666 }
667
668 pub async fn lock(&self) -> Result<LockedFile, io::Error> {
670 if let Some(target) = self.target() {
671 LockedFile::acquire(target.root().join(".lock"), target.root().user_display()).await
673 } else if let Some(prefix) = self.prefix() {
674 LockedFile::acquire(prefix.root().join(".lock"), prefix.root().user_display()).await
676 } else if self.is_virtualenv() {
677 LockedFile::acquire(
679 self.sys_prefix.join(".lock"),
680 self.sys_prefix.user_display(),
681 )
682 .await
683 } else {
684 LockedFile::acquire(
686 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
687 self.sys_prefix.user_display(),
688 )
689 .await
690 }
691 }
692}
693
694pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
697 let path = path.as_ref();
698 debug_assert!(
699 path.is_absolute(),
700 "path must be absolute: {}",
701 path.display()
702 );
703
704 #[cfg(windows)]
705 {
706 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
707 Ok(dunce::canonicalize(launcher.python_path)?)
708 } else {
709 Ok(path.to_path_buf())
710 }
711 }
712
713 #[cfg(unix)]
714 fs_err::canonicalize(path)
715}
716
717#[derive(Debug, Default, Clone)]
721pub struct ExternallyManaged {
722 error: Option<String>,
723}
724
725impl ExternallyManaged {
726 pub fn into_error(self) -> Option<String> {
728 self.error
729 }
730}
731
732#[derive(Debug, Error)]
733pub struct UnexpectedResponseError {
734 #[source]
735 pub(super) err: serde_json::Error,
736 pub(super) stdout: String,
737 pub(super) stderr: String,
738 pub(super) path: PathBuf,
739}
740
741impl Display for UnexpectedResponseError {
742 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
743 write!(
744 f,
745 "Querying Python at `{}` returned an invalid response: {}",
746 self.path.display(),
747 self.err
748 )?;
749
750 let mut non_empty = false;
751
752 if !self.stdout.trim().is_empty() {
753 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
754 non_empty = true;
755 }
756
757 if !self.stderr.trim().is_empty() {
758 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
759 non_empty = true;
760 }
761
762 if non_empty {
763 writeln!(f)?;
764 }
765
766 Ok(())
767 }
768}
769
770#[derive(Debug, Error)]
771pub struct StatusCodeError {
772 pub(super) code: ExitStatus,
773 pub(super) stdout: String,
774 pub(super) stderr: String,
775 pub(super) path: PathBuf,
776}
777
778impl Display for StatusCodeError {
779 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
780 write!(
781 f,
782 "Querying Python at `{}` failed with exit status {}",
783 self.path.display(),
784 self.code
785 )?;
786
787 let mut non_empty = false;
788
789 if !self.stdout.trim().is_empty() {
790 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
791 non_empty = true;
792 }
793
794 if !self.stderr.trim().is_empty() {
795 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
796 non_empty = true;
797 }
798
799 if non_empty {
800 writeln!(f)?;
801 }
802
803 Ok(())
804 }
805}
806
807#[derive(Debug, Error)]
808pub enum Error {
809 #[error("Failed to query Python interpreter")]
810 Io(#[from] io::Error),
811 #[error(transparent)]
812 BrokenSymlink(BrokenSymlink),
813 #[error("Python interpreter not found at `{0}`")]
814 NotFound(PathBuf),
815 #[error("Failed to query Python interpreter at `{path}`")]
816 SpawnFailed {
817 path: PathBuf,
818 #[source]
819 err: io::Error,
820 },
821 #[cfg(windows)]
822 #[error("Failed to query Python interpreter at `{path}`")]
823 CorruptWindowsPackage {
824 path: PathBuf,
825 #[source]
826 err: io::Error,
827 },
828 #[error("Failed to query Python interpreter at `{path}`")]
829 PermissionDenied {
830 path: PathBuf,
831 #[source]
832 err: io::Error,
833 },
834 #[error("{0}")]
835 UnexpectedResponse(UnexpectedResponseError),
836 #[error("{0}")]
837 StatusCode(StatusCodeError),
838 #[error("Can't use Python at `{path}`")]
839 QueryScript {
840 #[source]
841 err: InterpreterInfoError,
842 path: PathBuf,
843 },
844 #[error("Failed to write to cache")]
845 Encode(#[from] rmp_serde::encode::Error),
846}
847
848#[derive(Debug, Error)]
849pub struct BrokenSymlink {
850 pub path: PathBuf,
851 pub venv: bool,
853}
854
855impl Display for BrokenSymlink {
856 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
857 write!(
858 f,
859 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
860 self.path.user_display()
861 )?;
862 if self.venv {
863 write!(
864 f,
865 "\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
866 "hint".bold().cyan(),
867 ":".bold(),
868 "uv venv".green()
869 )?;
870 }
871 Ok(())
872 }
873}
874
875#[derive(Debug, Deserialize, Serialize)]
876#[serde(tag = "result", rename_all = "lowercase")]
877enum InterpreterInfoResult {
878 Error(InterpreterInfoError),
879 Success(Box<InterpreterInfo>),
880}
881
882#[derive(Debug, Error, Deserialize, Serialize)]
883#[serde(tag = "kind", rename_all = "snake_case")]
884pub enum InterpreterInfoError {
885 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
886 LibcNotFound,
887 #[error(
888 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
889 )]
890 BrokenMacVer,
891 #[error("Unknown operating system: `{operating_system}`")]
892 UnknownOperatingSystem { operating_system: String },
893 #[error("Python {python_version} is not supported. Please use Python 3.8 or newer.")]
894 UnsupportedPythonVersion { python_version: String },
895 #[error("Python executable does not support `-I` flag. Please use Python 3.8 or newer.")]
896 UnsupportedPython,
897 #[error(
898 "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`."
899 )]
900 MissingRequiredDistutils {
901 python_major: usize,
902 python_minor: usize,
903 },
904 #[error("Only Pyodide is support for Emscripten Python")]
905 EmscriptenNotPyodide,
906}
907
908#[allow(clippy::struct_excessive_bools)]
909#[derive(Debug, Deserialize, Serialize, Clone)]
910struct InterpreterInfo {
911 platform: Platform,
912 markers: MarkerEnvironment,
913 scheme: Scheme,
914 virtualenv: Scheme,
915 manylinux_compatible: bool,
916 sys_prefix: PathBuf,
917 sys_base_exec_prefix: PathBuf,
918 sys_base_prefix: PathBuf,
919 sys_base_executable: Option<PathBuf>,
920 sys_executable: PathBuf,
921 sys_path: Vec<PathBuf>,
922 site_packages: Vec<PathBuf>,
923 stdlib: PathBuf,
924 standalone: bool,
925 pointer_size: PointerSize,
926 gil_disabled: bool,
927 debug_enabled: bool,
928}
929
930impl InterpreterInfo {
931 pub(crate) fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
933 let tempdir = tempfile::tempdir_in(cache.root())?;
934 Self::setup_python_query_files(tempdir.path())?;
935
936 let script = format!(
940 r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
941 tempdir.path().escape_for_python()
942 );
943 let output = Command::new(interpreter)
944 .arg("-I") .arg("-B") .arg("-c")
947 .arg(script)
948 .output()
949 .map_err(|err| {
950 match err.kind() {
951 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
952 io::ErrorKind::PermissionDenied => {
953 return Error::PermissionDenied {
954 path: interpreter.to_path_buf(),
955 err,
956 };
957 }
958 _ => {}
959 }
960 #[cfg(windows)]
961 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
962 .raw_os_error()
963 .and_then(|code| u32::try_from(code).ok())
964 .map(WIN32_ERROR)
965 {
966 return Error::CorruptWindowsPackage {
969 path: interpreter.to_path_buf(),
970 err,
971 };
972 }
973 Error::SpawnFailed {
974 path: interpreter.to_path_buf(),
975 err,
976 }
977 })?;
978
979 if !output.status.success() {
980 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
981
982 if stderr.contains("Unknown option: -I") {
984 return Err(Error::QueryScript {
985 err: InterpreterInfoError::UnsupportedPython,
986 path: interpreter.to_path_buf(),
987 });
988 }
989
990 return Err(Error::StatusCode(StatusCodeError {
991 code: output.status,
992 stderr,
993 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
994 path: interpreter.to_path_buf(),
995 }));
996 }
997
998 let result: InterpreterInfoResult =
999 serde_json::from_slice(&output.stdout).map_err(|err| {
1000 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1001
1002 if stderr.contains("Unknown option: -I") {
1004 Error::QueryScript {
1005 err: InterpreterInfoError::UnsupportedPython,
1006 path: interpreter.to_path_buf(),
1007 }
1008 } else {
1009 Error::UnexpectedResponse(UnexpectedResponseError {
1010 err,
1011 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1012 stderr,
1013 path: interpreter.to_path_buf(),
1014 })
1015 }
1016 })?;
1017
1018 match result {
1019 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1020 err,
1021 path: interpreter.to_path_buf(),
1022 }),
1023 InterpreterInfoResult::Success(data) => Ok(*data),
1024 }
1025 }
1026
1027 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1030 let python_dir = root.join("python");
1031 fs_err::create_dir(&python_dir)?;
1032 fs_err::write(
1033 python_dir.join("get_interpreter_info.py"),
1034 include_str!("../python/get_interpreter_info.py"),
1035 )?;
1036 fs_err::write(
1037 python_dir.join("__init__.py"),
1038 include_str!("../python/__init__.py"),
1039 )?;
1040 let packaging_dir = python_dir.join("packaging");
1041 fs_err::create_dir(&packaging_dir)?;
1042 fs_err::write(
1043 packaging_dir.join("__init__.py"),
1044 include_str!("../python/packaging/__init__.py"),
1045 )?;
1046 fs_err::write(
1047 packaging_dir.join("_elffile.py"),
1048 include_str!("../python/packaging/_elffile.py"),
1049 )?;
1050 fs_err::write(
1051 packaging_dir.join("_manylinux.py"),
1052 include_str!("../python/packaging/_manylinux.py"),
1053 )?;
1054 fs_err::write(
1055 packaging_dir.join("_musllinux.py"),
1056 include_str!("../python/packaging/_musllinux.py"),
1057 )?;
1058 Ok(())
1059 }
1060
1061 pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1067 let absolute = std::path::absolute(executable)?;
1068
1069 let handle_io_error = |err: io::Error| -> Error {
1073 if err.kind() == io::ErrorKind::NotFound {
1074 if absolute
1077 .symlink_metadata()
1078 .is_ok_and(|metadata| metadata.is_symlink())
1079 {
1080 Error::BrokenSymlink(BrokenSymlink {
1081 path: executable.to_path_buf(),
1082 venv: uv_fs::is_virtualenv_executable(executable),
1083 })
1084 } else {
1085 Error::NotFound(executable.to_path_buf())
1086 }
1087 } else {
1088 err.into()
1089 }
1090 };
1091
1092 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1093
1094 let cache_entry = cache.entry(
1095 CacheBucket::Interpreter,
1096 cache_digest(&(
1099 ARCH,
1100 sys_info::os_type().unwrap_or_default(),
1101 sys_info::os_release().unwrap_or_default(),
1102 )),
1103 format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
1111 );
1112
1113 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1116
1117 if cache
1119 .freshness(&cache_entry, None, None)
1120 .is_ok_and(Freshness::is_fresh)
1121 {
1122 if let Ok(data) = fs::read(cache_entry.path()) {
1123 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1124 Ok(cached) => {
1125 if cached.timestamp == modified {
1126 trace!(
1127 "Found cached interpreter info for Python {}, skipping query of: {}",
1128 cached.data.markers.python_full_version(),
1129 executable.user_display()
1130 );
1131 return Ok(cached.data);
1132 }
1133
1134 trace!(
1135 "Ignoring stale interpreter markers for: {}",
1136 executable.user_display()
1137 );
1138 }
1139 Err(err) => {
1140 warn!(
1141 "Broken interpreter cache entry at {}, removing: {err}",
1142 cache_entry.path().user_display()
1143 );
1144 let _ = fs_err::remove_file(cache_entry.path());
1145 }
1146 }
1147 }
1148 }
1149
1150 trace!(
1152 "Querying interpreter executable at {}",
1153 executable.display()
1154 );
1155 let info = Self::query(executable, cache)?;
1156
1157 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1160 fs::create_dir_all(cache_entry.dir())?;
1161 write_atomic_sync(
1162 cache_entry.path(),
1163 rmp_serde::to_vec(&CachedByTimestamp {
1164 timestamp: modified,
1165 data: info.clone(),
1166 })?,
1167 )?;
1168 }
1169
1170 Ok(info)
1171 }
1172}
1173
1174fn find_base_python(
1200 executable: &Path,
1201 major: u8,
1202 minor: u8,
1203 suffix: &str,
1204) -> Result<PathBuf, io::Error> {
1205 fn is_root(path: &Path) -> bool {
1207 let mut components = path.components();
1208 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1209 }
1210
1211 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1215 if cfg!(windows) {
1216 dir.join("Lib").join("os.py").is_file()
1217 } else {
1218 dir.join("lib")
1219 .join(format!("python{major}.{minor}{suffix}"))
1220 .join("os.py")
1221 .is_file()
1222 }
1223 }
1224
1225 let mut executable = Cow::Borrowed(executable);
1226
1227 loop {
1228 debug!(
1229 "Assessing Python executable as base candidate: {}",
1230 executable.display()
1231 );
1232
1233 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1235 if is_prefix(prefix, major, minor, suffix) {
1236 return Ok(executable.into_owned());
1237 }
1238 }
1239
1240 let resolved = fs_err::read_link(&executable)?;
1242
1243 let resolved = if resolved.is_relative() {
1245 if let Some(parent) = executable.parent() {
1246 parent.join(resolved)
1247 } else {
1248 return Err(io::Error::other("Symlink has no parent directory"));
1249 }
1250 } else {
1251 resolved
1252 };
1253
1254 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1256
1257 executable = Cow::Owned(resolved);
1258 }
1259}
1260
1261#[cfg(unix)]
1262#[cfg(test)]
1263mod tests {
1264 use std::str::FromStr;
1265
1266 use fs_err as fs;
1267 use indoc::{formatdoc, indoc};
1268 use tempfile::tempdir;
1269
1270 use uv_cache::Cache;
1271 use uv_pep440::Version;
1272
1273 use crate::Interpreter;
1274
1275 #[test]
1276 fn test_cache_invalidation() {
1277 let mock_dir = tempdir().unwrap();
1278 let mocked_interpreter = mock_dir.path().join("python");
1279 let json = indoc! {r##"
1280 {
1281 "result": "success",
1282 "platform": {
1283 "os": {
1284 "name": "manylinux",
1285 "major": 2,
1286 "minor": 38
1287 },
1288 "arch": "x86_64"
1289 },
1290 "manylinux_compatible": false,
1291 "standalone": false,
1292 "markers": {
1293 "implementation_name": "cpython",
1294 "implementation_version": "3.12.0",
1295 "os_name": "posix",
1296 "platform_machine": "x86_64",
1297 "platform_python_implementation": "CPython",
1298 "platform_release": "6.5.0-13-generic",
1299 "platform_system": "Linux",
1300 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1301 "python_full_version": "3.12.0",
1302 "python_version": "3.12",
1303 "sys_platform": "linux"
1304 },
1305 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1306 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1307 "sys_prefix": "/home/ferris/projects/uv/.venv",
1308 "sys_executable": "/home/ferris/projects/uv/.venv/bin/python",
1309 "sys_path": [
1310 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1311 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1312 ],
1313 "site_packages": [
1314 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1315 ],
1316 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1317 "scheme": {
1318 "data": "/home/ferris/.pyenv/versions/3.12.0",
1319 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1320 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1321 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1322 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1323 },
1324 "virtualenv": {
1325 "data": "",
1326 "include": "include",
1327 "platlib": "lib/python3.12/site-packages",
1328 "purelib": "lib/python3.12/site-packages",
1329 "scripts": "bin"
1330 },
1331 "pointer_size": "64",
1332 "gil_disabled": true,
1333 "debug_enabled": false
1334 }
1335 "##};
1336
1337 let cache = Cache::temp().unwrap().init().unwrap();
1338
1339 fs::write(
1340 &mocked_interpreter,
1341 formatdoc! {r"
1342 #!/bin/sh
1343 echo '{json}'
1344 "},
1345 )
1346 .unwrap();
1347
1348 fs::set_permissions(
1349 &mocked_interpreter,
1350 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1351 )
1352 .unwrap();
1353 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1354 assert_eq!(
1355 interpreter.markers.python_version().version,
1356 Version::from_str("3.12").unwrap()
1357 );
1358 fs::write(
1359 &mocked_interpreter,
1360 formatdoc! {r"
1361 #!/bin/sh
1362 echo '{}'
1363 ", json.replace("3.12", "3.13")},
1364 )
1365 .unwrap();
1366 let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1367 assert_eq!(
1368 interpreter.markers.python_version().version,
1369 Version::from_str("3.13").unwrap()
1370 );
1371 }
1372}