Skip to main content

uv_python/
interpreter.rs

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