Skip to main content

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