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#[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#[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#[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 Virtual,
93 System,
95 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 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 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 let executable = virtualenv_python_executable(&root);
216
217 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 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 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 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 pub fn root(&self) -> &Path {
262 &self.0.root
263 }
264
265 pub fn interpreter(&self) -> &Interpreter {
269 &self.0.interpreter
270 }
271
272 pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
275 Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
276 }
277
278 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 pub fn relocatable(&self) -> bool {
290 self.cfg().is_ok_and(|cfg| cfg.is_relocatable())
291 }
292
293 pub fn python_executable(&self) -> &Path {
295 self.0.interpreter.sys_executable()
296 }
297
298 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
306 self.0.interpreter.site_packages()
307 }
308
309 pub fn scripts(&self) -> &Path {
311 self.0.interpreter.scripts()
312 }
313
314 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
316 self.0.interpreter.lock().await
317 }
318
319 pub fn into_interpreter(self) -> Interpreter {
323 Arc::unwrap_or_clone(self.0).interpreter
324 }
325
326 pub fn uses(&self, interpreter: &Interpreter) -> bool {
328 if cfg!(windows) {
331 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 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 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 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 pub fn cache_virtualenv(&self, system_site_packages: bool, cache: &Cache) -> Result<(), Error> {
373 if env::var_os(EnvVars::PYTHONEXECUTABLE).is_some()
377 || env::var_os(EnvVars::PYVENV_LAUNCHER).is_some()
378 {
379 return Ok(());
380 }
381
382 if system_site_packages {
387 return Ok(());
388 }
389
390 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}