Skip to main content

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