Skip to main content

uv_python_interpreter/
environment.rs

1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::{env, fmt};
5
6use owo_colors::OwoColorize;
7use thiserror::Error;
8use tracing::debug;
9
10use uv_cache::Cache;
11use uv_fs::{LockedFile, LockedFileError, Simplified};
12use uv_pep440::Version;
13use uv_static::EnvVars;
14
15use crate::Interpreter;
16use crate::interpreter::InterpreterInfo;
17use crate::virtualenv::{PyVenvConfiguration, virtualenv_python_executable};
18use uv_python_types::{EnvironmentPreference, Prefix, PythonRequest, Target};
19
20/// A failure while inspecting an existing Python environment.
21#[derive(Debug, Error)]
22pub enum Error {
23    #[error(transparent)]
24    Io(#[from] std::io::Error),
25    #[error(transparent)]
26    Query(#[from] crate::InterpreterError),
27    #[error(transparent)]
28    VirtualEnv(#[from] crate::VirtualEnvError),
29    #[error(transparent)]
30    MissingEnvironment(#[from] EnvironmentNotFound),
31    #[error(transparent)]
32    InvalidEnvironment(#[from] InvalidEnvironment),
33}
34
35impl uv_errors::Hinted for Error {
36    fn hints(&self) -> uv_errors::Hints<'_> {
37        match self {
38            Self::Query(error) => error.hints(),
39            Self::Io(_)
40            | Self::VirtualEnv(_)
41            | Self::MissingEnvironment(_)
42            | Self::InvalidEnvironment(_) => uv_errors::Hints::none(),
43        }
44    }
45}
46
47/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
48#[derive(Debug, Clone, Eq, PartialEq)]
49pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
50
51#[derive(Debug, Clone, Eq, PartialEq)]
52struct PythonEnvironmentShared {
53    root: PathBuf,
54    interpreter: Interpreter,
55}
56
57/// The result of failed environment discovery.
58///
59/// Records the requested Python and the environment kinds that were searched.
60#[derive(Clone, Debug, Error)]
61pub struct EnvironmentNotFound {
62    request: PythonRequest,
63    preference: EnvironmentPreference,
64}
65
66#[derive(Clone, Debug, Error)]
67pub struct InvalidEnvironment {
68    path: PathBuf,
69    pub kind: InvalidEnvironmentKind,
70}
71#[derive(Debug, Clone)]
72pub enum InvalidEnvironmentKind {
73    NotDirectory,
74    Empty,
75    MissingExecutable(PathBuf),
76}
77
78impl EnvironmentNotFound {
79    pub fn new(request: PythonRequest, preference: EnvironmentPreference) -> Self {
80        Self {
81            request,
82            preference,
83        }
84    }
85}
86
87impl fmt::Display for EnvironmentNotFound {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        #[derive(Debug, Copy, Clone)]
90        enum SearchType {
91            /// Only virtual environments were searched.
92            Virtual,
93            /// Only system installations were searched.
94            System,
95            /// Both virtual and system installations were searched.
96            VirtualOrSystem,
97        }
98
99        impl fmt::Display for SearchType {
100            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
101                match self {
102                    Self::Virtual => write!(f, "virtual environment"),
103                    Self::System => write!(f, "system Python installation"),
104                    Self::VirtualOrSystem => {
105                        write!(f, "virtual environment or system Python installation")
106                    }
107                }
108            }
109        }
110
111        let search_type = match self.preference {
112            EnvironmentPreference::Any => SearchType::VirtualOrSystem,
113            EnvironmentPreference::ExplicitSystem => {
114                if self.request.is_explicit_system() {
115                    SearchType::VirtualOrSystem
116                } else {
117                    SearchType::Virtual
118                }
119            }
120            EnvironmentPreference::OnlySystem => SearchType::System,
121            EnvironmentPreference::OnlyVirtual => SearchType::Virtual,
122        };
123
124        if matches!(self.request, PythonRequest::Default | PythonRequest::Any) {
125            write!(f, "No {search_type} found")?;
126        } else {
127            write!(f, "No {search_type} found for {}", self.request)?;
128        }
129
130        match search_type {
131            // This error message assumes that the relevant API accepts the `--system` flag. This
132            // is true of the callsites today, since the project APIs never surface this error.
133            SearchType::Virtual => write!(
134                f,
135                "; run `{}` to create an environment, or pass `{}` to install into a non-virtual environment",
136                "uv venv".green(),
137                "--system".green()
138            )?,
139            SearchType::VirtualOrSystem => {
140                write!(f, "; run `{}` to create an environment", "uv venv".green())?;
141            }
142            SearchType::System => {}
143        }
144
145        Ok(())
146    }
147}
148
149impl fmt::Display for InvalidEnvironment {
150    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151        write!(
152            f,
153            "Invalid environment at `{}`: {}",
154            self.path.user_display(),
155            self.kind
156        )
157    }
158}
159
160impl fmt::Display for InvalidEnvironmentKind {
161    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
162        match self {
163            Self::NotDirectory => write!(f, "expected directory but found a file"),
164            Self::MissingExecutable(path) => {
165                write!(f, "missing Python executable at `{}`", path.user_display())
166            }
167            Self::Empty => write!(f, "directory is empty"),
168        }
169    }
170}
171
172impl PythonEnvironment {
173    /// Create a [`PythonEnvironment`] from the virtual environment at the given root.
174    ///
175    /// N.B. This function also works for system Python environments and users depend on this.
176    pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
177        debug!(
178            "Checking for Python environment at: {}",
179            root.as_ref().user_display()
180        );
181        match root.as_ref().try_exists() {
182            Ok(true) => {}
183            Ok(false) => {
184                return Err(Error::MissingEnvironment(EnvironmentNotFound {
185                    preference: EnvironmentPreference::Any,
186                    request: PythonRequest::Directory(root.as_ref().to_owned()),
187                }));
188            }
189            Err(err) => return Err(Error::Io(err)),
190        }
191
192        if root.as_ref().is_file() {
193            return Err(InvalidEnvironment {
194                path: root.as_ref().to_path_buf(),
195                kind: InvalidEnvironmentKind::NotDirectory,
196            }
197            .into());
198        }
199
200        if root
201            .as_ref()
202            .read_dir()
203            .is_ok_and(|mut dir| dir.next().is_none())
204        {
205            return Err(InvalidEnvironment {
206                path: root.as_ref().to_path_buf(),
207                kind: InvalidEnvironmentKind::Empty,
208            }
209            .into());
210        }
211
212        // Note we do not canonicalize the root path or the executable path, this is important
213        // because the path the interpreter is invoked at can determine the value of
214        // `sys.executable`.
215        let executable = virtualenv_python_executable(&root);
216
217        // If we can't find an executable, exit before querying to provide a better error.
218        if !(executable.is_symlink() || executable.is_file()) {
219            return Err(InvalidEnvironment {
220                path: root.as_ref().to_path_buf(),
221                kind: InvalidEnvironmentKind::MissingExecutable(executable.clone()),
222            }
223            .into());
224        }
225
226        let interpreter = Interpreter::query(executable, cache)?;
227
228        Ok(Self(Arc::new(PythonEnvironmentShared {
229            root: interpreter.sys_prefix().to_path_buf(),
230            interpreter,
231        })))
232    }
233
234    /// Create a [`PythonEnvironment`] from an existing [`Interpreter`].
235    pub fn from_interpreter(interpreter: Interpreter) -> Self {
236        Self(Arc::new(PythonEnvironmentShared {
237            root: interpreter.sys_prefix().to_path_buf(),
238            interpreter,
239        }))
240    }
241
242    /// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--target` directory.
243    pub fn with_target(self, target: Target) -> std::io::Result<Self> {
244        let inner = Arc::unwrap_or_clone(self.0);
245        Ok(Self(Arc::new(PythonEnvironmentShared {
246            interpreter: inner.interpreter.with_target(target)?,
247            ..inner
248        })))
249    }
250
251    /// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--prefix` directory.
252    pub fn with_prefix(self, prefix: Prefix) -> std::io::Result<Self> {
253        let inner = Arc::unwrap_or_clone(self.0);
254        Ok(Self(Arc::new(PythonEnvironmentShared {
255            interpreter: inner.interpreter.with_prefix(prefix)?,
256            ..inner
257        })))
258    }
259
260    /// Returns the root (i.e., `prefix`) of the Python interpreter.
261    pub fn root(&self) -> &Path {
262        &self.0.root
263    }
264
265    /// Return the [`Interpreter`] for this virtual environment.
266    ///
267    /// See also [`PythonEnvironment::into_interpreter`].
268    pub fn interpreter(&self) -> &Interpreter {
269        &self.0.interpreter
270    }
271
272    /// Return the [`PyVenvConfiguration`] for this environment, as extracted from the
273    /// `pyvenv.cfg` file.
274    pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
275        Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
276    }
277
278    /// Set a key-value pair in the `pyvenv.cfg` file.
279    pub fn set_pyvenv_cfg(&self, key: &str, value: &str) -> Result<(), Error> {
280        let content = fs_err::read_to_string(self.0.root.join("pyvenv.cfg"))?;
281        fs_err::write(
282            self.0.root.join("pyvenv.cfg"),
283            PyVenvConfiguration::set(&content, key, value),
284        )?;
285        Ok(())
286    }
287
288    /// Returns `true` if the environment is "relocatable".
289    pub fn relocatable(&self) -> bool {
290        self.cfg().is_ok_and(|cfg| cfg.is_relocatable())
291    }
292
293    /// Returns the location of the Python executable.
294    pub fn python_executable(&self) -> &Path {
295        self.0.interpreter.sys_executable()
296    }
297
298    /// Returns an iterator over the `site-packages` directories inside the environment.
299    ///
300    /// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
301    /// a single element; however, in some distributions, they may be different.
302    ///
303    /// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
304    /// still deduplicate the entries, returning a single path.
305    pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
306        self.0.interpreter.site_packages()
307    }
308
309    /// Returns the path to the `bin` directory inside this environment.
310    pub fn scripts(&self) -> &Path {
311        self.0.interpreter.scripts()
312    }
313
314    /// Grab a file lock for the environment to prevent concurrent writes across processes.
315    pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
316        self.0.interpreter.lock().await
317    }
318
319    /// Return the [`Interpreter`] for this environment.
320    ///
321    /// See also [`PythonEnvironment::interpreter`].
322    pub fn into_interpreter(self) -> Interpreter {
323        Arc::unwrap_or_clone(self.0).interpreter
324    }
325
326    /// Returns `true` if the [`PythonEnvironment`] uses the same underlying [`Interpreter`].
327    pub fn uses(&self, interpreter: &Interpreter) -> bool {
328        // TODO(zanieb): Consider using `sysconfig.get_path("stdlib")` instead, which
329        // should be generally robust.
330        if cfg!(windows) {
331            // On Windows, we can't canonicalize an interpreter based on its executable path
332            // because the executables are separate shim files (not links). Instead, we
333            // compare the `sys.base_prefix`.
334            let old_base_prefix = self.interpreter().sys_base_prefix();
335            let selected_base_prefix = interpreter.sys_base_prefix();
336            old_base_prefix == selected_base_prefix
337        } else {
338            // On Unix, we can see if the canonicalized executable is the same file.
339            self.interpreter().sys_executable() == interpreter.sys_executable()
340                || same_file::is_same_file(
341                    self.interpreter().sys_executable(),
342                    interpreter.sys_executable(),
343                )
344                .unwrap_or(false)
345        }
346    }
347
348    /// Check if the `pyvenv.cfg` version is the same as the interpreter's Python version.
349    ///
350    /// Returns [`None`] if the versions are the consistent or there is no `pyvenv.cfg`. If the
351    /// versions do not match, returns a tuple of the `pyvenv.cfg` and interpreter's Python versions
352    /// for display.
353    pub fn get_pyvenv_version_conflict(&self) -> Option<(Version, Version)> {
354        let cfg = self.cfg().ok()?;
355        let cfg_version = cfg.version?.into_version();
356
357        // Determine if we should be checking for patch or pre-release equality
358        let exe_version = if cfg_version.release().get(2).is_none() {
359            self.interpreter().python_minor_version()
360        } else if cfg_version.pre().is_none() {
361            self.interpreter().python_patch_version()
362        } else {
363            self.interpreter().python_version().clone()
364        };
365
366        (cfg_version != exe_version).then_some((cfg_version, exe_version))
367    }
368
369    /// Cache the interpreter metadata for this venv.
370    ///
371    /// Derive the metadata from the base interpreter without running the venv's Python.
372    pub fn cache_virtualenv(&self, system_site_packages: bool, cache: &Cache) -> Result<(), Error> {
373        // Launcher overrides can change `sys.executable` and `sys.prefix`, while
374        // `sys._base_executable` isn't affected. Instead of trying to stitch together this edge
375        // case, query the actual metadata on the next run.
376        if env::var_os(EnvVars::PYTHONEXECUTABLE).is_some()
377            || env::var_os(EnvVars::PYVENV_LAUNCHER).is_some()
378        {
379            return Ok(());
380        }
381
382        // TODO: Handle system-site-packages correctly.
383        // We should infer the site packages path from the base interpreter,
384        // but for that, we first need to fix cache invalidation when it changes.
385        // https://github.com/astral-sh/uv/issues/18510
386        if system_site_packages {
387            return Ok(());
388        }
389
390        // An upgradeable venv can use a minor-version link instead of the selected base
391        // interpreter. Python may report different base paths when started through that link.
392        if let Some(home) = self.cfg()?.home
393            && self
394                .interpreter()
395                .to_base_python()?
396                .parent()
397                .is_some_and(|base| base.simplified() != home.simplified())
398        {
399            Interpreter::clear_cache(self.interpreter().sys_executable(), cache)?;
400            return Ok(());
401        }
402
403        let info = InterpreterInfo::from_virtualenv(self.interpreter())?;
404        info.cache(self.interpreter().sys_executable(), cache)?;
405        Ok(())
406    }
407}