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