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 astral_ini::Options;
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#[cfg_attr(unix, expect(clippy::struct_excessive_bools))]
46#[derive(Debug, Clone)]
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 #[cfg(unix)]
61 standalone: bool,
62 tags: OnceLock<Tags>,
63 target: Option<Target>,
64 prefix: Option<Prefix>,
65 pointer_size: PointerSize,
66 gil_disabled: bool,
67 real_executable: PathBuf,
68 debug_enabled: bool,
69}
70
71impl Interpreter {
72 pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
74 let executable = executable.as_ref();
75 let info = InterpreterInfo::query_cached(executable, cache)?;
76
77 debug_assert!(
78 info.sys_executable.is_absolute(),
79 "`sys.executable` is not an absolute Python; Python installation is broken: {}",
80 info.sys_executable.display()
81 );
82
83 Ok(Self {
84 platform: info.platform,
85 markers: Box::new(info.markers),
86 scheme: info.scheme,
87 virtualenv: info.virtualenv,
88 manylinux_compatible: info.manylinux_compatible,
89 sys_prefix: info.sys_prefix,
90 pointer_size: info.pointer_size,
91 gil_disabled: info.gil_disabled,
92 debug_enabled: info.debug_enabled,
93 sys_base_prefix: info.sys_base_prefix,
94 sys_base_executable: info.sys_base_executable,
95 sys_executable: info.sys_executable,
96 site_packages: info.site_packages,
97 stdlib: info.stdlib,
98 extension_suffixes: info.extension_suffixes,
99 #[cfg(unix)]
100 standalone: info.standalone,
101 tags: OnceLock::new(),
102 target: None,
103 prefix: None,
104 real_executable: executable.to_path_buf(),
105 })
106 }
107
108 pub fn clear_cache(executable: impl AsRef<Path>, cache: &Cache) -> Result<(), Error> {
110 let absolute = std::path::absolute(executable.as_ref())?;
111 let canonical = canonicalize_executable(&absolute)?;
112 let cache_entry = InterpreterInfo::cache_entry(&absolute, &canonical, cache);
113
114 match fs::remove_file(cache_entry.path()) {
115 Ok(()) => Ok(()),
116 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
117 Err(err) => Err(err.into()),
118 }
119 }
120
121 #[must_use]
123 pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
124 Self {
125 scheme: virtualenv.scheme,
126 sys_base_executable: Some(virtualenv.base_executable),
127 sys_executable: virtualenv.executable,
128 sys_prefix: virtualenv.root,
129 target: None,
130 prefix: None,
131 site_packages: vec![],
132 ..self
133 }
134 }
135
136 pub(crate) fn with_target(self, target: Target) -> io::Result<Self> {
138 fs::create_dir_all(target.root())?;
139 Ok(Self {
140 target: Some(target),
141 ..self
142 })
143 }
144
145 pub(crate) fn with_prefix(self, prefix: Prefix) -> io::Result<Self> {
147 fs::create_dir_all(prefix.root().join(&self.virtualenv.purelib))?;
148 Ok(Self {
149 prefix: Some(prefix),
150 ..self
151 })
152 }
153
154 pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
164 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
165 let base_python = std::path::absolute(base_executable)?;
166 Ok(base_python)
167 }
168
169 pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
180 let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
181 let base_python = match find_base_python(
191 base_executable,
192 self.python_major(),
193 self.python_minor(),
194 self.variant().executable_suffix(),
195 ) {
196 Ok(path) => path,
197 Err(err) => {
198 warn!("Failed to find base Python executable: {err}");
199 canonicalize_executable(base_executable)?
200 }
201 };
202 Ok(base_python)
203 }
204
205 #[inline]
207 pub fn platform(&self) -> &Platform {
208 &self.platform
209 }
210
211 #[inline]
213 pub const fn markers(&self) -> &MarkerEnvironment {
214 &self.markers
215 }
216
217 pub fn to_resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
219 ResolverMarkerEnvironment::from(self.markers().clone())
220 }
221
222 pub fn key(&self) -> PythonInstallationKey {
224 PythonInstallationKey::new(
225 LenientImplementationName::from(self.implementation_name()),
226 self.python_major(),
227 self.python_minor(),
228 self.python_patch(),
229 self.python_version().pre(),
230 uv_platform::Platform::new(self.os(), self.arch(), self.libc()),
231 self.variant(),
232 )
233 }
234
235 pub fn variant(&self) -> PythonVariant {
236 if self.gil_disabled() {
237 if self.debug_enabled() {
238 PythonVariant::FreethreadedDebug
239 } else {
240 PythonVariant::Freethreaded
241 }
242 } else if self.debug_enabled() {
243 PythonVariant::Debug
244 } else {
245 PythonVariant::default()
246 }
247 }
248
249 pub fn arch(&self) -> Arch {
251 Arch::from(&self.platform().arch())
252 }
253
254 pub fn libc(&self) -> Libc {
256 Libc::from(self.platform().os())
257 }
258
259 pub fn os(&self) -> Os {
261 Os::from(self.platform().os())
262 }
263
264 pub fn tags(&self) -> Result<&Tags, TagsError> {
266 if self.tags.get().is_none() {
267 let tags = Tags::from_env(
268 self.platform().clone(),
269 self.python_tuple(),
270 self.implementation_name(),
271 self.implementation_tuple(),
272 TagsOptions {
273 manylinux_compatible: self.manylinux_compatible,
274 gil_disabled: self.gil_disabled,
275 debug_enabled: self.debug_enabled,
276 is_cross: false,
277 },
278 )?;
279 self.tags.set(tags).expect("tags should not be set");
280 }
281 Ok(self.tags.get().expect("tags should be set"))
282 }
283
284 pub fn is_virtualenv(&self) -> bool {
288 self.sys_prefix != self.sys_base_prefix
290 }
291
292 fn is_target(&self) -> bool {
294 self.target.is_some()
295 }
296
297 fn is_prefix(&self) -> bool {
299 self.prefix.is_some()
300 }
301
302 pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
307 if self.is_virtualenv() {
309 return None;
310 }
311
312 if self.is_target() || self.is_prefix() {
314 return None;
315 }
316
317 let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
318 return None;
319 };
320
321 let Ok(ini) = Options::default().case_sensitive(true).parse(&contents) else {
322 return Some(ExternallyManaged::default());
325 };
326
327 Some(ExternallyManaged {
328 error: ini.get("externally-managed", "Error").map(str::to_owned),
329 })
330 }
331
332 #[inline]
334 pub fn python_full_version(&self) -> &StringVersion {
335 self.markers.python_full_version()
336 }
337
338 #[inline]
340 pub fn python_version(&self) -> &Version {
341 &self.markers.python_full_version().version
342 }
343
344 #[inline]
346 pub fn python_minor_version(&self) -> Version {
347 Version::new(self.python_version().release().iter().take(2).copied())
348 }
349
350 #[inline]
352 pub(crate) fn python_patch_version(&self) -> Version {
353 Version::new(self.python_version().release().iter().take(3).copied())
354 }
355
356 pub fn python_major(&self) -> u8 {
358 let major = self.markers.python_full_version().version.release()[0];
359 u8::try_from(major).expect("invalid major version")
360 }
361
362 pub fn python_minor(&self) -> u8 {
364 let minor = self.markers.python_full_version().version.release()[1];
365 u8::try_from(minor).expect("invalid minor version")
366 }
367
368 fn python_patch(&self) -> u8 {
370 let minor = self.markers.python_full_version().version.release()[2];
371 u8::try_from(minor).expect("invalid patch version")
372 }
373
374 pub fn python_tuple(&self) -> (u8, u8) {
376 (self.python_major(), self.python_minor())
377 }
378
379 fn implementation_major(&self) -> u8 {
381 let major = self.markers.implementation_version().version.release()[0];
382 u8::try_from(major).expect("invalid major version")
383 }
384
385 fn implementation_minor(&self) -> u8 {
387 let minor = self.markers.implementation_version().version.release()[1];
388 u8::try_from(minor).expect("invalid minor version")
389 }
390
391 pub fn implementation_tuple(&self) -> (u8, u8) {
393 (self.implementation_major(), self.implementation_minor())
394 }
395
396 pub fn implementation_name(&self) -> &str {
398 self.markers.implementation_name()
399 }
400
401 pub fn sys_base_prefix(&self) -> &Path {
403 &self.sys_base_prefix
404 }
405
406 pub fn sys_prefix(&self) -> &Path {
408 &self.sys_prefix
409 }
410
411 fn sys_base_executable(&self) -> Option<&Path> {
414 self.sys_base_executable.as_deref()
415 }
416
417 pub fn sys_executable(&self) -> &Path {
419 &self.sys_executable
420 }
421
422 pub fn extension_suffixes(&self) -> &[Box<str>] {
424 &self.extension_suffixes
425 }
426
427 pub fn real_executable(&self) -> &Path {
429 &self.real_executable
430 }
431
432 pub fn runtime_site_packages(&self) -> &[PathBuf] {
440 &self.site_packages
441 }
442
443 pub fn stdlib(&self) -> &Path {
445 &self.stdlib
446 }
447
448 fn purelib(&self) -> &Path {
450 &self.scheme.purelib
451 }
452
453 fn platlib(&self) -> &Path {
455 &self.scheme.platlib
456 }
457
458 pub fn scripts(&self) -> &Path {
460 &self.scheme.scripts
461 }
462
463 fn data(&self) -> &Path {
465 &self.scheme.data
466 }
467
468 fn include(&self) -> &Path {
470 &self.scheme.include
471 }
472
473 pub fn virtualenv(&self) -> &Scheme {
475 &self.virtualenv
476 }
477
478 pub fn manylinux_compatible(&self) -> bool {
480 self.manylinux_compatible
481 }
482
483 pub fn pointer_size(&self) -> PointerSize {
485 self.pointer_size
486 }
487
488 pub fn gil_disabled(&self) -> bool {
494 self.gil_disabled
495 }
496
497 pub fn debug_enabled(&self) -> bool {
500 self.debug_enabled
501 }
502
503 fn target(&self) -> Option<&Target> {
505 self.target.as_ref()
506 }
507
508 fn prefix(&self) -> Option<&Prefix> {
510 self.prefix.as_ref()
511 }
512
513 #[cfg(unix)]
522 pub fn is_standalone(&self) -> bool {
523 self.standalone
524 }
525
526 pub fn layout(&self) -> Layout {
528 Layout {
529 python_version: self.python_tuple(),
530 sys_executable: self.sys_executable().to_path_buf(),
531 os_name: self.markers.os_name().to_string(),
532 scheme: if let Some(target) = self.target.as_ref() {
533 Scheme {
534 purelib: target.root().to_path_buf(),
535 platlib: target.root().to_path_buf(),
536 scripts: target.root().join("bin"),
537 data: target.root().to_path_buf(),
538 include: target.root().join("include"),
539 }
540 } else if let Some(prefix) = self.prefix.as_ref() {
541 Scheme {
542 purelib: prefix.root().join(&self.virtualenv.purelib),
543 platlib: prefix.root().join(&self.virtualenv.platlib),
544 scripts: prefix.root().join(&self.virtualenv.scripts),
545 data: prefix.root().join(&self.virtualenv.data),
546 include: prefix.root().join(&self.virtualenv.include),
547 }
548 } else {
549 Scheme {
550 purelib: self.purelib().to_path_buf(),
551 platlib: self.platlib().to_path_buf(),
552 scripts: self.scripts().to_path_buf(),
553 data: self.data().to_path_buf(),
554 include: if self.is_virtualenv() {
555 self.sys_prefix.join("include").join("site").join(format!(
558 "python{}.{}",
559 self.python_major(),
560 self.python_minor()
561 ))
562 } else {
563 self.include().to_path_buf()
564 },
565 }
566 },
567 }
568 }
569
570 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
581 let target = self.target().map(Target::root);
582
583 let prefix = self
584 .prefix()
585 .map(|prefix| prefix.root().join(&self.virtualenv.purelib));
586
587 let interpreter = if target.is_none() && prefix.is_none() {
588 let purelib = self.purelib();
589 let platlib = self.platlib();
590 Some(std::iter::once(purelib).chain(
591 if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
592 None
593 } else {
594 Some(platlib)
595 },
596 ))
597 } else {
598 None
599 };
600
601 target
602 .into_iter()
603 .map(Cow::Borrowed)
604 .chain(prefix.into_iter().map(Cow::Owned))
605 .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
606 }
607
608 pub fn has_default_executable_name(&self) -> bool {
611 let Some(file_name) = self.sys_executable().file_name() else {
612 return false;
613 };
614 let Some(name) = file_name.to_str() else {
615 return false;
616 };
617 VersionRequest::Default
618 .executable_names(None)
619 .into_iter()
620 .any(|default_name| name == default_name.to_string())
621 }
622
623 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
625 if let Some(target) = self.target() {
626 LockedFile::acquire(
628 target.root().join(".lock"),
629 LockedFileMode::Exclusive,
630 target.root().user_display(),
631 )
632 .await
633 } else if let Some(prefix) = self.prefix() {
634 LockedFile::acquire(
636 prefix.root().join(".lock"),
637 LockedFileMode::Exclusive,
638 prefix.root().user_display(),
639 )
640 .await
641 } else if self.is_virtualenv() {
642 LockedFile::acquire(
644 self.sys_prefix.join(".lock"),
645 LockedFileMode::Exclusive,
646 self.sys_prefix.user_display(),
647 )
648 .await
649 } else {
650 LockedFile::acquire(
652 env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
653 LockedFileMode::Exclusive,
654 self.sys_prefix.user_display(),
655 )
656 .await
657 }
658 }
659
660 fn matches_variant(&self, variant: PythonVariant) -> bool {
662 match variant {
663 PythonVariant::Default => {
664 if (self.python_major(), self.python_minor()) >= (3, 14) {
667 true
670 } else {
671 !self.gil_disabled()
674 }
675 }
676 PythonVariant::Debug => self.debug_enabled(),
677 PythonVariant::Freethreaded => self.gil_disabled(),
678 PythonVariant::FreethreadedDebug => self.gil_disabled() && self.debug_enabled(),
679 PythonVariant::Gil => !self.gil_disabled(),
680 PythonVariant::GilDebug => !self.gil_disabled() && self.debug_enabled(),
681 }
682 }
683
684 pub fn matches_implementation(&self, implementation: ImplementationName) -> bool {
686 match implementation {
687 ImplementationName::Pyodide => self.os().is_emscripten(),
688 _ => self
689 .implementation_name()
690 .eq_ignore_ascii_case(implementation.long_name()),
691 }
692 }
693
694 pub fn matches_version_request(&self, request: &VersionRequest) -> bool {
696 match request {
697 VersionRequest::Any => true,
698 VersionRequest::Default => self.matches_variant(PythonVariant::Default),
700 VersionRequest::Major(major, variant) => {
701 self.python_major() == *major && self.matches_variant(*variant)
702 }
703 VersionRequest::MajorMinor(major, minor, variant) => {
704 (self.python_major(), self.python_minor()) == (*major, *minor)
705 && self.matches_variant(*variant)
706 }
707 VersionRequest::MajorMinorPatch(major, minor, patch, variant) => {
708 (
709 self.python_major(),
710 self.python_minor(),
711 self.python_patch(),
712 ) == (*major, *minor, *patch)
713 && self.python_version().pre().is_none()
716 && self.matches_variant(*variant)
717 }
718 VersionRequest::Range(specifiers, variant) => {
719 let version = if specifiers
722 .iter()
723 .any(uv_pep440::VersionSpecifier::any_prerelease)
724 {
725 Cow::Borrowed(self.python_version())
726 } else {
727 Cow::Owned(self.python_version().only_release())
728 };
729 specifiers.contains(&version) && self.matches_variant(*variant)
730 }
731 VersionRequest::MajorMinorPrerelease(major, minor, prerelease, variant) => {
732 let version = self.python_version();
733 let Some(interpreter_prerelease) = version.pre() else {
734 return false;
735 };
736 (
737 self.python_major(),
738 self.python_minor(),
739 interpreter_prerelease,
740 ) == (*major, *minor, *prerelease)
741 && self.matches_variant(*variant)
742 }
743 VersionRequest::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
744 let version = self.python_version();
745 let Some(interpreter_prerelease) = version.pre() else {
746 return false;
747 };
748 (
749 self.python_major(),
750 self.python_minor(),
751 self.python_patch(),
752 interpreter_prerelease,
753 ) == (*major, *minor, *patch, *prerelease)
754 && self.matches_variant(*variant)
755 }
756 }
757 }
758
759 pub fn matches_download_request(&self, request: &PythonDownloadRequest) -> bool {
761 let executable = self.sys_executable().display();
762 if let Some(version) = request.version()
763 && !self.matches_version_request(version)
764 {
765 let interpreter_version = self.python_version();
766 debug!(
767 "Skipping interpreter at `{executable}`: version `{interpreter_version}` does not match request `{version}`"
768 );
769 return false;
770 }
771 let platform = request.platform();
772 let interpreter_platform = uv_platform::Platform::from(self.platform());
773 if !platform.matches(&interpreter_platform) {
774 debug!(
775 "Skipping interpreter at `{executable}`: platform `{interpreter_platform}` does not match request `{platform}`",
776 );
777 return false;
778 }
779 if let Some(implementation) = request.implementation()
780 && !self.matches_implementation(*implementation)
781 {
782 debug!(
783 "Skipping interpreter at `{executable}`: implementation `{}` does not match request `{implementation}`",
784 self.implementation_name(),
785 );
786 return false;
787 }
788 true
789 }
790
791 pub fn matches_request(&self, request: &PythonRequest, cache: &Cache) -> bool {
793 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
795 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
796 }
797
798 match request {
799 PythonRequest::Default | PythonRequest::Any => true,
800 PythonRequest::Version(version_request) => {
801 self.matches_version_request(version_request)
802 }
803 PythonRequest::Directory(directory) => {
804 is_same_executable(directory, self.sys_prefix())
806 || is_same_executable(
807 virtualenv_python_executable(directory).as_path(),
808 self.sys_executable(),
809 )
810 }
811 PythonRequest::File(file) => {
812 if is_same_executable(self.sys_executable(), file) {
814 return true;
815 }
816 if self
818 .sys_base_executable()
819 .is_some_and(|sys_base_executable| {
820 is_same_executable(sys_base_executable, file)
821 })
822 {
823 return true;
824 }
825 if cfg!(windows) {
830 if let Ok(file_interpreter) = Self::query(file, cache) {
831 if let (Some(file_base), Some(interpreter_base)) = (
832 file_interpreter.sys_base_executable(),
833 self.sys_base_executable(),
834 ) {
835 if is_same_executable(file_base, interpreter_base) {
836 return true;
837 }
838 }
839 }
840 }
841 false
842 }
843 PythonRequest::ExecutableName(name) => {
844 if self
846 .sys_executable()
847 .file_name()
848 .is_some_and(|filename| filename == name.as_str())
849 {
850 return true;
851 }
852 if self
854 .sys_base_executable()
855 .and_then(|executable| executable.file_name())
856 .is_some_and(|file_name| file_name == name.as_str())
857 {
858 return true;
859 }
860 if which(name)
863 .ok()
864 .as_ref()
865 .and_then(|executable| executable.file_name())
866 .is_some_and(|file_name| file_name == name.as_str())
867 {
868 return true;
869 }
870 false
871 }
872 PythonRequest::Implementation(implementation) => {
873 self.matches_implementation(*implementation)
874 }
875 PythonRequest::ImplementationVersion(implementation, version) => {
876 self.matches_version_request(version)
877 && self.matches_implementation(*implementation)
878 }
879 PythonRequest::Key(request) => self.matches_download_request(request),
880 }
881 }
882}
883
884pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
887 let path = path.as_ref();
888 debug_assert!(
889 path.is_absolute(),
890 "path must be absolute: {}",
891 path.display()
892 );
893
894 #[cfg(windows)]
895 {
896 if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
897 Ok(dunce::canonicalize(launcher.python_path)?)
898 } else {
899 Ok(path.to_path_buf())
900 }
901 }
902
903 #[cfg(unix)]
904 fs_err::canonicalize(path)
905}
906
907#[derive(Debug, Default, Clone)]
911pub struct ExternallyManaged {
912 error: Option<String>,
913}
914
915impl ExternallyManaged {
916 pub fn into_error(self) -> Option<String> {
918 self.error
919 }
920}
921
922#[derive(Debug, Error)]
923pub struct UnexpectedResponseError {
924 #[source]
925 err: serde_json::Error,
926 stdout: String,
927 stderr: String,
928 pub path: PathBuf,
929}
930
931impl Display for UnexpectedResponseError {
932 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
933 write!(
934 f,
935 "Querying Python at `{}` returned an invalid response: {}",
936 self.path.display(),
937 self.err
938 )?;
939
940 let mut non_empty = false;
941
942 if !self.stdout.trim().is_empty() {
943 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
944 non_empty = true;
945 }
946
947 if !self.stderr.trim().is_empty() {
948 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
949 non_empty = true;
950 }
951
952 if non_empty {
953 writeln!(f)?;
954 }
955
956 Ok(())
957 }
958}
959
960#[derive(Debug, Error)]
961pub struct StatusCodeError {
962 code: ExitStatus,
963 stdout: String,
964 stderr: String,
965 pub path: PathBuf,
966}
967
968impl Display for StatusCodeError {
969 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
970 write!(
971 f,
972 "Querying Python at `{}` failed with exit status {}",
973 self.path.display(),
974 self.code
975 )?;
976
977 let mut non_empty = false;
978
979 if !self.stdout.trim().is_empty() {
980 write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
981 non_empty = true;
982 }
983
984 if !self.stderr.trim().is_empty() {
985 write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
986 non_empty = true;
987 }
988
989 if non_empty {
990 writeln!(f)?;
991 }
992
993 Ok(())
994 }
995}
996
997#[derive(Debug, Error)]
998pub enum Error {
999 #[error("Failed to query Python interpreter")]
1000 Io(#[from] io::Error),
1001 #[error(transparent)]
1002 BrokenLink(BrokenLink),
1003 #[error("Python interpreter not found at `{0}`")]
1004 NotFound(PathBuf),
1005 #[error("Failed to query Python interpreter at `{path}`")]
1006 SpawnFailed {
1007 path: PathBuf,
1008 #[source]
1009 err: io::Error,
1010 },
1011 #[cfg(windows)]
1012 #[error("Failed to query Python interpreter at `{path}`")]
1013 CorruptWindowsPackage {
1014 path: PathBuf,
1015 #[source]
1016 err: io::Error,
1017 },
1018 #[error("Failed to query Python interpreter at `{path}`")]
1019 PermissionDenied {
1020 path: PathBuf,
1021 #[source]
1022 err: io::Error,
1023 },
1024 #[error("{0}")]
1025 UnexpectedResponse(UnexpectedResponseError),
1026 #[error("{0}")]
1027 StatusCode(StatusCodeError),
1028 #[error("Can't use Python at `{path}`")]
1029 QueryScript {
1030 #[source]
1031 err: InterpreterInfoError,
1032 path: PathBuf,
1033 },
1034 #[error("Failed to write to cache")]
1035 Encode(#[from] rmp_serde::encode::Error),
1036}
1037
1038impl uv_errors::Hinted for Error {
1039 fn hints(&self) -> uv_errors::Hints<'_> {
1040 match self {
1041 Self::BrokenLink(err) => err.hints(),
1042 _ => uv_errors::Hints::none(),
1043 }
1044 }
1045}
1046
1047#[derive(Debug, Error)]
1048pub struct BrokenLink {
1049 pub path: PathBuf,
1050 pub unix: bool,
1053 pub venv: bool,
1055}
1056
1057impl Display for BrokenLink {
1058 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1059 if self.unix {
1060 write!(
1061 f,
1062 "Broken symlink at `{}`, was the underlying Python interpreter removed?",
1063 self.path.user_display()
1064 )
1065 } else {
1066 write!(
1067 f,
1068 "Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
1069 self.path.user_display()
1070 )
1071 }
1072 }
1073}
1074
1075impl uv_errors::Hinted for BrokenLink {
1076 fn hints(&self) -> uv_errors::Hints<'_> {
1077 if self.venv {
1078 uv_errors::Hints::from(format!(
1079 "Consider recreating the environment (e.g., with `{}`)",
1080 "uv venv".green()
1081 ))
1082 } else {
1083 uv_errors::Hints::none()
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Deserialize, Serialize)]
1089#[serde(tag = "result", rename_all = "lowercase")]
1090enum InterpreterInfoResult {
1091 Error(InterpreterInfoError),
1092 Success(Box<InterpreterInfo>),
1093}
1094
1095#[derive(Debug, Error, Deserialize, Serialize)]
1096#[serde(tag = "kind", rename_all = "snake_case")]
1097pub enum InterpreterInfoError {
1098 #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
1099 LibcNotFound,
1100 #[error(
1101 "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
1102 )]
1103 BrokenMacVer,
1104 #[error("Unknown operating system: `{operating_system}`")]
1105 UnknownOperatingSystem { operating_system: String },
1106 #[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
1107 UnsupportedPythonVersion { python_version: String },
1108 #[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
1109 UnsupportedPython,
1110 #[error(
1111 "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`."
1112 )]
1113 MissingRequiredDistutils {
1114 python_major: usize,
1115 python_minor: usize,
1116 },
1117 #[error("Only Pyodide is supported for Emscripten Python")]
1118 EmscriptenNotPyodide,
1119}
1120
1121#[expect(clippy::struct_excessive_bools)]
1122#[derive(Debug, Deserialize, Serialize, Clone)]
1123struct InterpreterInfo {
1124 platform: Platform,
1125 markers: MarkerEnvironment,
1126 scheme: Scheme,
1127 virtualenv: Scheme,
1128 manylinux_compatible: bool,
1129 sys_prefix: PathBuf,
1130 sys_base_exec_prefix: PathBuf,
1131 sys_base_prefix: PathBuf,
1132 sys_base_executable: Option<PathBuf>,
1133 sys_executable: PathBuf,
1134 sys_path: Vec<PathBuf>,
1135 site_packages: Vec<PathBuf>,
1136 stdlib: PathBuf,
1137 extension_suffixes: Vec<Box<str>>,
1138 standalone: bool,
1139 pointer_size: PointerSize,
1140 gil_disabled: bool,
1141 debug_enabled: bool,
1142}
1143
1144impl InterpreterInfo {
1145 fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
1147 let tempdir = tempfile::tempdir_in(cache.root())?;
1148 Self::setup_python_query_files(tempdir.path())?;
1149
1150 let script = format!(
1158 r"import sys; sys.path = [{}] + sys.path; from python.get_interpreter_info import main; main()",
1159 tempdir.path().escape_for_python()
1160 );
1161 let mut command = Command::new(interpreter);
1162 command
1163 .arg("-I") .arg("-B") .arg("-c")
1166 .arg(script);
1167
1168 #[cfg(target_os = "macos")]
1177 command.env("SYSTEM_VERSION_COMPAT", "0");
1178
1179 let output = command.output().map_err(|err| {
1180 match err.kind() {
1181 io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
1182 io::ErrorKind::PermissionDenied => {
1183 return Error::PermissionDenied {
1184 path: interpreter.to_path_buf(),
1185 err,
1186 };
1187 }
1188 _ => {}
1189 }
1190 #[cfg(windows)]
1191 if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
1192 .raw_os_error()
1193 .and_then(|code| u32::try_from(code).ok())
1194 .map(WIN32_ERROR)
1195 {
1196 return Error::CorruptWindowsPackage {
1199 path: interpreter.to_path_buf(),
1200 err,
1201 };
1202 }
1203 Error::SpawnFailed {
1204 path: interpreter.to_path_buf(),
1205 err,
1206 }
1207 })?;
1208
1209 if !output.status.success() {
1210 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1211
1212 if python_home(interpreter).is_some_and(|home| !home.exists()) {
1218 return Err(Error::BrokenLink(BrokenLink {
1219 path: interpreter.to_path_buf(),
1220 unix: false,
1221 venv: uv_fs::is_virtualenv_executable(interpreter),
1222 }));
1223 }
1224
1225 if stderr.contains("Unknown option: -I") {
1227 return Err(Error::QueryScript {
1228 err: InterpreterInfoError::UnsupportedPython,
1229 path: interpreter.to_path_buf(),
1230 });
1231 }
1232
1233 return Err(Error::StatusCode(StatusCodeError {
1234 code: output.status,
1235 stderr,
1236 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1237 path: interpreter.to_path_buf(),
1238 }));
1239 }
1240
1241 let result: InterpreterInfoResult =
1242 serde_json::from_slice(&output.stdout).map_err(|err| {
1243 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1244
1245 if stderr.contains("Unknown option: -I") {
1247 Error::QueryScript {
1248 err: InterpreterInfoError::UnsupportedPython,
1249 path: interpreter.to_path_buf(),
1250 }
1251 } else {
1252 Error::UnexpectedResponse(UnexpectedResponseError {
1253 err,
1254 stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1255 stderr,
1256 path: interpreter.to_path_buf(),
1257 })
1258 }
1259 })?;
1260
1261 match result {
1262 InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1263 err,
1264 path: interpreter.to_path_buf(),
1265 }),
1266 InterpreterInfoResult::Success(data) => Ok(*data),
1267 }
1268 }
1269
1270 fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1273 let python_dir = root.join("python");
1274 fs_err::create_dir(&python_dir)?;
1275 fs_err::write(
1276 python_dir.join("get_interpreter_info.py"),
1277 include_str!("../python/get_interpreter_info.py"),
1278 )?;
1279 fs_err::write(
1280 python_dir.join("__init__.py"),
1281 include_str!("../python/__init__.py"),
1282 )?;
1283 let packaging_dir = python_dir.join("packaging");
1284 fs_err::create_dir(&packaging_dir)?;
1285 fs_err::write(
1286 packaging_dir.join("__init__.py"),
1287 include_str!("../python/packaging/__init__.py"),
1288 )?;
1289 fs_err::write(
1290 packaging_dir.join("_elffile.py"),
1291 include_str!("../python/packaging/_elffile.py"),
1292 )?;
1293 fs_err::write(
1294 packaging_dir.join("_manylinux.py"),
1295 include_str!("../python/packaging/_manylinux.py"),
1296 )?;
1297 fs_err::write(
1298 packaging_dir.join("_musllinux.py"),
1299 include_str!("../python/packaging/_musllinux.py"),
1300 )?;
1301 Ok(())
1302 }
1303
1304 fn cache_entry(absolute: &Path, canonical: &Path, cache: &Cache) -> CacheEntry {
1306 let python_executable = env::var_os(EnvVars::PYTHONEXECUTABLE).map(PathBuf::from);
1307 let pyvenv_launcher = env::var_os(EnvVars::PYVENV_LAUNCHER).map(PathBuf::from);
1308
1309 cache.entry(
1310 CacheBucket::Interpreter,
1311 cache_digest(&(
1314 ARCH,
1315 uv_platform::OsType::from_env()
1316 .map(|os_type| os_type.to_string())
1317 .unwrap_or_default(),
1318 uv_platform::OsRelease::from_env()
1319 .map(|os_release| os_release.to_string())
1320 .unwrap_or_default(),
1321 )),
1322 format!(
1333 "{}.msgpack",
1334 cache_digest(&(absolute, canonical, &python_executable, &pyvenv_launcher))
1335 ),
1336 )
1337 }
1338
1339 fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1345 let absolute = std::path::absolute(executable)?;
1346
1347 let handle_io_error = |err: io::Error| -> Error {
1351 if err.kind() == io::ErrorKind::NotFound {
1352 if absolute
1355 .symlink_metadata()
1356 .is_ok_and(|metadata| metadata.is_symlink())
1357 {
1358 Error::BrokenLink(BrokenLink {
1359 path: executable.to_path_buf(),
1360 unix: true,
1361 venv: uv_fs::is_virtualenv_executable(executable),
1362 })
1363 } else {
1364 Error::NotFound(executable.to_path_buf())
1365 }
1366 } else {
1367 err.into()
1368 }
1369 };
1370
1371 let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1372 let cache_entry = Self::cache_entry(&absolute, &canonical, cache);
1373
1374 let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1377
1378 if cache
1380 .freshness(&cache_entry, None, None)
1381 .is_ok_and(Freshness::is_fresh)
1382 {
1383 if let Ok(data) = fs::read(cache_entry.path()) {
1384 match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1385 Ok(cached) => {
1386 if cached.timestamp == modified {
1387 trace!(
1388 "Found cached interpreter info for Python {}, skipping query of: {}",
1389 cached.data.markers.python_full_version(),
1390 executable.user_display()
1391 );
1392 return Ok(cached.data);
1393 }
1394
1395 trace!(
1396 "Ignoring stale interpreter markers for: {}",
1397 executable.user_display()
1398 );
1399 }
1400 Err(err) => {
1401 warn!(
1402 "Broken interpreter cache entry at `{}`, removing: {err}",
1403 cache_entry.path().user_display()
1404 );
1405 let _ = fs_err::remove_file(cache_entry.path());
1406 }
1407 }
1408 }
1409 }
1410
1411 trace!(
1413 "Querying interpreter executable at `{}`",
1414 executable.display()
1415 );
1416 let info = Self::query(executable, cache)?;
1417
1418 if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1421 fs::create_dir_all(cache_entry.dir())?;
1422 write_atomic_sync(
1423 cache_entry.path(),
1424 rmp_serde::to_vec(&CachedByTimestamp {
1425 timestamp: modified,
1426 data: &info,
1427 })?,
1428 )?;
1429 }
1430
1431 Ok(info)
1432 }
1433}
1434
1435fn find_base_python(
1461 executable: &Path,
1462 major: u8,
1463 minor: u8,
1464 suffix: &str,
1465) -> Result<PathBuf, io::Error> {
1466 fn is_root(path: &Path) -> bool {
1468 let mut components = path.components();
1469 components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1470 }
1471
1472 fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1476 if cfg!(windows) {
1477 dir.join("Lib").join("os.py").is_file()
1478 } else {
1479 dir.join("lib")
1480 .join(format!("python{major}.{minor}{suffix}"))
1481 .join("os.py")
1482 .is_file()
1483 }
1484 }
1485
1486 let mut executable = Cow::Borrowed(executable);
1487
1488 loop {
1489 debug!(
1490 "Assessing Python executable as base candidate: {}",
1491 executable.display()
1492 );
1493
1494 for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1496 if is_prefix(prefix, major, minor, suffix) {
1497 return Ok(executable.into_owned());
1498 }
1499 }
1500
1501 let resolved = fs_err::read_link(&executable)?;
1503
1504 let resolved = if resolved.is_relative() {
1506 if let Some(parent) = executable.parent() {
1507 parent.join(resolved)
1508 } else {
1509 return Err(io::Error::other("Symlink has no parent directory"));
1510 }
1511 } else {
1512 resolved
1513 };
1514
1515 let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1517
1518 executable = Cow::Owned(resolved);
1519 }
1520}
1521
1522fn python_home(interpreter: &Path) -> Option<PathBuf> {
1524 let venv_root = interpreter.parent()?.parent()?;
1525 let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
1526 pyvenv_cfg.home
1527}
1528
1529#[cfg(unix)]
1530#[cfg(test)]
1531mod tests {
1532 use std::str::FromStr;
1533 use std::time::{Duration, UNIX_EPOCH};
1534
1535 use anyhow::Result;
1536 use fs_err as fs;
1537 use indoc::{formatdoc, indoc};
1538 use serde_json::Value;
1539 use tempfile::tempdir;
1540
1541 use uv_cache::{Cache, CacheBucket, CachedByTimestamp};
1542 use uv_cache_info::Timestamp;
1543 use uv_pep440::Version;
1544
1545 use crate::Interpreter;
1546 use crate::interpreter::{InterpreterInfo, canonicalize_executable};
1547
1548 fn mocked_interpreter_response() -> &'static str {
1549 indoc! {r##"
1550 {
1551 "result": "success",
1552 "platform": {
1553 "os": {
1554 "name": "manylinux",
1555 "major": 2,
1556 "minor": 38
1557 },
1558 "arch": "x86_64"
1559 },
1560 "manylinux_compatible": false,
1561 "standalone": false,
1562 "markers": {
1563 "implementation_name": "cpython",
1564 "implementation_version": "3.12.0",
1565 "os_name": "posix",
1566 "platform_machine": "x86_64",
1567 "platform_python_implementation": "CPython",
1568 "platform_release": "6.5.0-13-generic",
1569 "platform_system": "Linux",
1570 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
1571 "python_full_version": "3.12.0",
1572 "python_version": "3.12",
1573 "sys_platform": "linux"
1574 },
1575 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1576 "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1577 "sys_prefix": "/home/ferris/projects/uv/.venv",
1578 "sys_executable": "{sys_executable}",
1579 "sys_path": [
1580 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1581 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1582 ],
1583 "site_packages": [
1584 "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1585 ],
1586 "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1587 "extension_suffixes": [".cpython-312-x86_64-linux-gnu.so", ".abi3.so", ".so"],
1588 "scheme": {
1589 "data": "/home/ferris/.pyenv/versions/3.12.0",
1590 "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1591 "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1592 "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1593 "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1594 },
1595 "virtualenv": {
1596 "data": "",
1597 "include": "include",
1598 "platlib": "lib/python3.12/site-packages",
1599 "purelib": "lib/python3.12/site-packages",
1600 "scripts": "bin"
1601 },
1602 "pointer_size": "64",
1603 "gil_disabled": true,
1604 "debug_enabled": false
1605 }
1606 "##}
1607 }
1608
1609 #[tokio::test]
1610 async fn test_cache_invalidation() -> Result<()> {
1611 let mock_dir = tempdir()?;
1612 let mocked_interpreter = mock_dir.path().join("python");
1613 let query_log = mock_dir.path().join("queries");
1614 let json = mocked_interpreter_response().replace(
1615 "{sys_executable}",
1616 &mocked_interpreter.display().to_string(),
1617 );
1618
1619 let cache = Cache::temp()?.init().await?;
1620
1621 fs::write(
1622 &mocked_interpreter,
1623 formatdoc! {r"
1624 #!/bin/sh
1625 echo queried >> '{}'
1626 echo '{json}'
1627 ", query_log.display()},
1628 )?;
1629
1630 fs::set_permissions(
1631 &mocked_interpreter,
1632 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1633 )?;
1634 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1635 assert_eq!(
1636 interpreter.markers.python_version().version,
1637 Version::from_str("3.12")?
1638 );
1639 assert!(cache.bucket(CacheBucket::Interpreter).is_dir());
1640 assert_eq!(fs::read_to_string(&query_log)?, "queried\n");
1641
1642 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1643 assert_eq!(
1644 interpreter.markers.python_version().version,
1645 Version::from_str("3.12")?
1646 );
1647 assert_eq!(fs::read_to_string(&query_log)?, "queried\n");
1648
1649 let absolute = std::path::absolute(&mocked_interpreter)?;
1650 let canonical = canonicalize_executable(&absolute)?;
1651 let cache_entry = InterpreterInfo::cache_entry(&absolute, &canonical, &cache);
1652 let mut cached: CachedByTimestamp<InterpreterInfo> =
1653 rmp_serde::from_slice(&fs::read(cache_entry.path())?)?;
1654 assert_eq!(cached.timestamp, Timestamp::from_path(&canonical)?);
1655 assert_eq!(
1656 cached.data.markers.python_version().version,
1657 Version::from_str("3.12")?
1658 );
1659
1660 fs::write(
1661 &mocked_interpreter,
1662 formatdoc! {r"
1663 #!/bin/sh
1664 echo queried >> '{}'
1665 echo '{}'
1666 ", query_log.display(), json.replace("3.12", "3.13")},
1667 )?;
1668
1669 let modified = Timestamp::from_path(&canonical)?;
1672 cached.timestamp = if modified == Timestamp::from(UNIX_EPOCH) {
1673 Timestamp::from(UNIX_EPOCH + Duration::from_secs(1))
1674 } else {
1675 Timestamp::from(UNIX_EPOCH)
1676 };
1677 fs::write(cache_entry.path(), rmp_serde::to_vec(&cached)?)?;
1678 assert_ne!(cached.timestamp, Timestamp::from_path(&canonical)?);
1679
1680 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1681 assert_eq!(
1682 interpreter.markers.python_version().version,
1683 Version::from_str("3.13")?
1684 );
1685 assert_eq!(fs::read_to_string(&query_log)?, "queried\nqueried\n");
1686
1687 let refreshed: CachedByTimestamp<InterpreterInfo> =
1688 rmp_serde::from_slice(&fs::read(cache_entry.path())?)?;
1689 assert_eq!(refreshed.timestamp, Timestamp::from_path(&canonical)?);
1690 assert_eq!(
1691 refreshed.data.markers.python_version().version,
1692 Version::from_str("3.13")?
1693 );
1694
1695 let interpreter = Interpreter::query(&mocked_interpreter, &cache)?;
1696 assert_eq!(
1697 interpreter.markers.python_version().version,
1698 Version::from_str("3.13")?
1699 );
1700 assert_eq!(fs::read_to_string(&query_log)?, "queried\nqueried\n");
1701 Ok(())
1702 }
1703
1704 #[tokio::test]
1705 async fn test_cache_eviction_with_unchanged_executable() -> Result<()> {
1706 let mock_dir = tempdir()?;
1707 let mocked_interpreter = mock_dir.path().join("python");
1708 let response_file = mock_dir.path().join("response.json");
1709 let query_count = mock_dir.path().join("queries");
1710
1711 let mut response = serde_json::from_str::<Value>(mocked_interpreter_response())?;
1712 response["sys_executable"] = serde_json::to_value(&mocked_interpreter)?;
1713 fs::write(&response_file, serde_json::to_vec(&response)?)?;
1714 fs::write(
1715 &mocked_interpreter,
1716 formatdoc! {r#"
1717 #!/bin/sh
1718 printf '.' >> "{}"
1719 cat "{}"
1720 "#, query_count.display(), response_file.display()},
1721 )?;
1722 fs::set_permissions(
1723 &mocked_interpreter,
1724 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1725 )?;
1726
1727 let cache = Cache::temp()?.init().await?;
1728 let original_version = Version::from_str("3.12.0")?;
1729 let updated_version = Version::from_str("3.12.13")?;
1730
1731 assert_eq!(
1732 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1733 &original_version
1734 );
1735
1736 response["markers"]["implementation_version"] = "3.12.13".into();
1737 response["markers"]["python_full_version"] = "3.12.13".into();
1738 fs::write(&response_file, serde_json::to_vec(&response)?)?;
1739
1740 assert_eq!(
1741 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1742 &original_version,
1743 "an unchanged executable should retain its cached interpreter metadata"
1744 );
1745
1746 Interpreter::clear_cache(&mocked_interpreter, &cache)?;
1747 assert_eq!(
1748 fs::read_to_string(&query_count)?,
1749 ".",
1750 "clearing cached metadata should not query the interpreter"
1751 );
1752 assert_eq!(
1753 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1754 &updated_version,
1755 "clearing the cache should force the next query to run the interpreter"
1756 );
1757 assert_eq!(
1758 Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
1759 &updated_version,
1760 "the next query should persist the updated interpreter metadata"
1761 );
1762 assert_eq!(
1763 fs::read_to_string(&query_count)?,
1764 "..",
1765 "the updated interpreter metadata should be cached again"
1766 );
1767
1768 Ok(())
1769 }
1770}