uv_python/
interpreter.rs

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/// A Python executable and its associated platform markers.
43#[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    /// Detect the interpreter info for the given Python executable.
71    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    /// Return a new [`Interpreter`] with the given virtual environment root.
106    #[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    /// Return a new [`Interpreter`] to install into the given `--target` directory.
121    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    /// Return a new [`Interpreter`] to install into the given `--prefix` directory.
130    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    /// Return the base Python executable; that is, the Python executable that should be
139    /// considered the "base" for the virtual environment. This is typically the Python executable
140    /// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
141    /// the base Python executable is the Python executable of the interpreter's base interpreter.
142    ///
143    /// This routine relies on `sys._base_executable`, falling back to `sys.executable` if unset.
144    /// Broadly, this routine should be used when attempting to determine the "base Python
145    /// executable" in a way that is consistent with the CPython standard library, such as when
146    /// determining the `home` key for a virtual environment.
147    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    /// Determine the base Python executable; that is, the Python executable that should be
154    /// considered the "base" for the virtual environment. This is typically the Python executable
155    /// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
156    /// the base Python executable is the Python executable of the interpreter's base interpreter.
157    ///
158    /// This routine mimics the CPython `getpath.py` logic in order to make a more robust assessment
159    /// of the appropriate base Python executable. Broadly, this routine should be used when
160    /// attempting to determine the "true" base executable for a Python interpreter by resolving
161    /// symlinks until a valid Python installation is found. In particular, we tend to use this
162    /// routine for our own managed (or standalone) Python installations.
163    pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
164        let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
165        // In `python-build-standalone`, a symlinked interpreter will return its own executable path
166        // as `sys._base_executable`. Using the symlinked path as the base Python executable can be
167        // incorrect, since it could cause `home` to point to something that is _not_ a Python
168        // installation. Specifically, if the interpreter _itself_ is symlinked to an arbitrary
169        // location, we need to fully resolve it to the actual Python executable; however, if the
170        // entire standalone interpreter is symlinked, then we can use the symlinked path.
171        //
172        // We emulate CPython's `getpath.py` to ensure that the base executable results in a valid
173        // Python prefix when converted into the `home` key for `pyvenv.cfg`.
174        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    /// Returns the path to the Python virtual environment.
190    #[inline]
191    pub fn platform(&self) -> &Platform {
192        &self.platform
193    }
194
195    /// Returns the [`MarkerEnvironment`] for this Python executable.
196    #[inline]
197    pub const fn markers(&self) -> &MarkerEnvironment {
198        &self.markers
199    }
200
201    /// Return the [`ResolverMarkerEnvironment`] for this Python executable.
202    pub fn resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
203        ResolverMarkerEnvironment::from(self.markers().clone())
204    }
205
206    /// Returns the [`PythonInstallationKey`] for this interpreter.
207    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    /// Return the [`Arch`] reported by the interpreter platform tags.
234    pub fn arch(&self) -> Arch {
235        Arch::from(&self.platform().arch())
236    }
237
238    /// Return the [`Libc`] reported by the interpreter platform tags.
239    pub fn libc(&self) -> Libc {
240        Libc::from(self.platform().os())
241    }
242
243    /// Return the [`Os`] reported by the interpreter platform tags.
244    pub fn os(&self) -> Os {
245        Os::from(self.platform().os())
246    }
247
248    /// Returns the [`Tags`] for this Python executable.
249    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    /// Returns `true` if the environment is a PEP 405-compliant virtual environment.
266    ///
267    /// See: <https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/utils/virtualenv.py#L14>
268    pub fn is_virtualenv(&self) -> bool {
269        // Maybe this should return `false` if it's a target?
270        self.sys_prefix != self.sys_base_prefix
271    }
272
273    /// Returns `true` if the environment is a `--target` environment.
274    pub fn is_target(&self) -> bool {
275        self.target.is_some()
276    }
277
278    /// Returns `true` if the environment is a `--prefix` environment.
279    pub fn is_prefix(&self) -> bool {
280        self.prefix.is_some()
281    }
282
283    /// Returns `true` if this interpreter is managed by uv.
284    ///
285    /// Returns `false` if we cannot determine the path of the uv managed Python interpreters.
286    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            // During testing, we collect interpreters into an artificial search path and need to
291            // be able to mock whether an interpreter is managed or not.
292            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    /// Returns `Some` if the environment is externally managed, optionally including an error
324    /// message from the `EXTERNALLY-MANAGED` file.
325    ///
326    /// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
327    pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
328        // Per the spec, a virtual environment is never externally managed.
329        if self.is_virtualenv() {
330            return None;
331        }
332
333        // If we're installing into a target or prefix directory, it's never externally managed.
334        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            // If a file exists but is not a valid INI file, we assume the environment is
347            // externally managed.
348            return Some(ExternallyManaged::default());
349        };
350
351        let Some(section) = sections.get_mut("externally-managed") else {
352            // If the file exists but does not contain an "externally-managed" section, we assume
353            // the environment is externally managed.
354            return Some(ExternallyManaged::default());
355        };
356
357        let Some(error) = section.remove("Error") else {
358            // If the file exists but does not contain an "Error" key, we assume the environment is
359            // externally managed.
360            return Some(ExternallyManaged::default());
361        };
362
363        Some(ExternallyManaged { error })
364    }
365
366    /// Returns the `python_full_version` marker corresponding to this Python version.
367    #[inline]
368    pub fn python_full_version(&self) -> &StringVersion {
369        self.markers.python_full_version()
370    }
371
372    /// Returns the full Python version.
373    #[inline]
374    pub fn python_version(&self) -> &Version {
375        &self.markers.python_full_version().version
376    }
377
378    /// Returns the Python version up to the minor component.
379    #[inline]
380    pub fn python_minor_version(&self) -> Version {
381        Version::new(self.python_version().release().iter().take(2).copied())
382    }
383
384    /// Returns the Python version up to the patch component.
385    #[inline]
386    pub fn python_patch_version(&self) -> Version {
387        Version::new(self.python_version().release().iter().take(3).copied())
388    }
389
390    /// Return the major version component of this Python version.
391    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    /// Return the minor version component of this Python version.
397    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    /// Return the patch version component of this Python version.
403    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    /// Returns the Python version as a simple tuple, e.g., `(3, 12)`.
409    pub fn python_tuple(&self) -> (u8, u8) {
410        (self.python_major(), self.python_minor())
411    }
412
413    /// Return the major version of the implementation (e.g., `CPython` or `PyPy`).
414    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    /// Return the minor version of the implementation (e.g., `CPython` or `PyPy`).
420    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    /// Returns the implementation version as a simple tuple.
426    pub fn implementation_tuple(&self) -> (u8, u8) {
427        (self.implementation_major(), self.implementation_minor())
428    }
429
430    /// Returns the implementation name (e.g., `CPython` or `PyPy`).
431    pub fn implementation_name(&self) -> &str {
432        self.markers.implementation_name()
433    }
434
435    /// Return the `sys.base_exec_prefix` path for this Python interpreter.
436    pub fn sys_base_exec_prefix(&self) -> &Path {
437        &self.sys_base_exec_prefix
438    }
439
440    /// Return the `sys.base_prefix` path for this Python interpreter.
441    pub fn sys_base_prefix(&self) -> &Path {
442        &self.sys_base_prefix
443    }
444
445    /// Return the `sys.prefix` path for this Python interpreter.
446    pub fn sys_prefix(&self) -> &Path {
447        &self.sys_prefix
448    }
449
450    /// Return the `sys._base_executable` path for this Python interpreter. Some platforms do not
451    /// have this attribute, so it may be `None`.
452    pub fn sys_base_executable(&self) -> Option<&Path> {
453        self.sys_base_executable.as_deref()
454    }
455
456    /// Return the `sys.executable` path for this Python interpreter.
457    pub fn sys_executable(&self) -> &Path {
458        &self.sys_executable
459    }
460
461    /// Return the "real" queried executable path for this Python interpreter.
462    pub fn real_executable(&self) -> &Path {
463        &self.real_executable
464    }
465
466    /// Return the `sys.path` for this Python interpreter.
467    pub fn sys_path(&self) -> &[PathBuf] {
468        &self.sys_path
469    }
470
471    /// Return the `site.getsitepackages` for this Python interpreter.
472    ///
473    /// These are the paths Python will search for packages in at runtime. We use this for
474    /// environment layering, but not for checking for installed packages. We could use these paths
475    /// to check for installed packages, but it introduces a lot of complexity, so instead we use a
476    /// simplified version that does not respect customized site-packages. See
477    /// [`Interpreter::site_packages`].
478    pub fn runtime_site_packages(&self) -> &[PathBuf] {
479        &self.site_packages
480    }
481
482    /// Return the `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
483    pub fn stdlib(&self) -> &Path {
484        &self.stdlib
485    }
486
487    /// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
488    pub fn purelib(&self) -> &Path {
489        &self.scheme.purelib
490    }
491
492    /// Return the `platlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
493    pub fn platlib(&self) -> &Path {
494        &self.scheme.platlib
495    }
496
497    /// Return the `scripts` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
498    pub fn scripts(&self) -> &Path {
499        &self.scheme.scripts
500    }
501
502    /// Return the `data` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
503    pub fn data(&self) -> &Path {
504        &self.scheme.data
505    }
506
507    /// Return the `include` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
508    pub fn include(&self) -> &Path {
509        &self.scheme.include
510    }
511
512    /// Return the [`Scheme`] for a virtual environment created by this [`Interpreter`].
513    pub fn virtualenv(&self) -> &Scheme {
514        &self.virtualenv
515    }
516
517    /// Return whether this interpreter is `manylinux` compatible.
518    pub fn manylinux_compatible(&self) -> bool {
519        self.manylinux_compatible
520    }
521
522    /// Return the [`PointerSize`] of the Python interpreter (i.e., 32- vs. 64-bit).
523    pub fn pointer_size(&self) -> PointerSize {
524        self.pointer_size
525    }
526
527    /// Return whether this is a Python 3.13+ freethreading Python, as specified by the sysconfig var
528    /// `Py_GIL_DISABLED`.
529    ///
530    /// freethreading Python is incompatible with earlier native modules, re-introducing
531    /// abiflags with a `t` flag. <https://peps.python.org/pep-0703/#build-configuration-changes>
532    pub fn gil_disabled(&self) -> bool {
533        self.gil_disabled
534    }
535
536    /// Return whether this is a debug build of Python, as specified by the sysconfig var
537    /// `Py_DEBUG`.
538    pub fn debug_enabled(&self) -> bool {
539        self.debug_enabled
540    }
541
542    /// Return the `--target` directory for this interpreter, if any.
543    pub fn target(&self) -> Option<&Target> {
544        self.target.as_ref()
545    }
546
547    /// Return the `--prefix` directory for this interpreter, if any.
548    pub fn prefix(&self) -> Option<&Prefix> {
549        self.prefix.as_ref()
550    }
551
552    /// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter.
553    ///
554    /// This method may return false positives, but it should not return false negatives. In other
555    /// words, if this method returns `true`, the interpreter _may_ be from
556    /// `python-build-standalone`; if it returns `false`, the interpreter is definitely _not_ from
557    /// `python-build-standalone`.
558    ///
559    /// See: <https://github.com/astral-sh/python-build-standalone/issues/382>
560    #[cfg(unix)]
561    pub fn is_standalone(&self) -> bool {
562        self.standalone
563    }
564
565    /// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter.
566    // TODO(john): Replace this approach with patching sysconfig on Windows to
567    // set `PYTHON_BUILD_STANDALONE=1`.`
568    #[cfg(windows)]
569    pub fn is_standalone(&self) -> bool {
570        self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
571    }
572
573    /// Return the [`Layout`] environment used to install wheels into this interpreter.
574    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                        // If the interpreter is a venv, then the `include` directory has a different structure.
591                        // See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
592                        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    /// Returns an iterator over the `site-packages` directories inside the environment.
606    ///
607    /// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
608    /// a single element; however, in some distributions, they may be different.
609    ///
610    /// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
611    /// still deduplicate the entries, returning a single path.
612    ///
613    /// Note this does not include all runtime site-packages directories if the interpreter has been
614    /// customized. See [`Interpreter::runtime_site_packages`].
615    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    /// Check if the interpreter matches the given Python version.
645    ///
646    /// If a patch version is present, we will require an exact match.
647    /// Otherwise, just the major and minor version numbers need to match.
648    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    /// Whether or not this Python interpreter is from a default Python executable name, like
657    /// `python`, `python3`, or `python.exe`.
658    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    /// Grab a file lock for the environment to prevent concurrent writes across processes.
672    pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
673        if let Some(target) = self.target() {
674            // If we're installing into a `--target`, use a target-specific lockfile.
675            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            // Likewise, if we're installing into a `--prefix`, use a prefix-specific lockfile.
683            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            // If the environment a virtualenv, use a virtualenv-specific lockfile.
691            LockedFile::acquire(
692                self.sys_prefix.join(".lock"),
693                LockedFileMode::Exclusive,
694                self.sys_prefix.user_display(),
695            )
696            .await
697        } else {
698            // Otherwise, use a global lockfile.
699            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
709/// Calls `fs_err::canonicalize` on Unix. On Windows, avoids attempting to resolve symlinks
710/// but will resolve junctions if they are part of a trampoline target.
711pub 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/// The `EXTERNALLY-MANAGED` file in a Python installation.
733///
734/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
735#[derive(Debug, Default, Clone)]
736pub struct ExternallyManaged {
737    error: Option<String>,
738}
739
740impl ExternallyManaged {
741    /// Return the `EXTERNALLY-MANAGED` error message, if any.
742    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    /// Whether the interpreter path looks like a virtual environment.
867    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    /// Return the resolved [`InterpreterInfo`] for the given Python executable.
947    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        // Sanitize the path by (1) running under isolated mode (`-I`) to ignore any site packages
952        // modifications, and then (2) adding the path containing our query script to the front of
953        // `sys.path` so that we can import it.
954        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") // Isolated mode.
960            .arg("-B") // Don't write bytecode.
961            .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                    // These error codes are returned if the Python interpreter is a corrupt MSIX
982                    // package, which we want to differentiate from a typical spawn failure.
983                    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 the Python version is too old, we may not even be able to invoke the query script
998            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 the Python version is too old, we may not even be able to invoke the query script
1018                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    /// Duplicate the directory structure we have in `../python` into a tempdir, so we can run
1043    /// the Python probing scripts with `python -m python.get_interpreter_info` from that tempdir.
1044    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    /// A wrapper around [`markers::query_interpreter_info`] to cache the computed markers.
1077    ///
1078    /// Running a Python script is (relatively) expensive, and the markers won't change
1079    /// unless the Python executable changes, so we use the executable's last modified
1080    /// time as a cache key.
1081    pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1082        let absolute = std::path::absolute(executable)?;
1083
1084        // Provide a better error message if the link is broken or the file does not exist. Since
1085        // `canonicalize_executable` does not resolve the file on Windows, we must re-use this logic
1086        // for the subsequent metadata read as we may not have actually resolved the path.
1087        let handle_io_error = |err: io::Error| -> Error {
1088            if err.kind() == io::ErrorKind::NotFound {
1089                // Check if it looks like a venv interpreter where the underlying Python
1090                // installation was removed.
1091                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            // Shard interpreter metadata by host architecture, operating system, and version, to
1112            // invalidate the cache (e.g.) on OS upgrades.
1113            cache_digest(&(
1114                ARCH,
1115                sys_info::os_type().unwrap_or_default(),
1116                sys_info::os_release().unwrap_or_default(),
1117            )),
1118            // We use the absolute path for the cache entry to avoid cache collisions for relative
1119            // paths. But we don't want to query the executable with symbolic links resolved because
1120            // that can change reported values, e.g., `sys.executable`. We include the canonical
1121            // path in the cache entry as well, otherwise we can have cache collisions if an
1122            // absolute path refers to different interpreters with matching ctimes, e.g., if you
1123            // have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
1124            // modified at the same time.
1125            format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
1126        );
1127
1128        // We check the timestamp of the canonicalized executable to check if an underlying
1129        // interpreter has been modified.
1130        let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1131
1132        // Read from the cache.
1133        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        // Otherwise, run the Python script.
1166        trace!(
1167            "Querying interpreter executable at {}",
1168            executable.display()
1169        );
1170        let info = Self::query(executable, cache)?;
1171
1172        // If `executable` is a pyenv shim, a bash script that redirects to the activated
1173        // python executable at another path, we're not allowed to cache the interpreter info.
1174        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
1189/// Find the Python executable that should be considered the "base" for a virtual environment.
1190///
1191/// Assumes that the provided executable is that of a standalone Python interpreter.
1192///
1193/// The strategy here mimics that of `getpath.py`: we search up the ancestor path to determine
1194/// whether a given executable will convert into a valid Python prefix; if not, we resolve the
1195/// symlink and try again.
1196///
1197/// This ensures that:
1198///
1199/// 1. We avoid using symlinks to arbitrary locations as the base Python executable. For example,
1200///    if a user symlinks a Python _executable_ to `/Users/user/foo`, we want to avoid using
1201///    `/Users/user` as `home`, since it's not a Python installation, and so the relevant libraries
1202///    and headers won't be found when it's used as the executable directory.
1203///    See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L367-L400>
1204///
1205/// 2. We use the "first" resolved symlink that _is_ a valid Python prefix, and thereby preserve
1206///    symlinks. For example, if a user symlinks a Python _installation_ to `/Users/user/foo`, such
1207///    that `/Users/user/foo/bin/python` is the resulting executable, we want to use `/Users/user/foo`
1208///    as `home`, rather than resolving to the symlink target. Concretely, this allows users to
1209///    symlink patch versions (like `cpython-3.12.6-macos-aarch64-none`) to minor version aliases
1210///    (like `cpython-3.12-macos-aarch64-none`) and preserve those aliases in the resulting virtual
1211///    environments.
1212///
1213/// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L591-L594>
1214fn find_base_python(
1215    executable: &Path,
1216    major: u8,
1217    minor: u8,
1218    suffix: &str,
1219) -> Result<PathBuf, io::Error> {
1220    /// Returns `true` if `path` is the root directory.
1221    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    /// Determining whether `dir` is a valid Python prefix by searching for a "landmark".
1227    ///
1228    /// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L183>
1229    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        // Determine whether this executable will produce a valid `home` for a virtual environment.
1249        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        // If not, resolve the symlink.
1256        let resolved = fs_err::read_link(&executable)?;
1257
1258        // If the symlink is relative, resolve it relative to the executable.
1259        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        // Normalize the resolved path.
1270        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}