uv_python/
environment.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use owo_colors::OwoColorize;
7use tracing::debug;
8
9use uv_cache::Cache;
10use uv_fs::{LockedFile, LockedFileError, Simplified};
11use uv_pep440::Version;
12use uv_preview::Preview;
13
14use crate::discovery::find_python_installation;
15use crate::installation::PythonInstallation;
16use crate::virtualenv::{PyVenvConfiguration, virtualenv_python_executable};
17use crate::{
18    EnvironmentPreference, Error, Interpreter, Prefix, PythonNotFound, PythonPreference,
19    PythonRequest, Target,
20};
21
22/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
23#[derive(Debug, Clone)]
24pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
25
26#[derive(Debug, Clone)]
27struct PythonEnvironmentShared {
28    root: PathBuf,
29    interpreter: Interpreter,
30}
31
32/// The result of failed environment discovery.
33///
34/// Generally this is cast from [`PythonNotFound`] by [`PythonEnvironment::find`].
35#[derive(Clone, Debug, Error)]
36pub struct EnvironmentNotFound {
37    request: PythonRequest,
38    preference: EnvironmentPreference,
39}
40
41#[derive(Clone, Debug, Error)]
42pub struct InvalidEnvironment {
43    path: PathBuf,
44    pub kind: InvalidEnvironmentKind,
45}
46#[derive(Debug, Clone)]
47pub enum InvalidEnvironmentKind {
48    NotDirectory,
49    Empty,
50    MissingExecutable(PathBuf),
51}
52
53impl From<PythonNotFound> for EnvironmentNotFound {
54    fn from(value: PythonNotFound) -> Self {
55        Self {
56            request: value.request,
57            preference: value.environment_preference,
58        }
59    }
60}
61
62impl fmt::Display for EnvironmentNotFound {
63    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64        #[derive(Debug, Copy, Clone)]
65        enum SearchType {
66            /// Only virtual environments were searched.
67            Virtual,
68            /// Only system installations were searched.
69            System,
70            /// Both virtual and system installations were searched.
71            VirtualOrSystem,
72        }
73
74        impl fmt::Display for SearchType {
75            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76                match self {
77                    Self::Virtual => write!(f, "virtual environment"),
78                    Self::System => write!(f, "system Python installation"),
79                    Self::VirtualOrSystem => {
80                        write!(f, "virtual environment or system Python installation")
81                    }
82                }
83            }
84        }
85
86        let search_type = match self.preference {
87            EnvironmentPreference::Any => SearchType::VirtualOrSystem,
88            EnvironmentPreference::ExplicitSystem => {
89                if self.request.is_explicit_system() {
90                    SearchType::VirtualOrSystem
91                } else {
92                    SearchType::Virtual
93                }
94            }
95            EnvironmentPreference::OnlySystem => SearchType::System,
96            EnvironmentPreference::OnlyVirtual => SearchType::Virtual,
97        };
98
99        if matches!(self.request, PythonRequest::Default | PythonRequest::Any) {
100            write!(f, "No {search_type} found")?;
101        } else {
102            write!(f, "No {search_type} found for {}", self.request)?;
103        }
104
105        match search_type {
106            // This error message assumes that the relevant API accepts the `--system` flag. This
107            // is true of the callsites today, since the project APIs never surface this error.
108            SearchType::Virtual => write!(
109                f,
110                "; run `{}` to create an environment, or pass `{}` to install into a non-virtual environment",
111                "uv venv".green(),
112                "--system".green()
113            )?,
114            SearchType::VirtualOrSystem => {
115                write!(f, "; run `{}` to create an environment", "uv venv".green())?;
116            }
117            SearchType::System => {}
118        }
119
120        Ok(())
121    }
122}
123
124impl fmt::Display for InvalidEnvironment {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        write!(
127            f,
128            "Invalid environment at `{}`: {}",
129            self.path.user_display(),
130            self.kind
131        )
132    }
133}
134
135impl fmt::Display for InvalidEnvironmentKind {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        match self {
138            Self::NotDirectory => write!(f, "expected directory but found a file"),
139            Self::MissingExecutable(path) => {
140                write!(f, "missing Python executable at `{}`", path.user_display())
141            }
142            Self::Empty => write!(f, "directory is empty"),
143        }
144    }
145}
146
147impl PythonEnvironment {
148    /// Find a [`PythonEnvironment`] matching the given request and preference.
149    ///
150    /// If looking for a Python interpreter to create a new environment, use [`PythonInstallation::find`]
151    /// instead.
152    pub fn find(
153        request: &PythonRequest,
154        preference: EnvironmentPreference,
155        python_preference: PythonPreference,
156        cache: &Cache,
157        preview: Preview,
158    ) -> Result<Self, Error> {
159        let installation =
160            match find_python_installation(request, preference, python_preference, cache, preview)?
161            {
162                Ok(installation) => installation,
163                Err(err) => return Err(EnvironmentNotFound::from(err).into()),
164            };
165        Ok(Self::from_installation(installation))
166    }
167
168    /// Create a [`PythonEnvironment`] from the virtual environment at the given root.
169    ///
170    /// N.B. This function also works for system Python environments and users depend on this.
171    pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
172        debug!(
173            "Checking for Python environment at: `{}`",
174            root.as_ref().user_display()
175        );
176        match root.as_ref().try_exists() {
177            Ok(true) => {}
178            Ok(false) => {
179                return Err(Error::MissingEnvironment(EnvironmentNotFound {
180                    preference: EnvironmentPreference::Any,
181                    request: PythonRequest::Directory(root.as_ref().to_owned()),
182                }));
183            }
184            Err(err) => return Err(Error::Discovery(err.into())),
185        }
186
187        if root.as_ref().is_file() {
188            return Err(InvalidEnvironment {
189                path: root.as_ref().to_path_buf(),
190                kind: InvalidEnvironmentKind::NotDirectory,
191            }
192            .into());
193        }
194
195        if root
196            .as_ref()
197            .read_dir()
198            .is_ok_and(|mut dir| dir.next().is_none())
199        {
200            return Err(InvalidEnvironment {
201                path: root.as_ref().to_path_buf(),
202                kind: InvalidEnvironmentKind::Empty,
203            }
204            .into());
205        }
206
207        // Note we do not canonicalize the root path or the executable path, this is important
208        // because the path the interpreter is invoked at can determine the value of
209        // `sys.executable`.
210        let executable = virtualenv_python_executable(&root);
211
212        // If we can't find an executable, exit before querying to provide a better error.
213        if !(executable.is_symlink() || executable.is_file()) {
214            return Err(InvalidEnvironment {
215                path: root.as_ref().to_path_buf(),
216                kind: InvalidEnvironmentKind::MissingExecutable(executable.clone()),
217            }
218            .into());
219        }
220
221        let interpreter = Interpreter::query(executable, cache)?;
222
223        Ok(Self(Arc::new(PythonEnvironmentShared {
224            root: interpreter.sys_prefix().to_path_buf(),
225            interpreter,
226        })))
227    }
228
229    /// Create a [`PythonEnvironment`] from an existing [`PythonInstallation`].
230    pub fn from_installation(installation: PythonInstallation) -> Self {
231        Self::from_interpreter(installation.into_interpreter())
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}