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