Skip to main content

uv_python_interpreter/
environment.rs

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