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