Skip to main content

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