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::{LockedFile, PythonExt, Simplified, write_atomic_sync};
22use uv_install_wheel::Layout;
23use uv_pep440::Version;
24use uv_pep508::{MarkerEnvironment, StringVersion};
25use uv_platform::{Arch, Libc, Os};
26use uv_platform_tags::{Platform, Tags, TagsError};
27use uv_pypi_types::{ResolverMarkerEnvironment, Scheme};
28
29use crate::implementation::LenientImplementationName;
30use crate::managed::ManagedPythonInstallations;
31use crate::pointer_size::PointerSize;
32use crate::{
33    Prefix, PythonInstallationKey, PythonVariant, PythonVersion, Target, VersionRequest,
34    VirtualEnvironment,
35};
36
37#[cfg(windows)]
38use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
39
40/// A Python executable and its associated platform markers.
41#[allow(clippy::struct_excessive_bools)]
42#[derive(Debug, Clone)]
43pub struct Interpreter {
44    platform: Platform,
45    markers: Box<MarkerEnvironment>,
46    scheme: Scheme,
47    virtualenv: Scheme,
48    manylinux_compatible: bool,
49    sys_prefix: PathBuf,
50    sys_base_exec_prefix: PathBuf,
51    sys_base_prefix: PathBuf,
52    sys_base_executable: Option<PathBuf>,
53    sys_executable: PathBuf,
54    sys_path: Vec<PathBuf>,
55    site_packages: Vec<PathBuf>,
56    stdlib: PathBuf,
57    standalone: bool,
58    tags: OnceLock<Tags>,
59    target: Option<Target>,
60    prefix: Option<Prefix>,
61    pointer_size: PointerSize,
62    gil_disabled: bool,
63    real_executable: PathBuf,
64    debug_enabled: bool,
65}
66
67impl Interpreter {
68    /// Detect the interpreter info for the given Python executable.
69    pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
70        let info = InterpreterInfo::query_cached(executable.as_ref(), cache)?;
71
72        debug_assert!(
73            info.sys_executable.is_absolute(),
74            "`sys.executable` is not an absolute Python; Python installation is broken: {}",
75            info.sys_executable.display()
76        );
77
78        Ok(Self {
79            platform: info.platform,
80            markers: Box::new(info.markers),
81            scheme: info.scheme,
82            virtualenv: info.virtualenv,
83            manylinux_compatible: info.manylinux_compatible,
84            sys_prefix: info.sys_prefix,
85            sys_base_exec_prefix: info.sys_base_exec_prefix,
86            pointer_size: info.pointer_size,
87            gil_disabled: info.gil_disabled,
88            debug_enabled: info.debug_enabled,
89            sys_base_prefix: info.sys_base_prefix,
90            sys_base_executable: info.sys_base_executable,
91            sys_executable: info.sys_executable,
92            sys_path: info.sys_path,
93            site_packages: info.site_packages,
94            stdlib: info.stdlib,
95            standalone: info.standalone,
96            tags: OnceLock::new(),
97            target: None,
98            prefix: None,
99            real_executable: executable.as_ref().to_path_buf(),
100        })
101    }
102
103    /// Return a new [`Interpreter`] with the given virtual environment root.
104    #[must_use]
105    pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
106        Self {
107            scheme: virtualenv.scheme,
108            sys_base_executable: Some(virtualenv.base_executable),
109            sys_executable: virtualenv.executable,
110            sys_prefix: virtualenv.root,
111            target: None,
112            prefix: None,
113            site_packages: vec![],
114            ..self
115        }
116    }
117
118    /// Return a new [`Interpreter`] to install into the given `--target` directory.
119    pub fn with_target(self, target: Target) -> io::Result<Self> {
120        target.init()?;
121        Ok(Self {
122            target: Some(target),
123            ..self
124        })
125    }
126
127    /// Return a new [`Interpreter`] to install into the given `--prefix` directory.
128    pub fn with_prefix(self, prefix: Prefix) -> io::Result<Self> {
129        prefix.init(self.virtualenv())?;
130        Ok(Self {
131            prefix: Some(prefix),
132            ..self
133        })
134    }
135
136    /// Return the base Python executable; that is, the Python executable that should be
137    /// considered the "base" for the virtual environment. This is typically the Python executable
138    /// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
139    /// the base Python executable is the Python executable of the interpreter's base interpreter.
140    ///
141    /// This routine relies on `sys._base_executable`, falling back to `sys.executable` if unset.
142    /// Broadly, this routine should be used when attempting to determine the "base Python
143    /// executable" in a way that is consistent with the CPython standard library, such as when
144    /// determining the `home` key for a virtual environment.
145    pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
146        let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
147        let base_python = std::path::absolute(base_executable)?;
148        Ok(base_python)
149    }
150
151    /// Determine the base Python executable; that is, the Python executable that should be
152    /// considered the "base" for the virtual environment. This is typically the Python executable
153    /// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
154    /// the base Python executable is the Python executable of the interpreter's base interpreter.
155    ///
156    /// This routine mimics the CPython `getpath.py` logic in order to make a more robust assessment
157    /// of the appropriate base Python executable. Broadly, this routine should be used when
158    /// attempting to determine the "true" base executable for a Python interpreter by resolving
159    /// symlinks until a valid Python installation is found. In particular, we tend to use this
160    /// routine for our own managed (or standalone) Python installations.
161    pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
162        let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
163        // In `python-build-standalone`, a symlinked interpreter will return its own executable path
164        // as `sys._base_executable`. Using the symlinked path as the base Python executable can be
165        // incorrect, since it could cause `home` to point to something that is _not_ a Python
166        // installation. Specifically, if the interpreter _itself_ is symlinked to an arbitrary
167        // location, we need to fully resolve it to the actual Python executable; however, if the
168        // entire standalone interpreter is symlinked, then we can use the symlinked path.
169        //
170        // We emulate CPython's `getpath.py` to ensure that the base executable results in a valid
171        // Python prefix when converted into the `home` key for `pyvenv.cfg`.
172        let base_python = match find_base_python(
173            base_executable,
174            self.python_major(),
175            self.python_minor(),
176            self.variant().executable_suffix(),
177        ) {
178            Ok(path) => path,
179            Err(err) => {
180                warn!("Failed to find base Python executable: {err}");
181                canonicalize_executable(base_executable)?
182            }
183        };
184        Ok(base_python)
185    }
186
187    /// Returns the path to the Python virtual environment.
188    #[inline]
189    pub fn platform(&self) -> &Platform {
190        &self.platform
191    }
192
193    /// Returns the [`MarkerEnvironment`] for this Python executable.
194    #[inline]
195    pub const fn markers(&self) -> &MarkerEnvironment {
196        &self.markers
197    }
198
199    /// Return the [`ResolverMarkerEnvironment`] for this Python executable.
200    pub fn resolver_marker_environment(&self) -> ResolverMarkerEnvironment {
201        ResolverMarkerEnvironment::from(self.markers().clone())
202    }
203
204    /// Returns the [`PythonInstallationKey`] for this interpreter.
205    pub fn key(&self) -> PythonInstallationKey {
206        PythonInstallationKey::new(
207            LenientImplementationName::from(self.implementation_name()),
208            self.python_major(),
209            self.python_minor(),
210            self.python_patch(),
211            self.python_version().pre(),
212            uv_platform::Platform::new(self.os(), self.arch(), self.libc()),
213            self.variant(),
214        )
215    }
216
217    pub fn variant(&self) -> PythonVariant {
218        if self.gil_disabled() {
219            if self.debug_enabled() {
220                PythonVariant::FreethreadedDebug
221            } else {
222                PythonVariant::Freethreaded
223            }
224        } else if self.debug_enabled() {
225            PythonVariant::Debug
226        } else {
227            PythonVariant::default()
228        }
229    }
230
231    /// Return the [`Arch`] reported by the interpreter platform tags.
232    pub fn arch(&self) -> Arch {
233        Arch::from(&self.platform().arch())
234    }
235
236    /// Return the [`Libc`] reported by the interpreter platform tags.
237    pub fn libc(&self) -> Libc {
238        Libc::from(self.platform().os())
239    }
240
241    /// Return the [`Os`] reported by the interpreter platform tags.
242    pub fn os(&self) -> Os {
243        Os::from(self.platform().os())
244    }
245
246    /// Returns the [`Tags`] for this Python executable.
247    pub fn tags(&self) -> Result<&Tags, TagsError> {
248        if self.tags.get().is_none() {
249            let tags = Tags::from_env(
250                self.platform(),
251                self.python_tuple(),
252                self.implementation_name(),
253                self.implementation_tuple(),
254                self.manylinux_compatible,
255                self.gil_disabled,
256            )?;
257            self.tags.set(tags).expect("tags should not be set");
258        }
259        Ok(self.tags.get().expect("tags should be set"))
260    }
261
262    /// Returns `true` if the environment is a PEP 405-compliant virtual environment.
263    ///
264    /// See: <https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/utils/virtualenv.py#L14>
265    pub fn is_virtualenv(&self) -> bool {
266        // Maybe this should return `false` if it's a target?
267        self.sys_prefix != self.sys_base_prefix
268    }
269
270    /// Returns `true` if the environment is a `--target` environment.
271    pub fn is_target(&self) -> bool {
272        self.target.is_some()
273    }
274
275    /// Returns `true` if the environment is a `--prefix` environment.
276    pub fn is_prefix(&self) -> bool {
277        self.prefix.is_some()
278    }
279
280    /// Returns `true` if this interpreter is managed by uv.
281    ///
282    /// Returns `false` if we cannot determine the path of the uv managed Python interpreters.
283    pub fn is_managed(&self) -> bool {
284        if let Ok(test_managed) =
285            std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED)
286        {
287            // During testing, we collect interpreters into an artificial search path and need to
288            // be able to mock whether an interpreter is managed or not.
289            return test_managed.split_ascii_whitespace().any(|item| {
290                let version = <PythonVersion as std::str::FromStr>::from_str(item).expect(
291                    "`UV_INTERNAL__TEST_PYTHON_MANAGED` items should be valid Python versions",
292                );
293                if version.patch().is_some() {
294                    version.version() == self.python_version()
295                } else {
296                    (version.major(), version.minor()) == self.python_tuple()
297                }
298            });
299        }
300
301        let Ok(installations) = ManagedPythonInstallations::from_settings(None) else {
302            return false;
303        };
304
305        let Ok(suffix) = self.sys_base_prefix.strip_prefix(installations.root()) else {
306            return false;
307        };
308
309        let Some(first_component) = suffix.components().next() else {
310            return false;
311        };
312
313        let Some(name) = first_component.as_os_str().to_str() else {
314            return false;
315        };
316
317        PythonInstallationKey::from_str(name).is_ok()
318    }
319
320    /// Returns `Some` if the environment is externally managed, optionally including an error
321    /// message from the `EXTERNALLY-MANAGED` file.
322    ///
323    /// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
324    pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
325        // Per the spec, a virtual environment is never externally managed.
326        if self.is_virtualenv() {
327            return None;
328        }
329
330        // If we're installing into a target or prefix directory, it's never externally managed.
331        if self.is_target() || self.is_prefix() {
332            return None;
333        }
334
335        let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
336            return None;
337        };
338
339        let mut ini = Ini::new_cs();
340        ini.set_multiline(true);
341
342        let Ok(mut sections) = ini.read(contents) else {
343            // If a file exists but is not a valid INI file, we assume the environment is
344            // externally managed.
345            return Some(ExternallyManaged::default());
346        };
347
348        let Some(section) = sections.get_mut("externally-managed") else {
349            // If the file exists but does not contain an "externally-managed" section, we assume
350            // the environment is externally managed.
351            return Some(ExternallyManaged::default());
352        };
353
354        let Some(error) = section.remove("Error") else {
355            // If the file exists but does not contain an "Error" key, we assume the environment is
356            // externally managed.
357            return Some(ExternallyManaged::default());
358        };
359
360        Some(ExternallyManaged { error })
361    }
362
363    /// Returns the `python_full_version` marker corresponding to this Python version.
364    #[inline]
365    pub fn python_full_version(&self) -> &StringVersion {
366        self.markers.python_full_version()
367    }
368
369    /// Returns the full Python version.
370    #[inline]
371    pub fn python_version(&self) -> &Version {
372        &self.markers.python_full_version().version
373    }
374
375    /// Returns the Python version up to the minor component.
376    #[inline]
377    pub fn python_minor_version(&self) -> Version {
378        Version::new(self.python_version().release().iter().take(2).copied())
379    }
380
381    /// Returns the Python version up to the patch component.
382    #[inline]
383    pub fn python_patch_version(&self) -> Version {
384        Version::new(self.python_version().release().iter().take(3).copied())
385    }
386
387    /// Return the major version component of this Python version.
388    pub fn python_major(&self) -> u8 {
389        let major = self.markers.python_full_version().version.release()[0];
390        u8::try_from(major).expect("invalid major version")
391    }
392
393    /// Return the minor version component of this Python version.
394    pub fn python_minor(&self) -> u8 {
395        let minor = self.markers.python_full_version().version.release()[1];
396        u8::try_from(minor).expect("invalid minor version")
397    }
398
399    /// Return the patch version component of this Python version.
400    pub fn python_patch(&self) -> u8 {
401        let minor = self.markers.python_full_version().version.release()[2];
402        u8::try_from(minor).expect("invalid patch version")
403    }
404
405    /// Returns the Python version as a simple tuple, e.g., `(3, 12)`.
406    pub fn python_tuple(&self) -> (u8, u8) {
407        (self.python_major(), self.python_minor())
408    }
409
410    /// Return the major version of the implementation (e.g., `CPython` or `PyPy`).
411    pub fn implementation_major(&self) -> u8 {
412        let major = self.markers.implementation_version().version.release()[0];
413        u8::try_from(major).expect("invalid major version")
414    }
415
416    /// Return the minor version of the implementation (e.g., `CPython` or `PyPy`).
417    pub fn implementation_minor(&self) -> u8 {
418        let minor = self.markers.implementation_version().version.release()[1];
419        u8::try_from(minor).expect("invalid minor version")
420    }
421
422    /// Returns the implementation version as a simple tuple.
423    pub fn implementation_tuple(&self) -> (u8, u8) {
424        (self.implementation_major(), self.implementation_minor())
425    }
426
427    /// Returns the implementation name (e.g., `CPython` or `PyPy`).
428    pub fn implementation_name(&self) -> &str {
429        self.markers.implementation_name()
430    }
431
432    /// Return the `sys.base_exec_prefix` path for this Python interpreter.
433    pub fn sys_base_exec_prefix(&self) -> &Path {
434        &self.sys_base_exec_prefix
435    }
436
437    /// Return the `sys.base_prefix` path for this Python interpreter.
438    pub fn sys_base_prefix(&self) -> &Path {
439        &self.sys_base_prefix
440    }
441
442    /// Return the `sys.prefix` path for this Python interpreter.
443    pub fn sys_prefix(&self) -> &Path {
444        &self.sys_prefix
445    }
446
447    /// Return the `sys._base_executable` path for this Python interpreter. Some platforms do not
448    /// have this attribute, so it may be `None`.
449    pub fn sys_base_executable(&self) -> Option<&Path> {
450        self.sys_base_executable.as_deref()
451    }
452
453    /// Return the `sys.executable` path for this Python interpreter.
454    pub fn sys_executable(&self) -> &Path {
455        &self.sys_executable
456    }
457
458    /// Return the "real" queried executable path for this Python interpreter.
459    pub fn real_executable(&self) -> &Path {
460        &self.real_executable
461    }
462
463    /// Return the `sys.path` for this Python interpreter.
464    pub fn sys_path(&self) -> &[PathBuf] {
465        &self.sys_path
466    }
467
468    /// Return the `site.getsitepackages` for this Python interpreter.
469    ///
470    /// These are the paths Python will search for packages in at runtime. We use this for
471    /// environment layering, but not for checking for installed packages. We could use these paths
472    /// to check for installed packages, but it introduces a lot of complexity, so instead we use a
473    /// simplified version that does not respect customized site-packages. See
474    /// [`Interpreter::site_packages`].
475    pub fn runtime_site_packages(&self) -> &[PathBuf] {
476        &self.site_packages
477    }
478
479    /// Return the `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
480    pub fn stdlib(&self) -> &Path {
481        &self.stdlib
482    }
483
484    /// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
485    pub fn purelib(&self) -> &Path {
486        &self.scheme.purelib
487    }
488
489    /// Return the `platlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
490    pub fn platlib(&self) -> &Path {
491        &self.scheme.platlib
492    }
493
494    /// Return the `scripts` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
495    pub fn scripts(&self) -> &Path {
496        &self.scheme.scripts
497    }
498
499    /// Return the `data` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
500    pub fn data(&self) -> &Path {
501        &self.scheme.data
502    }
503
504    /// Return the `include` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
505    pub fn include(&self) -> &Path {
506        &self.scheme.include
507    }
508
509    /// Return the [`Scheme`] for a virtual environment created by this [`Interpreter`].
510    pub fn virtualenv(&self) -> &Scheme {
511        &self.virtualenv
512    }
513
514    /// Return whether this interpreter is `manylinux` compatible.
515    pub fn manylinux_compatible(&self) -> bool {
516        self.manylinux_compatible
517    }
518
519    /// Return the [`PointerSize`] of the Python interpreter (i.e., 32- vs. 64-bit).
520    pub fn pointer_size(&self) -> PointerSize {
521        self.pointer_size
522    }
523
524    /// Return whether this is a Python 3.13+ freethreading Python, as specified by the sysconfig var
525    /// `Py_GIL_DISABLED`.
526    ///
527    /// freethreading Python is incompatible with earlier native modules, re-introducing
528    /// abiflags with a `t` flag. <https://peps.python.org/pep-0703/#build-configuration-changes>
529    pub fn gil_disabled(&self) -> bool {
530        self.gil_disabled
531    }
532
533    /// Return whether this is a debug build of Python, as specified by the sysconfig var
534    /// `Py_DEBUG`.
535    pub fn debug_enabled(&self) -> bool {
536        self.debug_enabled
537    }
538
539    /// Return the `--target` directory for this interpreter, if any.
540    pub fn target(&self) -> Option<&Target> {
541        self.target.as_ref()
542    }
543
544    /// Return the `--prefix` directory for this interpreter, if any.
545    pub fn prefix(&self) -> Option<&Prefix> {
546        self.prefix.as_ref()
547    }
548
549    /// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter.
550    ///
551    /// This method may return false positives, but it should not return false negatives. In other
552    /// words, if this method returns `true`, the interpreter _may_ be from
553    /// `python-build-standalone`; if it returns `false`, the interpreter is definitely _not_ from
554    /// `python-build-standalone`.
555    ///
556    /// See: <https://github.com/astral-sh/python-build-standalone/issues/382>
557    #[cfg(unix)]
558    pub fn is_standalone(&self) -> bool {
559        self.standalone
560    }
561
562    /// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter.
563    // TODO(john): Replace this approach with patching sysconfig on Windows to
564    // set `PYTHON_BUILD_STANDALONE=1`.`
565    #[cfg(windows)]
566    pub fn is_standalone(&self) -> bool {
567        self.standalone || (self.is_managed() && self.markers().implementation_name() == "cpython")
568    }
569
570    /// Return the [`Layout`] environment used to install wheels into this interpreter.
571    pub fn layout(&self) -> Layout {
572        Layout {
573            python_version: self.python_tuple(),
574            sys_executable: self.sys_executable().to_path_buf(),
575            os_name: self.markers.os_name().to_string(),
576            scheme: if let Some(target) = self.target.as_ref() {
577                target.scheme()
578            } else if let Some(prefix) = self.prefix.as_ref() {
579                prefix.scheme(&self.virtualenv)
580            } else {
581                Scheme {
582                    purelib: self.purelib().to_path_buf(),
583                    platlib: self.platlib().to_path_buf(),
584                    scripts: self.scripts().to_path_buf(),
585                    data: self.data().to_path_buf(),
586                    include: if self.is_virtualenv() {
587                        // If the interpreter is a venv, then the `include` directory has a different structure.
588                        // See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
589                        self.sys_prefix.join("include").join("site").join(format!(
590                            "python{}.{}",
591                            self.python_major(),
592                            self.python_minor()
593                        ))
594                    } else {
595                        self.include().to_path_buf()
596                    },
597                }
598            },
599        }
600    }
601
602    /// Returns an iterator over the `site-packages` directories inside the environment.
603    ///
604    /// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
605    /// a single element; however, in some distributions, they may be different.
606    ///
607    /// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
608    /// still deduplicate the entries, returning a single path.
609    ///
610    /// Note this does not include all runtime site-packages directories if the interpreter has been
611    /// customized. See [`Interpreter::runtime_site_packages`].
612    pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
613        let target = self.target().map(Target::site_packages);
614
615        let prefix = self
616            .prefix()
617            .map(|prefix| prefix.site_packages(self.virtualenv()));
618
619        let interpreter = if target.is_none() && prefix.is_none() {
620            let purelib = self.purelib();
621            let platlib = self.platlib();
622            Some(std::iter::once(purelib).chain(
623                if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
624                    None
625                } else {
626                    Some(platlib)
627                },
628            ))
629        } else {
630            None
631        };
632
633        target
634            .into_iter()
635            .flatten()
636            .map(Cow::Borrowed)
637            .chain(prefix.into_iter().flatten().map(Cow::Owned))
638            .chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
639    }
640
641    /// Check if the interpreter matches the given Python version.
642    ///
643    /// If a patch version is present, we will require an exact match.
644    /// Otherwise, just the major and minor version numbers need to match.
645    pub fn satisfies(&self, version: &PythonVersion) -> bool {
646        if version.patch().is_some() {
647            version.version() == self.python_version()
648        } else {
649            (version.major(), version.minor()) == self.python_tuple()
650        }
651    }
652
653    /// Whether or not this Python interpreter is from a default Python executable name, like
654    /// `python`, `python3`, or `python.exe`.
655    pub(crate) fn has_default_executable_name(&self) -> bool {
656        let Some(file_name) = self.sys_executable().file_name() else {
657            return false;
658        };
659        let Some(name) = file_name.to_str() else {
660            return false;
661        };
662        VersionRequest::Default
663            .executable_names(None)
664            .into_iter()
665            .any(|default_name| name == default_name.to_string())
666    }
667
668    /// Grab a file lock for the environment to prevent concurrent writes across processes.
669    pub async fn lock(&self) -> Result<LockedFile, io::Error> {
670        if let Some(target) = self.target() {
671            // If we're installing into a `--target`, use a target-specific lockfile.
672            LockedFile::acquire(target.root().join(".lock"), target.root().user_display()).await
673        } else if let Some(prefix) = self.prefix() {
674            // Likewise, if we're installing into a `--prefix`, use a prefix-specific lockfile.
675            LockedFile::acquire(prefix.root().join(".lock"), prefix.root().user_display()).await
676        } else if self.is_virtualenv() {
677            // If the environment a virtualenv, use a virtualenv-specific lockfile.
678            LockedFile::acquire(
679                self.sys_prefix.join(".lock"),
680                self.sys_prefix.user_display(),
681            )
682            .await
683        } else {
684            // Otherwise, use a global lockfile.
685            LockedFile::acquire(
686                env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
687                self.sys_prefix.user_display(),
688            )
689            .await
690        }
691    }
692}
693
694/// Calls `fs_err::canonicalize` on Unix. On Windows, avoids attempting to resolve symlinks
695/// but will resolve junctions if they are part of a trampoline target.
696pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
697    let path = path.as_ref();
698    debug_assert!(
699        path.is_absolute(),
700        "path must be absolute: {}",
701        path.display()
702    );
703
704    #[cfg(windows)]
705    {
706        if let Ok(Some(launcher)) = uv_trampoline_builder::Launcher::try_from_path(path) {
707            Ok(dunce::canonicalize(launcher.python_path)?)
708        } else {
709            Ok(path.to_path_buf())
710        }
711    }
712
713    #[cfg(unix)]
714    fs_err::canonicalize(path)
715}
716
717/// The `EXTERNALLY-MANAGED` file in a Python installation.
718///
719/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
720#[derive(Debug, Default, Clone)]
721pub struct ExternallyManaged {
722    error: Option<String>,
723}
724
725impl ExternallyManaged {
726    /// Return the `EXTERNALLY-MANAGED` error message, if any.
727    pub fn into_error(self) -> Option<String> {
728        self.error
729    }
730}
731
732#[derive(Debug, Error)]
733pub struct UnexpectedResponseError {
734    #[source]
735    pub(super) err: serde_json::Error,
736    pub(super) stdout: String,
737    pub(super) stderr: String,
738    pub(super) path: PathBuf,
739}
740
741impl Display for UnexpectedResponseError {
742    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
743        write!(
744            f,
745            "Querying Python at `{}` returned an invalid response: {}",
746            self.path.display(),
747            self.err
748        )?;
749
750        let mut non_empty = false;
751
752        if !self.stdout.trim().is_empty() {
753            write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
754            non_empty = true;
755        }
756
757        if !self.stderr.trim().is_empty() {
758            write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
759            non_empty = true;
760        }
761
762        if non_empty {
763            writeln!(f)?;
764        }
765
766        Ok(())
767    }
768}
769
770#[derive(Debug, Error)]
771pub struct StatusCodeError {
772    pub(super) code: ExitStatus,
773    pub(super) stdout: String,
774    pub(super) stderr: String,
775    pub(super) path: PathBuf,
776}
777
778impl Display for StatusCodeError {
779    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
780        write!(
781            f,
782            "Querying Python at `{}` failed with exit status {}",
783            self.path.display(),
784            self.code
785        )?;
786
787        let mut non_empty = false;
788
789        if !self.stdout.trim().is_empty() {
790            write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout)?;
791            non_empty = true;
792        }
793
794        if !self.stderr.trim().is_empty() {
795            write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr)?;
796            non_empty = true;
797        }
798
799        if non_empty {
800            writeln!(f)?;
801        }
802
803        Ok(())
804    }
805}
806
807#[derive(Debug, Error)]
808pub enum Error {
809    #[error("Failed to query Python interpreter")]
810    Io(#[from] io::Error),
811    #[error(transparent)]
812    BrokenSymlink(BrokenSymlink),
813    #[error("Python interpreter not found at `{0}`")]
814    NotFound(PathBuf),
815    #[error("Failed to query Python interpreter at `{path}`")]
816    SpawnFailed {
817        path: PathBuf,
818        #[source]
819        err: io::Error,
820    },
821    #[cfg(windows)]
822    #[error("Failed to query Python interpreter at `{path}`")]
823    CorruptWindowsPackage {
824        path: PathBuf,
825        #[source]
826        err: io::Error,
827    },
828    #[error("Failed to query Python interpreter at `{path}`")]
829    PermissionDenied {
830        path: PathBuf,
831        #[source]
832        err: io::Error,
833    },
834    #[error("{0}")]
835    UnexpectedResponse(UnexpectedResponseError),
836    #[error("{0}")]
837    StatusCode(StatusCodeError),
838    #[error("Can't use Python at `{path}`")]
839    QueryScript {
840        #[source]
841        err: InterpreterInfoError,
842        path: PathBuf,
843    },
844    #[error("Failed to write to cache")]
845    Encode(#[from] rmp_serde::encode::Error),
846}
847
848#[derive(Debug, Error)]
849pub struct BrokenSymlink {
850    pub path: PathBuf,
851    /// Whether the interpreter path looks like a virtual environment.
852    pub venv: bool,
853}
854
855impl Display for BrokenSymlink {
856    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
857        write!(
858            f,
859            "Broken symlink at `{}`, was the underlying Python interpreter removed?",
860            self.path.user_display()
861        )?;
862        if self.venv {
863            write!(
864                f,
865                "\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
866                "hint".bold().cyan(),
867                ":".bold(),
868                "uv venv".green()
869            )?;
870        }
871        Ok(())
872    }
873}
874
875#[derive(Debug, Deserialize, Serialize)]
876#[serde(tag = "result", rename_all = "lowercase")]
877enum InterpreterInfoResult {
878    Error(InterpreterInfoError),
879    Success(Box<InterpreterInfo>),
880}
881
882#[derive(Debug, Error, Deserialize, Serialize)]
883#[serde(tag = "kind", rename_all = "snake_case")]
884pub enum InterpreterInfoError {
885    #[error("Could not detect a glibc or a musl libc (while running on Linux)")]
886    LibcNotFound,
887    #[error(
888        "Broken Python installation, `platform.mac_ver()` returned an empty value, please reinstall Python"
889    )]
890    BrokenMacVer,
891    #[error("Unknown operating system: `{operating_system}`")]
892    UnknownOperatingSystem { operating_system: String },
893    #[error("Python {python_version} is not supported. Please use Python 3.8 or newer.")]
894    UnsupportedPythonVersion { python_version: String },
895    #[error("Python executable does not support `-I` flag. Please use Python 3.8 or newer.")]
896    UnsupportedPython,
897    #[error(
898        "Python installation is missing `distutils`, which is required for packaging on older Python versions. Your system may package it separately, e.g., as `python{python_major}-distutils` or `python{python_major}.{python_minor}-distutils`."
899    )]
900    MissingRequiredDistutils {
901        python_major: usize,
902        python_minor: usize,
903    },
904    #[error("Only Pyodide is support for Emscripten Python")]
905    EmscriptenNotPyodide,
906}
907
908#[allow(clippy::struct_excessive_bools)]
909#[derive(Debug, Deserialize, Serialize, Clone)]
910struct InterpreterInfo {
911    platform: Platform,
912    markers: MarkerEnvironment,
913    scheme: Scheme,
914    virtualenv: Scheme,
915    manylinux_compatible: bool,
916    sys_prefix: PathBuf,
917    sys_base_exec_prefix: PathBuf,
918    sys_base_prefix: PathBuf,
919    sys_base_executable: Option<PathBuf>,
920    sys_executable: PathBuf,
921    sys_path: Vec<PathBuf>,
922    site_packages: Vec<PathBuf>,
923    stdlib: PathBuf,
924    standalone: bool,
925    pointer_size: PointerSize,
926    gil_disabled: bool,
927    debug_enabled: bool,
928}
929
930impl InterpreterInfo {
931    /// Return the resolved [`InterpreterInfo`] for the given Python executable.
932    pub(crate) fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
933        let tempdir = tempfile::tempdir_in(cache.root())?;
934        Self::setup_python_query_files(tempdir.path())?;
935
936        // Sanitize the path by (1) running under isolated mode (`-I`) to ignore any site packages
937        // modifications, and then (2) adding the path containing our query script to the front of
938        // `sys.path` so that we can import it.
939        let script = format!(
940            r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
941            tempdir.path().escape_for_python()
942        );
943        let output = Command::new(interpreter)
944            .arg("-I") // Isolated mode.
945            .arg("-B") // Don't write bytecode.
946            .arg("-c")
947            .arg(script)
948            .output()
949            .map_err(|err| {
950                match err.kind() {
951                    io::ErrorKind::NotFound => return Error::NotFound(interpreter.to_path_buf()),
952                    io::ErrorKind::PermissionDenied => {
953                        return Error::PermissionDenied {
954                            path: interpreter.to_path_buf(),
955                            err,
956                        };
957                    }
958                    _ => {}
959                }
960                #[cfg(windows)]
961                if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
962                    .raw_os_error()
963                    .and_then(|code| u32::try_from(code).ok())
964                    .map(WIN32_ERROR)
965                {
966                    // These error codes are returned if the Python interpreter is a corrupt MSIX
967                    // package, which we want to differentiate from a typical spawn failure.
968                    return Error::CorruptWindowsPackage {
969                        path: interpreter.to_path_buf(),
970                        err,
971                    };
972                }
973                Error::SpawnFailed {
974                    path: interpreter.to_path_buf(),
975                    err,
976                }
977            })?;
978
979        if !output.status.success() {
980            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
981
982            // If the Python version is too old, we may not even be able to invoke the query script
983            if stderr.contains("Unknown option: -I") {
984                return Err(Error::QueryScript {
985                    err: InterpreterInfoError::UnsupportedPython,
986                    path: interpreter.to_path_buf(),
987                });
988            }
989
990            return Err(Error::StatusCode(StatusCodeError {
991                code: output.status,
992                stderr,
993                stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
994                path: interpreter.to_path_buf(),
995            }));
996        }
997
998        let result: InterpreterInfoResult =
999            serde_json::from_slice(&output.stdout).map_err(|err| {
1000                let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1001
1002                // If the Python version is too old, we may not even be able to invoke the query script
1003                if stderr.contains("Unknown option: -I") {
1004                    Error::QueryScript {
1005                        err: InterpreterInfoError::UnsupportedPython,
1006                        path: interpreter.to_path_buf(),
1007                    }
1008                } else {
1009                    Error::UnexpectedResponse(UnexpectedResponseError {
1010                        err,
1011                        stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
1012                        stderr,
1013                        path: interpreter.to_path_buf(),
1014                    })
1015                }
1016            })?;
1017
1018        match result {
1019            InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
1020                err,
1021                path: interpreter.to_path_buf(),
1022            }),
1023            InterpreterInfoResult::Success(data) => Ok(*data),
1024        }
1025    }
1026
1027    /// Duplicate the directory structure we have in `../python` into a tempdir, so we can run
1028    /// the Python probing scripts with `python -m python.get_interpreter_info` from that tempdir.
1029    fn setup_python_query_files(root: &Path) -> Result<(), Error> {
1030        let python_dir = root.join("python");
1031        fs_err::create_dir(&python_dir)?;
1032        fs_err::write(
1033            python_dir.join("get_interpreter_info.py"),
1034            include_str!("../python/get_interpreter_info.py"),
1035        )?;
1036        fs_err::write(
1037            python_dir.join("__init__.py"),
1038            include_str!("../python/__init__.py"),
1039        )?;
1040        let packaging_dir = python_dir.join("packaging");
1041        fs_err::create_dir(&packaging_dir)?;
1042        fs_err::write(
1043            packaging_dir.join("__init__.py"),
1044            include_str!("../python/packaging/__init__.py"),
1045        )?;
1046        fs_err::write(
1047            packaging_dir.join("_elffile.py"),
1048            include_str!("../python/packaging/_elffile.py"),
1049        )?;
1050        fs_err::write(
1051            packaging_dir.join("_manylinux.py"),
1052            include_str!("../python/packaging/_manylinux.py"),
1053        )?;
1054        fs_err::write(
1055            packaging_dir.join("_musllinux.py"),
1056            include_str!("../python/packaging/_musllinux.py"),
1057        )?;
1058        Ok(())
1059    }
1060
1061    /// A wrapper around [`markers::query_interpreter_info`] to cache the computed markers.
1062    ///
1063    /// Running a Python script is (relatively) expensive, and the markers won't change
1064    /// unless the Python executable changes, so we use the executable's last modified
1065    /// time as a cache key.
1066    pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
1067        let absolute = std::path::absolute(executable)?;
1068
1069        // Provide a better error message if the link is broken or the file does not exist. Since
1070        // `canonicalize_executable` does not resolve the file on Windows, we must re-use this logic
1071        // for the subsequent metadata read as we may not have actually resolved the path.
1072        let handle_io_error = |err: io::Error| -> Error {
1073            if err.kind() == io::ErrorKind::NotFound {
1074                // Check if it looks like a venv interpreter where the underlying Python
1075                // installation was removed.
1076                if absolute
1077                    .symlink_metadata()
1078                    .is_ok_and(|metadata| metadata.is_symlink())
1079                {
1080                    Error::BrokenSymlink(BrokenSymlink {
1081                        path: executable.to_path_buf(),
1082                        venv: uv_fs::is_virtualenv_executable(executable),
1083                    })
1084                } else {
1085                    Error::NotFound(executable.to_path_buf())
1086                }
1087            } else {
1088                err.into()
1089            }
1090        };
1091
1092        let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1093
1094        let cache_entry = cache.entry(
1095            CacheBucket::Interpreter,
1096            // Shard interpreter metadata by host architecture, operating system, and version, to
1097            // invalidate the cache (e.g.) on OS upgrades.
1098            cache_digest(&(
1099                ARCH,
1100                sys_info::os_type().unwrap_or_default(),
1101                sys_info::os_release().unwrap_or_default(),
1102            )),
1103            // We use the absolute path for the cache entry to avoid cache collisions for relative
1104            // paths. But we don't want to query the executable with symbolic links resolved because
1105            // that can change reported values, e.g., `sys.executable`. We include the canonical
1106            // path in the cache entry as well, otherwise we can have cache collisions if an
1107            // absolute path refers to different interpreters with matching ctimes, e.g., if you
1108            // have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
1109            // modified at the same time.
1110            format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
1111        );
1112
1113        // We check the timestamp of the canonicalized executable to check if an underlying
1114        // interpreter has been modified.
1115        let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
1116
1117        // Read from the cache.
1118        if cache
1119            .freshness(&cache_entry, None, None)
1120            .is_ok_and(Freshness::is_fresh)
1121        {
1122            if let Ok(data) = fs::read(cache_entry.path()) {
1123                match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
1124                    Ok(cached) => {
1125                        if cached.timestamp == modified {
1126                            trace!(
1127                                "Found cached interpreter info for Python {}, skipping query of: {}",
1128                                cached.data.markers.python_full_version(),
1129                                executable.user_display()
1130                            );
1131                            return Ok(cached.data);
1132                        }
1133
1134                        trace!(
1135                            "Ignoring stale interpreter markers for: {}",
1136                            executable.user_display()
1137                        );
1138                    }
1139                    Err(err) => {
1140                        warn!(
1141                            "Broken interpreter cache entry at {}, removing: {err}",
1142                            cache_entry.path().user_display()
1143                        );
1144                        let _ = fs_err::remove_file(cache_entry.path());
1145                    }
1146                }
1147            }
1148        }
1149
1150        // Otherwise, run the Python script.
1151        trace!(
1152            "Querying interpreter executable at {}",
1153            executable.display()
1154        );
1155        let info = Self::query(executable, cache)?;
1156
1157        // If `executable` is a pyenv shim, a bash script that redirects to the activated
1158        // python executable at another path, we're not allowed to cache the interpreter info.
1159        if is_same_file(executable, &info.sys_executable).unwrap_or(false) {
1160            fs::create_dir_all(cache_entry.dir())?;
1161            write_atomic_sync(
1162                cache_entry.path(),
1163                rmp_serde::to_vec(&CachedByTimestamp {
1164                    timestamp: modified,
1165                    data: info.clone(),
1166                })?,
1167            )?;
1168        }
1169
1170        Ok(info)
1171    }
1172}
1173
1174/// Find the Python executable that should be considered the "base" for a virtual environment.
1175///
1176/// Assumes that the provided executable is that of a standalone Python interpreter.
1177///
1178/// The strategy here mimics that of `getpath.py`: we search up the ancestor path to determine
1179/// whether a given executable will convert into a valid Python prefix; if not, we resolve the
1180/// symlink and try again.
1181///
1182/// This ensures that:
1183///
1184/// 1. We avoid using symlinks to arbitrary locations as the base Python executable. For example,
1185///    if a user symlinks a Python _executable_ to `/Users/user/foo`, we want to avoid using
1186///    `/Users/user` as `home`, since it's not a Python installation, and so the relevant libraries
1187///    and headers won't be found when it's used as the executable directory.
1188///    See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L367-L400>
1189///
1190/// 2. We use the "first" resolved symlink that _is_ a valid Python prefix, and thereby preserve
1191///    symlinks. For example, if a user symlinks a Python _installation_ to `/Users/user/foo`, such
1192///    that `/Users/user/foo/bin/python` is the resulting executable, we want to use `/Users/user/foo`
1193///    as `home`, rather than resolving to the symlink target. Concretely, this allows users to
1194///    symlink patch versions (like `cpython-3.12.6-macos-aarch64-none`) to minor version aliases
1195///    (like `cpython-3.12-macos-aarch64-none`) and preserve those aliases in the resulting virtual
1196///    environments.
1197///
1198/// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L591-L594>
1199fn find_base_python(
1200    executable: &Path,
1201    major: u8,
1202    minor: u8,
1203    suffix: &str,
1204) -> Result<PathBuf, io::Error> {
1205    /// Returns `true` if `path` is the root directory.
1206    fn is_root(path: &Path) -> bool {
1207        let mut components = path.components();
1208        components.next() == Some(std::path::Component::RootDir) && components.next().is_none()
1209    }
1210
1211    /// Determining whether `dir` is a valid Python prefix by searching for a "landmark".
1212    ///
1213    /// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L183>
1214    fn is_prefix(dir: &Path, major: u8, minor: u8, suffix: &str) -> bool {
1215        if cfg!(windows) {
1216            dir.join("Lib").join("os.py").is_file()
1217        } else {
1218            dir.join("lib")
1219                .join(format!("python{major}.{minor}{suffix}"))
1220                .join("os.py")
1221                .is_file()
1222        }
1223    }
1224
1225    let mut executable = Cow::Borrowed(executable);
1226
1227    loop {
1228        debug!(
1229            "Assessing Python executable as base candidate: {}",
1230            executable.display()
1231        );
1232
1233        // Determine whether this executable will produce a valid `home` for a virtual environment.
1234        for prefix in executable.ancestors().take_while(|path| !is_root(path)) {
1235            if is_prefix(prefix, major, minor, suffix) {
1236                return Ok(executable.into_owned());
1237            }
1238        }
1239
1240        // If not, resolve the symlink.
1241        let resolved = fs_err::read_link(&executable)?;
1242
1243        // If the symlink is relative, resolve it relative to the executable.
1244        let resolved = if resolved.is_relative() {
1245            if let Some(parent) = executable.parent() {
1246                parent.join(resolved)
1247            } else {
1248                return Err(io::Error::other("Symlink has no parent directory"));
1249            }
1250        } else {
1251            resolved
1252        };
1253
1254        // Normalize the resolved path.
1255        let resolved = uv_fs::normalize_absolute_path(&resolved)?;
1256
1257        executable = Cow::Owned(resolved);
1258    }
1259}
1260
1261#[cfg(unix)]
1262#[cfg(test)]
1263mod tests {
1264    use std::str::FromStr;
1265
1266    use fs_err as fs;
1267    use indoc::{formatdoc, indoc};
1268    use tempfile::tempdir;
1269
1270    use uv_cache::Cache;
1271    use uv_pep440::Version;
1272
1273    use crate::Interpreter;
1274
1275    #[test]
1276    fn test_cache_invalidation() {
1277        let mock_dir = tempdir().unwrap();
1278        let mocked_interpreter = mock_dir.path().join("python");
1279        let json = indoc! {r##"
1280        {
1281            "result": "success",
1282            "platform": {
1283                "os": {
1284                    "name": "manylinux",
1285                    "major": 2,
1286                    "minor": 38
1287                },
1288                "arch": "x86_64"
1289            },
1290            "manylinux_compatible": false,
1291            "standalone": false,
1292            "markers": {
1293                "implementation_name": "cpython",
1294                "implementation_version": "3.12.0",
1295                "os_name": "posix",
1296                "platform_machine": "x86_64",
1297                "platform_python_implementation": "CPython",
1298                "platform_release": "6.5.0-13-generic",
1299                "platform_system": "Linux",
1300                "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov  3 12:16:05 UTC 2023",
1301                "python_full_version": "3.12.0",
1302                "python_version": "3.12",
1303                "sys_platform": "linux"
1304            },
1305            "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1306            "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
1307            "sys_prefix": "/home/ferris/projects/uv/.venv",
1308            "sys_executable": "/home/ferris/projects/uv/.venv/bin/python",
1309            "sys_path": [
1310                "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
1311                "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1312            ],
1313            "site_packages": [
1314                "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
1315            ],
1316            "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
1317            "scheme": {
1318                "data": "/home/ferris/.pyenv/versions/3.12.0",
1319                "include": "/home/ferris/.pyenv/versions/3.12.0/include",
1320                "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1321                "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
1322                "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
1323            },
1324            "virtualenv": {
1325                "data": "",
1326                "include": "include",
1327                "platlib": "lib/python3.12/site-packages",
1328                "purelib": "lib/python3.12/site-packages",
1329                "scripts": "bin"
1330            },
1331            "pointer_size": "64",
1332            "gil_disabled": true,
1333            "debug_enabled": false
1334        }
1335    "##};
1336
1337        let cache = Cache::temp().unwrap().init().unwrap();
1338
1339        fs::write(
1340            &mocked_interpreter,
1341            formatdoc! {r"
1342        #!/bin/sh
1343        echo '{json}'
1344        "},
1345        )
1346        .unwrap();
1347
1348        fs::set_permissions(
1349            &mocked_interpreter,
1350            std::os::unix::fs::PermissionsExt::from_mode(0o770),
1351        )
1352        .unwrap();
1353        let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1354        assert_eq!(
1355            interpreter.markers.python_version().version,
1356            Version::from_str("3.12").unwrap()
1357        );
1358        fs::write(
1359            &mocked_interpreter,
1360            formatdoc! {r"
1361        #!/bin/sh
1362        echo '{}'
1363        ", json.replace("3.12", "3.13")},
1364        )
1365        .unwrap();
1366        let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
1367        assert_eq!(
1368            interpreter.markers.python_version().version,
1369            Version::from_str("3.13").unwrap()
1370        );
1371    }
1372}