Skip to main content

uv_python/
interpreter.rs

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