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#[derive(Debug, Clone)]
23pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
24
25#[derive(Debug, Clone)]
26struct PythonEnvironmentShared {
27 root: PathBuf,
28 interpreter: Interpreter,
29}
30
31#[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 Virtual,
67 System,
69 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 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 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 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 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_installation(installation: PythonInstallation) -> Self {
234 Self::from_interpreter(installation.into_interpreter())
235 }
236
237 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 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 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 pub fn root(&self) -> &Path {
265 &self.0.root
266 }
267
268 pub fn interpreter(&self) -> &Interpreter {
272 &self.0.interpreter
273 }
274
275 pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
278 Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
279 }
280
281 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 pub fn relocatable(&self) -> bool {
293 self.cfg().is_ok_and(|cfg| cfg.is_relocatable())
294 }
295
296 pub fn python_executable(&self) -> &Path {
298 self.0.interpreter.sys_executable()
299 }
300
301 pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
309 self.0.interpreter.site_packages()
310 }
311
312 pub fn scripts(&self) -> &Path {
314 self.0.interpreter.scripts()
315 }
316
317 pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
319 self.0.interpreter.lock().await
320 }
321
322 pub fn into_interpreter(self) -> Interpreter {
326 Arc::unwrap_or_clone(self.0).interpreter
327 }
328
329 pub fn uses(&self, interpreter: &Interpreter) -> bool {
331 if cfg!(windows) {
334 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 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 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 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}