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