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#[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#[derive(Debug, Clone)]
47pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
48
49#[derive(Debug, Clone)]
50struct PythonEnvironmentShared {
51 root: PathBuf,
52 interpreter: Interpreter,
53}
54
55#[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 Virtual,
91 System,
93 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 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 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 let executable = virtualenv_python_executable(&root);
214
215 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 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 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 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 pub fn root(&self) -> &Path {
260 &self.0.root
261 }
262
263 pub fn interpreter(&self) -> &Interpreter {
267 &self.0.interpreter
268 }
269
270 pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
273 Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
274 }
275
276 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 pub fn relocatable(&self) -> bool {
288 self.cfg().is_ok_and(|cfg| cfg.is_relocatable())
289 }
290
291 pub fn python_executable(&self) -> &Path {
293 self.0.interpreter.sys_executable()
294 }
295
296 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
304 self.0.interpreter.site_packages()
305 }
306
307 pub fn scripts(&self) -> &Path {
309 self.0.interpreter.scripts()
310 }
311
312 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
314 self.0.interpreter.lock().await
315 }
316
317 pub fn into_interpreter(self) -> Interpreter {
321 Arc::unwrap_or_clone(self.0).interpreter
322 }
323
324 pub fn uses(&self, interpreter: &Interpreter) -> bool {
326 if cfg!(windows) {
329 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 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 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 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}