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::sync::OnceLock;
7use std::{env, io};
8
9use configparser::ini::Ini;
10use fs_err as fs;
11use owo_colors::OwoColorize;
12use same_file::is_same_file;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15use tracing::{debug, trace, warn};
16
17use uv_cache::{Cache, CacheBucket, CacheEntry, CachedByTimestamp, Freshness};
18use uv_cache_info::Timestamp;
19use uv_cache_key::cache_digest;
20use uv_fs::{
21 LockedFile, LockedFileError, LockedFileMode, PythonExt, Simplified, write_atomic_sync,
22};
23use uv_install_wheel::Layout;
24use uv_pep440::Version;
25use uv_pep508::{MarkerEnvironment, StringVersion};
26use uv_platform::{Arch, Libc, Os};
27use uv_platform_tags::{Platform, Tags, TagsError, TagsOptions};
28use uv_pypi_types::{ResolverMarkerEnvironment, Scheme};
29use uv_static::EnvVars;
30
31use crate::PointerSize;
32use crate::PyVenvConfiguration;
33use crate::VirtualEnvironment;
34use crate::virtualenv::virtualenv_python_executable;
35use uv_python_types::{
36 ImplementationName, LenientImplementationName, Prefix, PythonDownloadRequest,
37 PythonInstallationKey, PythonRequest, PythonVariant, Target, VersionRequest,
38};
39use which::which;
40
41#[cfg(windows)]
42use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
43
44#[expect(clippy::struct_excessive_bools)]
46#[derive(Debug, Clone, Eq, PartialEq)]
47pub struct Interpreter {
48 platform: Platform,
49 markers: Box<MarkerEnvironment>,
50 scheme: Scheme,
51 virtualenv: Scheme,
52 manylinux_compatible: bool,
53 sys_prefix: PathBuf,
54 sys_base_prefix: PathBuf,
55 sys_base_executable: Option<PathBuf>,
56 sys_executable: PathBuf,
57 site_packages: Vec<PathBuf>,
58 stdlib: PathBuf,
59 extension_suffixes: Vec<Box<str>>,
60 standalone: bool,
61 tags: OnceLock<Tags>,
62 target: Option<Target>,
63 prefix: Option<Prefix>,
64 pointer_size: PointerSize,
65 gil_disabled: bool,
66 real_executable: PathBuf,
67 debug_enabled: bool,
68}
69
70impl Interpreter {
71 pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
73 let executable = executable.as_ref();
74 let info = InterpreterInfo::query_cached(executable, cache)?;
75
76 debug_assert!(
77 info.sys_executable.is_absolute(),
78 "`sys.executable` is not an absolute Python; Python installation is broken: {}",
79 info.sys_executable.display()
80 );
81
82 Ok(Self {
83 platform: info.platform,
84 markers: Box::new(info.markers),
85 scheme: info.scheme,
86 virtualenv: info.virtualenv,
87 manylinux_compatible: info.manylinux_compatible,
88 sys_prefix: info.sys_prefix,
89 pointer_size: info.pointer_size,
90 gil_disabled: info.gil_disabled,
91 debug_enabled: info.debug_enabled,
92 sys_base_prefix: info.sys_base_prefix,
93 sys_base_executable: info.sys_base_executable,
94 sys_executable: info.sys_executable,
95 site_packages: info.site_packages,
96 stdlib: info.stdlib,
97 extension_suffixes: info.extension_suffixes,
98 standalone: info.standalone,
99 tags: OnceLock::new(),
100 target: None,
101 prefix: None,
102 real_executable: executable.to_path_buf(),
103 })
104 }
105
106 pub fn clear_cache(executable: impl AsRef<Path>, cache: &Cache) -> Result<(), Error> {
108 let absolute = std::path::absolute(executable.as_ref())?;
109 let canonical = canonicalize_executable(&absolute)?;
110 let cache_entry = InterpreterInfo::cache_entry(&absolute, &canonical, cache);
111
112 match fs::remove_file(cache_entry.path()) {
113 Ok(()) => Ok(()),
114 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
115 Err(err) => Err(err.into()),
116 }
117 }
118
119 #[must_use]
121 pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
122 let mut site_packages = Vec::new();
126 if self.markers.os_name() == "nt" {
127 site_packages.push(virtualenv.root.clone());
128 site_packages.push(virtualenv.scheme.purelib.clone());
129 } else {
130 if virtualenv.scheme.platlib == virtualenv.scheme.purelib {
131 site_packages.push(virtualenv.scheme.purelib.clone());
132 } else {
133 site_packages.push(virtualenv.scheme.platlib.clone());
134 site_packages.push(virtualenv.scheme.purelib.clone());
135 }
136 for path in &self.site_packages {
141 if let Ok(relative) = path.strip_prefix(&self.sys_prefix) {
142 let path = virtualenv.root.join(relative);
143 if !site_packages.contains(&path) {
144 site_packages.push(path);
145 }
146 }
147 }
148 }
149 Self {
150 scheme: virtualenv.scheme,
151 sys_base_executable: Some(virtualenv.base_executable),
152 sys_executable: virtualenv.executable,
153 sys_prefix: virtualenv.root,
154 target: None,
155 prefix: None,
156 site_packages,
157 ..self
158 }
159 }
160
161 pub(crate) fn with_target(self, target: Target) -> io::Result<Self> {
163 fs::create_dir_all(target.root())?;
164 Ok(Self {
165 target: Some(target),
166 ..self
167 })
168 }
169
170 pub(crate) fn with_prefix(self, prefix: Prefix) -> io::Result<Self> {
172 fs::create_dir_all(prefix.root().join(&self.virtualenv.purelib))?;
173 Ok(Self {
174 prefix: Some(prefix),
175 ..self
176 })
177 }
178
179 pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
189 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
190 let base_python = std::path::absolute(base_executable)?;
191 Ok(base_python)
192 }
193
194 pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
205 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
206 let base_python = match find_base_python(
216 base_executable,
217 self.python_major(),
218 self.python_minor(),
219 self.variant().executable_suffix(),
220 ) {
221 Ok(path) => path,
222 Err(err) => {
223 warn!("Failed to find base Python executable: {err}");
224 canonicalize_executable(base_executable)?
225 }
226 };
227 Ok(base_python)
228 }
229
230 #[inline]
232 pub fn platform(&self) -> &Platform {
233 &self.platform
234 }
235
236 #[inline]
238 pub const fn markers(&self) -> &MarkerEnvironment {
239 &self.markers
240 }
241
242 pub fn to_resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
244 ResolverMarkerEnvironment::from(self.markers().clone())
245 }
246
247 pub fn key(&self) -> PythonInstallationKey {
249 PythonInstallationKey::new(
250 LenientImplementationName::from(self.implementation_name()),
251 self.python_major(),
252 self.python_minor(),
253 self.python_patch(),
254 self.python_version().pre(),
255 uv_platform::Platform::new(self.os(), self.arch(), self.libc()),
256 self.variant(),
257 )
258 }
259
260 pub fn variant(&self) -> PythonVariant {
261 if self.gil_disabled() {
262 if self.debug_enabled() {
263 PythonVariant::FreethreadedDebug
264 } else {
265 PythonVariant::Freethreaded
266 }
267 } else if self.debug_enabled() {
268 PythonVariant::Debug
269 } else {
270 PythonVariant::default()
271 }
272 }
273
274 pub fn arch(&self) -> Arch {
276 Arch::from(&self.platform().arch())
277 }
278
279 pub fn libc(&self) -> Libc {
281 Libc::from(self.platform().os())
282 }
283
284 pub fn os(&self) -> Os {
286 Os::from(self.platform().os())
287 }
288
289 pub fn tags(&self) -> Result<&Tags, TagsError> {
291 if self.tags.get().is_none() {
292 let tags = Tags::from_env(
293 self.platform().clone(),
294 self.python_tuple(),
295 self.implementation_name(),
296 self.implementation_tuple(),
297 TagsOptions {
298 manylinux_compatible: self.manylinux_compatible,
299 gil_disabled: self.gil_disabled,
300 debug_enabled: self.debug_enabled,
301 is_cross: false,
302 },
303 )?;
304 self.tags.set(tags).expect("tags should not be set");
305 }
306 Ok(self.tags.get().expect("tags should be set"))
307 }
308
309 pub fn is_virtualenv(&self) -> bool {
313 self.sys_prefix != self.sys_base_prefix
315 }
316
317 fn is_target(&self) -> bool {
319 self.target.is_some()
320 }
321
322 fn is_prefix(&self) -> bool {
324 self.prefix.is_some()
325 }
326
327 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
332 if self.is_virtualenv() {
334 return None;
335 }
336
337 if self.is_target() || self.is_prefix() {
339 return None;
340 }
341
342 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
343 return None;
344 };
345
346 let mut ini = Ini::new_cs();
347 ini.set_multiline(true);
348
349 let Ok(mut sections) = ini.read(contents) else {
350 return Some(ExternallyManaged::default());
353 };
354
355 let Some(section) = sections.get_mut("externally-managed") else {
356 return Some(ExternallyManaged::default());
359 };
360
361 let Some(error) = section.remove("Error") else {
362 return Some(ExternallyManaged::default());
365 };
366
367 Some(ExternallyManaged { error })
368 }
369
370 #[inline]
372 pub fn python_full_version(&self) -> &StringVersion {
373 self.markers.python_full_version()
374 }
375
376 #[inline]
378 pub fn python_version(&self) -> &Version {
379 &self.markers.python_full_version().version
380 }
381
382 #[inline]
384 pub fn python_minor_version(&self) -> Version {
385 Version::new(self.python_version().release().iter().take(2).copied())
386 }
387
388 #[inline]
390 pub(crate) fn python_patch_version(&self) -> Version {
391 Version::new(self.python_version().release().iter().take(3).copied())
392 }
393
394 pub fn python_major(&self) -> u8 {
396 let major = self.markers.python_full_version().version.release()[0];
397 u8::try_from(major).expect("invalid major version")
398 }
399
400 pub fn python_minor(&self) -> u8 {
402 let minor = self.markers.python_full_version().version.release()[1];
403 u8::try_from(minor).expect("invalid minor version")
404 }
405
406 fn python_patch(&self) -> u8 {
408 let minor = self.markers.python_full_version().version.release()[2];
409 u8::try_from(minor).expect("invalid patch version")
410 }
411
412 pub fn python_tuple(&self) -> (u8, u8) {
414 (self.python_major(), self.python_minor())
415 }
416
417 fn implementation_major(&self) -> u8 {
419 let major = self.markers.implementation_version().version.release()[0];
420 u8::try_from(major).expect("invalid major version")
421 }
422
423 fn implementation_minor(&self) -> u8 {
425 let minor = self.markers.implementation_version().version.release()[1];
426 u8::try_from(minor).expect("invalid minor version")
427 }
428
429 pub fn implementation_tuple(&self) -> (u8, u8) {
431 (self.implementation_major(), self.implementation_minor())
432 }
433
434 pub fn implementation_name(&self) -> &str {
436 self.markers.implementation_name()
437 }
438
439 pub fn sys_base_prefix(&self) -> &Path {
441 &self.sys_base_prefix
442 }
443
444 pub fn sys_prefix(&self) -> &Path {
446 &self.sys_prefix
447 }
448
449 fn sys_base_executable(&self) -> Option<&Path> {
452 self.sys_base_executable.as_deref()
453 }
454
455 pub fn sys_executable(&self) -> &Path {
457 &self.sys_executable
458 }
459
460 pub fn extension_suffixes(&self) -> &[Box<str>] {
462 &self.extension_suffixes
463 }
464
465 pub fn real_executable(&self) -> &Path {
467 &self.real_executable
468 }
469
470 pub fn runtime_site_packages(&self) -> &[PathBuf] {
478 &self.site_packages
479 }
480
481 pub fn stdlib(&self) -> &Path {
483 &self.stdlib
484 }
485
486 fn purelib(&self) -> &Path {
488 &self.scheme.purelib
489 }
490
491 fn platlib(&self) -> &Path {
493 &self.scheme.platlib
494 }
495
496 pub fn scripts(&self) -> &Path {
498 &self.scheme.scripts
499 }
500
501 fn data(&self) -> &Path {
503 &self.scheme.data
504 }
505
506 fn include(&self) -> &Path {
508 &self.scheme.include
509 }
510
511 pub fn virtualenv(&self) -> &Scheme {
513 &self.virtualenv
514 }
515
516 pub fn manylinux_compatible(&self) -> bool {
518 self.manylinux_compatible
519 }
520
521 pub fn pointer_size(&self) -> PointerSize {
523 self.pointer_size
524 }
525
526 pub fn gil_disabled(&self) -> bool {
532 self.gil_disabled
533 }
534
535 pub fn debug_enabled(&self) -> bool {
538 self.debug_enabled
539 }
540
541 fn target(&self) -> Option<&Target> {
543 self.target.as_ref()
544 }
545
546 fn prefix(&self) -> Option<&Prefix> {
548 self.prefix.as_ref()
549 }
550
551 #[cfg(unix)]
560 pub fn is_standalone(&self) -> bool {
561 self.standalone
562 }
563
564 pub fn layout(&self) -> Layout {
566 Layout {
567 python_version: self.python_tuple(),
568 sys_executable: self.sys_executable().to_path_buf(),
569 os_name: self.markers.os_name().to_string(),
570 scheme: if let Some(target) = self.target.as_ref() {
571 Scheme {
572 purelib: target.root().to_path_buf(),
573 platlib: target.root().to_path_buf(),
574 scripts: target.root().join("bin"),
575 data: target.root().to_path_buf(),
576 include: target.root().join("include"),
577 }
578 } else if let Some(prefix) = self.prefix.as_ref() {
579 Scheme {
580 purelib: prefix.root().join(&self.virtualenv.purelib),
581 platlib: prefix.root().join(&self.virtualenv.platlib),
582 scripts: prefix.root().join(&self.virtualenv.scripts),
583 data: prefix.root().join(&self.virtualenv.data),
584 include: prefix.root().join(&self.virtualenv.include),
585 }
586 } else {
587 Scheme {
588 purelib: self.purelib().to_path_buf(),
589 platlib: self.platlib().to_path_buf(),
590 scripts: self.scripts().to_path_buf(),
591 data: self.data().to_path_buf(),
592 include: if self.is_virtualenv() {
593 self.sys_prefix.join("include").join("site").join(format!(
596 "python{}.{}",
597 self.python_major(),
598 self.python_minor()
599 ))
600 } else {
601 self.include().to_path_buf()
602 },
603 }
604 },
605 }
606 }
607
608 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
619 let target = self.target().map(Target::root);
620
621 let prefix = self
622 .prefix()
623 .map(|prefix| prefix.root().join(&self.virtualenv.purelib));
624
625 let interpreter = if target.is_none() && prefix.is_none() {
626 let purelib = self.purelib();
627 let platlib = self.platlib();
628 Some(std::iter::once(purelib).chain(
629 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
630 None
631 } else {
632 Some(platlib)
633 },
634 ))
635 } else {
636 None
637 };
638
639 target
640 .into_iter()
641 .map(Cow::Borrowed)
642 .chain(prefix.into_iter().map(Cow::Owned))
643 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
644 }
645
646 pub fn has_default_executable_name(&self) -> bool {
649 let Some(file_name) = self.sys_executable().file_name() else {
650 return false;
651 };
652 let Some(name) = file_name.to_str() else {
653 return false;
654 };
655 VersionRequest::Default
656 .executable_names(None)
657 .into_iter()
658 .any(|default_name| name == default_name.to_string())
659 }
660
661 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
663 if let Some(target) = self.target() {
664 LockedFile::acquire(
666 target.root().join(".lock"),
667 LockedFileMode::Exclusive,
668 target.root().user_display(),
669 )
670 .await
671 } else if let Some(prefix) = self.prefix() {
672 LockedFile::acquire(
674 prefix.root().join(".lock"),
675 LockedFileMode::Exclusive,
676 prefix.root().user_display(),
677 )
678 .await
679 } else if self.is_virtualenv() {
680 LockedFile::acquire(
682 self.sys_prefix.join(".lock"),
683 LockedFileMode::Exclusive,
684 self.sys_prefix.user_display(),
685 )
686 .await
687 } else {
688 LockedFile::acquire(
690 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
691 LockedFileMode::Exclusive,
692 self.sys_prefix.user_display(),
693 )
694 .await
695 }
696 }
697
698 fn matches_variant(&self, variant: PythonVariant) -> bool {
700 match variant {
701 PythonVariant::Default => {
702 if (self.python_major(), self.python_minor()) >= (3, 14) {
705 true
708 } else {
709 !self.gil_disabled()
712 }
713 }
714 PythonVariant::Debug => self.debug_enabled(),
715 PythonVariant::Freethreaded => self.gil_disabled(),
716 PythonVariant::FreethreadedDebug => self.gil_disabled() && self.debug_enabled(),
717 PythonVariant::Gil => !self.gil_disabled(),
718 PythonVariant::GilDebug => !self.gil_disabled() && self.debug_enabled(),
719 }
720 }
721
722 pub fn matches_implementation(&self, implementation: ImplementationName) -> bool {
724 match implementation {
725 ImplementationName::Pyodide => self.os().is_emscripten(),
726 _ => self
727 .implementation_name()
728 .eq_ignore_ascii_case(implementation.long_name()),
729 }
730 }
731
732 pub fn matches_version_request(&self, request: &VersionRequest) -> bool {
734 match request {
735 VersionRequest::Any => true,
736 VersionRequest::Default => self.matches_variant(PythonVariant::Default),
738 VersionRequest::Major(major, variant) => {
739 self.python_major() == *major && self.matches_variant(*variant)
740 }
741 VersionRequest::MajorMinor(major, minor, variant) => {
742 (self.python_major(), self.python_minor()) == (*major, *minor)
743 && self.matches_variant(*variant)
744 }
745 VersionRequest::MajorMinorPatch(major, minor, patch, variant) => {
746 (
747 self.python_major(),
748 self.python_minor(),
749 self.python_patch(),
750 ) == (*major, *minor, *patch)
751 && self.python_version().pre().is_none()
754 && self.matches_variant(*variant)
755 }
756 VersionRequest::Range(specifiers, variant) => {
757 let version = if specifiers
760 .iter()
761 .any(uv_pep440::VersionSpecifier::any_prerelease)
762 {
763 Cow::Borrowed(self.python_version())
764 } else {
765 Cow::Owned(self.python_version().only_release())
766 };
767 specifiers.contains(&version) && self.matches_variant(*variant)
768 }
769 VersionRequest::MajorMinorPrerelease(major, minor, prerelease, variant) => {
770 let version = self.python_version();
771 let Some(interpreter_prerelease) = version.pre() else {
772 return false;
773 };
774 (
775 self.python_major(),
776 self.python_minor(),
777 interpreter_prerelease,
778 ) == (*major, *minor, *prerelease)
779 && self.matches_variant(*variant)
780 }
781 VersionRequest::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
782 let version = self.python_version();
783 let Some(interpreter_prerelease) = version.pre() else {
784 return false;
785 };
786 (
787 self.python_major(),
788 self.python_minor(),
789 self.python_patch(),
790 interpreter_prerelease,
791 ) == (*major, *minor, *patch, *prerelease)
792 && self.matches_variant(*variant)
793 }
794 }
795 }
796
797 pub fn matches_download_request(&self, request: &PythonDownloadRequest) -> bool {
799 let executable = self.sys_executable().display();
800 if let Some(version) = request.version()
801 && !self.matches_version_request(version)
802 {
803 let interpreter_version = self.python_version();
804 debug!(
805 "Skipping interpreter at `{executable}`: version `{interpreter_version}` does not match request `{version}`"
806 );
807 return false;
808 }
809 let platform = request.platform();
810 let interpreter_platform = uv_platform::Platform::from(self.platform());
811 if !platform.matches(&interpreter_platform) {
812 debug!(
813 "Skipping interpreter at `{executable}`: platform `{interpreter_platform}` does not match request `{platform}`",
814 );
815 return false;
816 }
817 if let Some(implementation) = request.implementation()
818 && !self.matches_implementation(*implementation)
819 {
820 debug!(
821 "Skipping interpreter at `{executable}`: implementation `{}` does not match request `{implementation}`",
822 self.implementation_name(),
823 );
824 return false;
825 }
826 true
827 }
828
829 pub fn matches_request(&self, request: &PythonRequest, cache: &Cache) -> bool {
831 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
833 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
834 }
835
836 match request {
837 PythonRequest::Default | PythonRequest::Any => true,
838 PythonRequest::Version(version_request) => {
839 self.matches_version_request(version_request)
840 }
841 PythonRequest::Directory(directory) => {
842 is_same_executable(directory, self.sys_prefix())
844 || is_same_executable(
845 virtualenv_python_executable(directory).as_path(),
846 self.sys_executable(),
847 )
848 }
849 PythonRequest::File(file) => {
850 if is_same_executable(self.sys_executable(), file) {
852 return true;
853 }
854 if self
856 .sys_base_executable()
857 .is_some_and(|sys_base_executable| {
858 is_same_executable(sys_base_executable, file)
859 })
860 {
861 return true;
862 }
863 if cfg!(windows) {
868 if let Ok(file_interpreter) = Self::query(file, cache) {
869 if let (Some(file_base), Some(interpreter_base)) = (
870 file_interpreter.sys_base_executable(),
871 self.sys_base_executable(),
872 ) {
873 if is_same_executable(file_base, interpreter_base) {
874 return true;
875 }
876 }
877 }
878 }
879 false
880 }
881 PythonRequest::ExecutableName(name) => {
882 if self
884 .sys_executable()
885 .file_name()
886 .is_some_and(|filename| filename == name.as_str())
887 {
888 return true;
889 }
890 if self
892 .sys_base_executable()
893 .and_then(|executable| executable.file_name())
894 .is_some_and(|file_name| file_name == name.as_str())
895 {
896 return true;
897 }
898 if which(name)
901 .ok()
902 .as_ref()
903 .and_then(|executable| executable.file_name())
904 .is_some_and(|file_name| file_name == name.as_str())
905 {
906 return true;
907 }
908 false
909 }
910 PythonRequest::Implementation(implementation) => {
911 self.matches_implementation(*implementation)
912 }
913 PythonRequest::ImplementationVersion(implementation, version) => {
914 self.matches_version_request(version)
915 && self.matches_implementation(*implementation)
916 }
917 PythonRequest::Key(request) => self.matches_download_request(request),
918 }
919 }
920}
921
922pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
925 let path = path.as_ref();
926 debug_assert!(
927 path.is_absolute(),
928 "path must be absolute: {}",
929 path.display()
930 );
931
932 #[cfg(windows)]
933 {
934 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
935 Ok(dunce::canonicalize(launcher.python_path)?)
936 } else {
937 Ok(path.to_path_buf())
938 }
939 }
940
941 #[cfg(unix)]
942 fs_err::canonicalize(path)
943}
944
945#[derive(Debug, Default, Clone)]
949pub struct ExternallyManaged {
950 error: Option<String>,
951}
952
953impl ExternallyManaged {
954 pub fn into_error(self) -> Option<String> {
956 self.error
957 }
958}
959
960#[derive(Debug, Error)]
961pub struct UnexpectedResponseError {
962 #[source]
963 err: serde_json::Error,
964 stdout: String,
965 stderr: String,
966 pub path: PathBuf,
967}
968
969impl Display for UnexpectedResponseError {
970 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
971 write!(
972 f,
973 "Querying Python at `{}` returned an invalid response: {}",
974 self.path.display(),
975 self.err
976 )?;
977
978 let mut non_empty = false;
979
980 if !self.stdout.trim().is_empty() {
981 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
982 non_empty = true;
983 }
984
985 if !self.stderr.trim().is_empty() {
986 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
987 non_empty = true;
988 }
989
990 if non_empty {
991 writeln!(f)?;
992 }
993
994 Ok(())
995 }
996}
997
998#[derive(Debug, Error)]
999pub struct StatusCodeError {
1000 code: ExitStatus,
1001 stdout: String,
1002 stderr: String,
1003 pub path: PathBuf,
1004}
1005
1006impl Display for StatusCodeError {
1007 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1008 write!(
1009 f,
1010 "Querying Python at `{}` failed with exit status {}",
1011 self.path.display(),
1012 self.code
1013 )?;
1014
1015 let mut non_empty = false;
1016
1017 if !self.stdout.trim().is_empty() {
1018 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
1019 non_empty = true;
1020 }
1021
1022 if !self.stderr.trim().is_empty() {
1023 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
1024 non_empty = true;
1025 }
1026
1027 if non_empty {
1028 writeln!(f)?;
1029 }
1030
1031 Ok(())
1032 }
1033}
1034
1035#[derive(Debug, Error)]
1036pub enum Error {
1037 #[error("Failed to query Python interpreter")]
1038 Io(#[from] io::Error),
1039 #[error(transparent)]
1040 BrokenLink(BrokenLink),
1041 #[error("Python interpreter not found at `{0}`")]
1042 NotFound(PathBuf),
1043 #[error("Failed to query Python interpreter at `{path}`")]
1044 SpawnFailed {
1045 path: PathBuf,
1046 #[source]
1047 err: io::Error,
1048 },
1049 #[cfg(windows)]
1050 #[error("Failed to query Python interpreter at `{path}`")]
1051 CorruptWindowsPackage {
1052 path: PathBuf,
1053 #[source]
1054 err: io::Error,
1055 },
1056 #[error("Failed to query Python interpreter at `{path}`")]
1057 PermissionDenied {
1058 path: PathBuf,
1059 #[source]
1060 err: io::Error,
1061 },
1062 #[error("{0}")]
1063 UnexpectedResponse(UnexpectedResponseError),
1064 #[error("{0}")]
1065 StatusCode(StatusCodeError),
1066 #[error("Can't use Python at `{path}`")]
1067 QueryScript {
1068 #[source]
1069 err: InterpreterInfoError,
1070 path: PathBuf,
1071 },
1072 #[error("Failed to write to cache")]
1073 Encode(#[from] rmp_serde::encode::Error),
1074}
1075
1076impl uv_errors::Hinted for Error {
1077 fn hints(&self) -> uv_errors::Hints<'_> {
1078 match self {
1079 Self::BrokenLink(err) => err.hints(),
1080 _ => uv_errors::Hints::none(),
1081 }
1082 }
1083}
1084
1085#[derive(Debug, Error)]
1086pub struct BrokenLink {
1087 pub path: PathBuf,
1088 pub unix: bool,
1091 pub venv: bool,
1093}
1094
1095impl Display for BrokenLink {
1096 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1097 if self.unix {
1098 write!(
1099 f,
1100 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
1101 self.path.user_display()
1102 )
1103 } else {
1104 write!(
1105 f,
1106 "Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
1107 self.path.user_display()
1108 )
1109 }
1110 }
1111}
1112
1113impl uv_errors::Hinted for BrokenLink {
1114 fn hints(&self) -> uv_errors::Hints<'_> {
1115 if self.venv {
1116 uv_errors::Hints::from(format!(
1117 "Consider recreating the environment (e.g., with `{}`)",
1118 "uv venv".green()
1119 ))
1120 } else {
1121 uv_errors::Hints::none()
1122 }
1123 }
1124}
1125
1126#[derive(Debug, Deserialize, Serialize)]
1127#[serde(tag = "result", rename_all = "lowercase")]
1128enum InterpreterInfoResult {
1129 Error(InterpreterInfoError),
1130 Success(Box<InterpreterInfo>),
1131}
1132
1133#[derive(Debug, Error, Deserialize, Serialize)]
1134#[serde(tag = "kind", rename_all = "snake_case")]
1135pub enum InterpreterInfoError {
1136 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
1137 LibcNotFound,
1138 #[error(
1139 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
1140 )]
1141 BrokenMacVer,
1142 #[error("Unknown operating system: `{operating_system}`")]
1143 UnknownOperatingSystem { operating_system: String },
1144 #[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
1145 UnsupportedPythonVersion { python_version: String },
1146 #[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
1147 UnsupportedPython,
1148 #[error(
1149 "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`."
1150 )]
1151 MissingRequiredDistutils {
1152 python_major: usize,
1153 python_minor: usize,
1154 },
1155 #[error("Only Pyodide is supported for Emscripten Python")]
1156 EmscriptenNotPyodide,
1157}
1158
1159#[expect(clippy::struct_excessive_bools)]
1160#[derive(Debug, Deserialize, Serialize, Clone)]
1161pub(crate) struct InterpreterInfo {
1162 platform: Platform,
1163 markers: MarkerEnvironment,
1164 scheme: Scheme,
1165 virtualenv: Scheme,
1166 manylinux_compatible: bool,
1167 sys_prefix: PathBuf,
1168 sys_base_exec_prefix: PathBuf,
1169 sys_base_prefix: PathBuf,
1170 sys_base_executable: Option<PathBuf>,
1171 sys_executable: PathBuf,
1172 sys_path: Vec<PathBuf>,
1173 site_packages: Vec<PathBuf>,
1174 stdlib: PathBuf,
1175 extension_suffixes: Vec<Box<str>>,
1176 standalone: bool,
1177 pointer_size: PointerSize,
1178 gil_disabled: bool,
1179 debug_enabled: bool,
1180}
1181
1182impl InterpreterInfo {
1183 pub(crate) fn from_virtualenv(interpreter: &Interpreter) -> Result<Self, Error> {
1185 let scheme = Scheme {
1187 purelib: interpreter.scheme.purelib.simplified().to_path_buf(),
1188 platlib: interpreter.scheme.platlib.simplified().to_path_buf(),
1189 scripts: interpreter.scheme.scripts.simplified().to_path_buf(),
1190 data: interpreter.scheme.data.simplified().components().collect(),
1192 include: interpreter.scheme.include.simplified().to_path_buf(),
1193 };
1194 Ok(Self {
1195 platform: interpreter.platform.clone(),
1196 markers: (*interpreter.markers).clone(),
1197 scheme,
1198 virtualenv: interpreter.virtualenv.clone(),
1199 manylinux_compatible: interpreter.manylinux_compatible,
1200 sys_prefix: interpreter.sys_prefix.simplified().to_path_buf(),
1201 sys_base_exec_prefix: PathBuf::new(),
1203 sys_path: Vec::new(),
1204 sys_base_prefix: interpreter.sys_base_prefix.clone(),
1205 sys_base_executable: interpreter.sys_base_executable.clone(),
1206 sys_executable: std::path::absolute(interpreter.sys_executable())?
1207 .simplified()
1208 .to_path_buf(),
1209 site_packages: interpreter
1210 .site_packages
1211 .iter()
1212 .map(|path| path.simplified().to_path_buf())
1213 .collect(),
1214 stdlib: interpreter.stdlib.clone(),
1215 extension_suffixes: interpreter.extension_suffixes.clone(),
1216 standalone: interpreter.standalone,
1217 pointer_size: interpreter.pointer_size,
1218 gil_disabled: interpreter.gil_disabled,
1219 debug_enabled: interpreter.debug_enabled,
1220 })
1221 }
1222
1223 pub(crate) fn cache(&self, executable: &Path, cache: &Cache) -> Result<(), Error> {
1225 let absolute = std::path::absolute(executable)?;
1227 let canonical = canonicalize_executable(&absolute)?;
1228 let cache_entry = Self::cache_entry(&absolute, &canonical, cache);
1229 let modified = Timestamp::from_path(&canonical)?;
1230 self.write_cache(&cache_entry, modified)
1231 }
1232
1233 fn write_cache(&self, cache_entry: &CacheEntry, modified: Timestamp) -> Result<(), Error> {
1235 fs::create_dir_all(cache_entry.dir())?;
1236 write_atomic_sync(
1237 cache_entry.path(),
1238 rmp_serde::to_vec(&CachedByTimestamp {
1239 timestamp: modified,
1240 data: self,
1241 })?,
1242 )?;
1243 Ok(())
1244 }
1245
1246 fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
1248 let tempdir = tempfile::tempdir_in(cache.root())?;
1249 Self::setup_python_query_files(tempdir.path())?;
1250
1251 let script = format!(
1259 r"import sys; sys.path = [{}] + sys.path; from python.get_interpreter_info import main; main()",
1260 tempdir.path().escape_for_python()
1261 );
1262 let mut command = Command::new(interpreter);
1263 command
1264 .arg("-I") .arg("-B") .arg("-c")
1267 .arg(script);
1268
1269 #[cfg(target_os = "macos")]
1278 command.env("SYSTEM_VERSION_COMPAT", "0");
1279
1280 let output = command.output().map_err(|err| {
1281 match err.kind() {
1282 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
1283 io::ErrorKind::PermissionDenied => {
1284 return Error::PermissionDenied {
1285 path: interpreter.to_path_buf(),
1286 err,
1287 };
1288 }
1289 _ => {}
1290 }
1291 #[cfg(windows)]
1292 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
1293 .raw_os_error()
1294 .and_then(|code| u32::try_from(code).ok())
1295 .map(WIN32_ERROR)
1296 {
1297 return Error::CorruptWindowsPackage {
1300 path: interpreter.to_path_buf(),
1301 err,
1302 };
1303 }
1304 Error::SpawnFailed {
1305 path: interpreter.to_path_buf(),
1306 err,
1307 }
1308 })?;
1309
1310 if !output.status.success() {
1311 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1312
1313 if python_home(interpreter).is_some_and(|home| !home.exists()) {
1319 return Err(Error::BrokenLink(BrokenLink {
1320 path: interpreter.to_path_buf(),
1321 unix: false,
1322 venv: uv_fs::is_virtualenv_executable(interpreter),
1323 }));
1324 }
1325
1326 if stderr.contains("Unknown option: -I") {
1328 return Err(Error::QueryScript {
1329 err: InterpreterInfoError::UnsupportedPython,
1330 path: interpreter.to_path_buf(),
1331 });
1332 }
1333
1334 return Err(Error::StatusCode(StatusCodeError {
1335 code: output.status,
1336 stderr,
1337 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1338 path: interpreter.to_path_buf(),
1339 }));
1340 }
1341
1342 let result: InterpreterInfoResult =
1343 serde_json::from_slice(&output.stdout).map_err(|err| {
1344 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1345
1346 if stderr.contains("Unknown option: -I") {
1348 Error::QueryScript {
1349 err: InterpreterInfoError::UnsupportedPython,
1350 path: interpreter.to_path_buf(),
1351 }
1352 } else {
1353 Error::UnexpectedResponse(UnexpectedResponseError {
1354 err,
1355 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1356 stderr,
1357 path: interpreter.to_path_buf(),
1358 })
1359 }
1360 })?;
1361
1362 match result {
1363 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1364 err,
1365 path: interpreter.to_path_buf(),
1366 }),
1367 InterpreterInfoResult::Success(data) => Ok(*data),
1368 }
1369 }
1370
1371 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1374 let python_dir = root.join("python");
1375 fs_err::create_dir(&python_dir)?;
1376 fs_err::write(
1377 python_dir.join("get_interpreter_info.py"),
1378 include_str!("../python/get_interpreter_info.py"),
1379 )?;
1380 fs_err::write(
1381 python_dir.join("__init__.py"),
1382 include_str!("../python/__init__.py"),
1383 )?;
1384 let packaging_dir = python_dir.join("packaging");
1385 fs_err::create_dir(&packaging_dir)?;
1386 fs_err::write(
1387 packaging_dir.join("__init__.py"),
1388 include_str!("../python/packaging/__init__.py"),
1389 )?;
1390 fs_err::write(
1391 packaging_dir.join("_elffile.py"),
1392 include_str!("../python/packaging/_elffile.py"),
1393 )?;
1394 fs_err::write(
1395 packaging_dir.join("_manylinux.py"),
1396 include_str!("../python/packaging/_manylinux.py"),
1397 )?;
1398 fs_err::write(
1399 packaging_dir.join("_musllinux.py"),
1400 include_str!("../python/packaging/_musllinux.py"),
1401 )?;
1402 Ok(())
1403 }
1404
1405 fn cache_entry(absolute: &Path, canonical: &Path, cache: &Cache) -> CacheEntry {
1407 let python_executable = env::var_os(EnvVars::PYTHONEXECUTABLE).map(PathBuf::from);
1408 let pyvenv_launcher = env::var_os(EnvVars::PYVENV_LAUNCHER).map(PathBuf::from);
1409 let file_stem = cache_digest(&(absolute, canonical, &python_executable, &pyvenv_launcher));
1420 cache.entry(
1421 CacheBucket::Interpreter,
1422 cache_digest(&(
1425 ARCH,
1426 uv_platform::OsType::from_env()
1427 .map(|os_type| os_type.to_string())
1428 .unwrap_or_default(),
1429 uv_platform::OsRelease::from_env()
1430 .map(|os_release| os_release.to_string())
1431 .unwrap_or_default(),
1432 )),
1433 format!("{file_stem}.msgpack"),
1434 )
1435 }
1436
1437 fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1443 let absolute = std::path::absolute(executable)?;
1444
1445 let handle_io_error = |err: io::Error| -> Error {
1449 if err.kind() == io::ErrorKind::NotFound {
1450 if absolute
1453 .symlink_metadata()
1454 .is_ok_and(|metadata| metadata.is_symlink())
1455 {
1456 Error::BrokenLink(BrokenLink {
1457 path: executable.to_path_buf(),
1458 unix: true,
1459 venv: uv_fs::is_virtualenv_executable(executable),
1460 })
1461 } else {
1462 Error::NotFound(executable.to_path_buf())
1463 }
1464 } else {
1465 err.into()
1466 }
1467 };
1468
1469 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1470 let cache_entry = Self::cache_entry(&absolute, &canonical, cache);
1471
1472 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1475
1476 if cache
1478 .freshness(&cache_entry, None, None)
1479 .is_ok_and(Freshness::is_fresh)
1480 {
1481 if let Ok(data) = fs::read(cache_entry.path()) {
1482 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1483 Ok(cached) => {
1484 if cached.timestamp == modified {
1485 trace!(
1486 "Found cached interpreter info for Python {}, skipping query of: {}",
1487 cached.data.markers.python_full_version(),
1488 executable.user_display()
1489 );
1490 return Ok(cached.data);
1491 }
1492
1493 trace!(
1494 "Ignoring stale interpreter markers for: {}",
1495 executable.user_display()
1496 );
1497 }
1498 Err(err) => {
1499 warn!(
1500 "Broken interpreter cache entry at `{}`, removing: {err}",
1501 cache_entry.path().user_display()
1502 );
1503 let _ = fs_err::remove_file(cache_entry.path());
1504 }
1505 }
1506 }
1507 }
1508
1509 trace!(
1511 "Querying interpreter executable at `{}`",
1512 executable.display()
1513 );
1514 let info = Self::query(executable, cache)?;
1515
1516 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1519 info.write_cache(&cache_entry, modified)?;
1520 }
1521
1522 Ok(info)
1523 }
1524}
1525
1526fn find_base_python(
1552 executable: &Path,
1553 major: u8,
1554 minor: u8,
1555 suffix: &str,
1556) -> Result<PathBuf, io::Error> {
1557 fn is_root(path: &Path) -> bool {
1559 let mut components = path.components();
1560 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1561 }
1562
1563 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1567 if cfg!(windows) {
1568 dir.join("Lib").join("os.py").is_file()
1569 } else {
1570 dir.join("lib")
1571 .join(format!("python{major}.{minor}{suffix}"))
1572 .join("os.py")
1573 .is_file()
1574 }
1575 }
1576
1577 let mut executable = Cow::Borrowed(executable);
1578
1579 loop {
1580 debug!(
1581 "Assessing Python executable as base candidate: {}",
1582 executable.display()
1583 );
1584
1585 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1587 if is_prefix(prefix, major, minor, suffix) {
1588 return Ok(executable.into_owned());
1589 }
1590 }
1591
1592 let resolved = fs_err::read_link(&executable)?;
1594
1595 let resolved = if resolved.is_relative() {
1597 if let Some(parent) = executable.parent() {
1598 parent.join(resolved)
1599 } else {
1600 return Err(io::Error::other("Symlink has no parent directory"));
1601 }
1602 } else {
1603 resolved
1604 };
1605
1606 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1608
1609 executable = Cow::Owned(resolved);
1610 }
1611}
1612
1613fn python_home(interpreter: &Path) -> Option<PathBuf> {
1615 let venv_root = interpreter.parent()?.parent()?;
1616 let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
1617 pyvenv_cfg.home
1618}
1619
1620#[cfg(unix)]
1621#[cfg(test)]
1622mod tests {
1623 use std::str::FromStr;
1624 use std::time::{Duration, UNIX_EPOCH};
1625
1626 use anyhow::Result;
1627 use fs_err as fs;
1628 use indoc::{formatdoc, indoc};
1629 use serde_json::Value;
1630 use tempfile::tempdir;
1631
1632 use uv_cache::{Cache, CacheBucket, CachedByTimestamp};
1633 use uv_cache_info::Timestamp;
1634 use uv_pep440::Version;
1635
1636 use crate::Interpreter;
1637 use crate::interpreter::{InterpreterInfo, canonicalize_executable};
1638
1639 fn mocked_interpreter_response() -> &'static str {
1640 indoc! {r##"
1641 {
1642 "result": "success",
1643 "platform": {
1644 "os": {
1645 "name": "manylinux",
1646 "major": 2,
1647 "minor": 38
1648 },
1649 "arch": "x86_64"
1650 },
1651 "manylinux_compatible": false,
1652 "standalone": false,
1653 "markers": {
1654 "implementation_name": "cpython",
1655 "implementation_version": "3.12.0",
1656 "os_name": "posix",
1657 "platform_machine": "x86_64",
1658 "platform_python_implementation": "CPython",
1659 "platform_release": "6.5.0-13-generic",
1660 "platform_system": "Linux",
1661 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1662 "python_full_version": "3.12.0",
1663 "python_version": "3.12",
1664 "sys_platform": "linux"
1665 },
1666 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1667 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1668 "sys_prefix": "/home/ferris/projects/uv/.venv",
1669 "sys_executable": "{sys_executable}",
1670 "sys_path": [
1671 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1672 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1673 ],
1674 "site_packages": [
1675 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1676 ],
1677 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1678 "extension_suffixes": [".cpython-312-x86_64-linux-gnu.so", ".abi3.so", ".so"],
1679 "scheme": {
1680 "data": "/home/ferris/.pyenv/versions/3.12.0",
1681 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1682 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1683 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1684 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1685 },
1686 "virtualenv": {
1687 "data": "",
1688 "include": "include",
1689 "platlib": "lib/python3.12/site-packages",
1690 "purelib": "lib/python3.12/site-packages",
1691 "scripts": "bin"
1692 },
1693 "pointer_size": "64",
1694 "gil_disabled": true,
1695 "debug_enabled": false
1696 }
1697 "##}
1698 }
1699
1700 #[tokio::test]
1701 async fn test_cache_invalidation() -> Result<()> {
1702 let mock_dir = tempdir()?;
1703 let mocked_interpreter = mock_dir.path().join("python");
1704 let query_log = mock_dir.path().join("queries");
1705 let json = mocked_interpreter_response().replace(
1706 "{sys_executable}",
1707 &mocked_interpreter.display().to_string(),
1708 );
1709
1710 let cache = Cache::temp()?.init().await?;
1711
1712 fs::write(
1713 &mocked_interpreter,
1714 formatdoc! {r"
1715 #!/bin/sh
1716 echo queried >> '{}'
1717 echo '{json}'
1718 ", query_log.display()},
1719 )?;
1720
1721 fs::set_permissions(
1722 &mocked_interpreter,
1723 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1724 )?;
1725 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1726 assert_eq!(
1727 interpreter.markers.python_version().version,
1728 Version::from_str("3.12")?
1729 );
1730 assert!(cache.bucket(CacheBucket::Interpreter).is_dir());
1731 assert_eq!(fs::read_to_string(&query_log)?, "queried\n");
1732
1733 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1734 assert_eq!(
1735 interpreter.markers.python_version().version,
1736 Version::from_str("3.12")?
1737 );
1738 assert_eq!(fs::read_to_string(&query_log)?, "queried\n");
1739
1740 let absolute = std::path::absolute(&mocked_interpreter)?;
1741 let canonical = canonicalize_executable(&absolute)?;
1742 let cache_entry = InterpreterInfo::cache_entry(&absolute, &canonical, &cache);
1743 let mut cached: CachedByTimestamp<InterpreterInfo> =
1744 rmp_serde::from_slice(&fs::read(cache_entry.path())?)?;
1745 assert_eq!(cached.timestamp, Timestamp::from_path(&canonical)?);
1746 assert_eq!(
1747 cached.data.markers.python_version().version,
1748 Version::from_str("3.12")?
1749 );
1750
1751 fs::write(
1752 &mocked_interpreter,
1753 formatdoc! {r"
1754 #!/bin/sh
1755 echo queried >> '{}'
1756 echo '{}'
1757 ", query_log.display(), json.replace("3.12", "3.13")},
1758 )?;
1759
1760 let modified = Timestamp::from_path(&canonical)?;
1763 cached.timestamp = if modified == Timestamp::from(UNIX_EPOCH) {
1764 Timestamp::from(UNIX_EPOCH + Duration::from_secs(1))
1765 } else {
1766 Timestamp::from(UNIX_EPOCH)
1767 };
1768 fs::write(cache_entry.path(), rmp_serde::to_vec(&cached)?)?;
1769 assert_ne!(cached.timestamp, Timestamp::from_path(&canonical)?);
1770
1771 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1772 assert_eq!(
1773 interpreter.markers.python_version().version,
1774 Version::from_str("3.13")?
1775 );
1776 assert_eq!(fs::read_to_string(&query_log)?, "queried\nqueried\n");
1777
1778 let refreshed: CachedByTimestamp<InterpreterInfo> =
1779 rmp_serde::from_slice(&fs::read(cache_entry.path())?)?;
1780 assert_eq!(refreshed.timestamp, Timestamp::from_path(&canonical)?);
1781 assert_eq!(
1782 refreshed.data.markers.python_version().version,
1783 Version::from_str("3.13")?
1784 );
1785
1786 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1787 assert_eq!(
1788 interpreter.markers.python_version().version,
1789 Version::from_str("3.13")?
1790 );
1791 assert_eq!(fs::read_to_string(&query_log)?, "queried\nqueried\n");
1792 Ok(())
1793 }
1794
1795 #[tokio::test]
1796 async fn test_cache_eviction_with_unchanged_executable() -> Result<()> {
1797 let mock_dir = tempdir()?;
1798 let mocked_interpreter = mock_dir.path().join("python");
1799 let response_file = mock_dir.path().join("response.json");
1800 let query_count = mock_dir.path().join("queries");
1801
1802 let mut response = serde_json::from_str::<Value>(mocked_interpreter_response())?;
1803 response["sys_executable"] = serde_json::to_value(&mocked_interpreter)?;
1804 fs::write(&response_file, serde_json::to_vec(&response)?)?;
1805 fs::write(
1806 &mocked_interpreter,
1807 formatdoc! {r#"
1808 #!/bin/sh
1809 printf '.' >> "{}"
1810 cat "{}"
1811 "#, query_count.display(), response_file.display()},
1812 )?;
1813 fs::set_permissions(
1814 &mocked_interpreter,
1815 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1816 )?;
1817
1818 let cache = Cache::temp()?.init().await?;
1819 let original_version = Version::from_str("3.12.0")?;
1820 let updated_version = Version::from_str("3.12.13")?;
1821
1822 assert_eq!(
1823 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1824 &original_version
1825 );
1826
1827 response["markers"]["implementation_version"] = "3.12.13".into();
1828 response["markers"]["python_full_version"] = "3.12.13".into();
1829 fs::write(&response_file, serde_json::to_vec(&response)?)?;
1830
1831 assert_eq!(
1832 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1833 &original_version,
1834 "an unchanged executable should retain its cached interpreter metadata"
1835 );
1836
1837 Interpreter::clear_cache(&mocked_interpreter, &cache)?;
1838 assert_eq!(
1839 fs::read_to_string(&query_count)?,
1840 ".",
1841 "clearing cached metadata should not query the interpreter"
1842 );
1843 assert_eq!(
1844 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1845 &updated_version,
1846 "clearing the cache should force the next query to run the interpreter"
1847 );
1848 assert_eq!(
1849 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1850 &updated_version,
1851 "the next query should persist the updated interpreter metadata"
1852 );
1853 assert_eq!(
1854 fs::read_to_string(&query_count)?,
1855 "..",
1856 "the updated interpreter metadata should be cached again"
1857 );
1858
1859 Ok(())
1860 }
1861}