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#[derive(Debug, Clone)]
24pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
25
26#[derive(Debug, Clone)]
27struct PythonEnvironmentShared {
28 root: PathBuf,
29 interpreter: Interpreter,
30}
31
32#[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 Virtual,
68 System,
70 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 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 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 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 let executable = virtualenv_python_executable(&root);
211
212 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 pub fn from_installation(installation: PythonInstallation) -> Self {
231 Self::from_interpreter(installation.into_interpreter())
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}