1use std::borrow::Cow;
2use std::str::FromStr;
3use std::{
4 env, io,
5 path::{Path, PathBuf},
6};
7
8use fs_err as fs;
9use thiserror::Error;
10
11use uv_pypi_types::Scheme;
12use uv_static::EnvVars;
13
14use crate::PythonVersion;
15
16#[derive(Debug)]
18pub struct VirtualEnvironment {
19 pub root: PathBuf,
21
22 pub executable: PathBuf,
25
26 pub base_executable: PathBuf,
28
29 pub scheme: Scheme,
31}
32
33#[derive(Debug, Clone)]
35pub struct PyVenvConfiguration {
36 pub(super) home: Option<PathBuf>,
38 pub(super) virtualenv: bool,
40 pub(super) uv: bool,
42 pub(super) relocatable: bool,
44 pub(super) seed: bool,
46 pub(super) include_system_site_packages: bool,
48 pub(super) version: Option<PythonVersion>,
50}
51
52#[derive(Debug, Error)]
53pub enum Error {
54 #[error(transparent)]
55 Io(#[from] io::Error),
56 #[error("Broken virtual environment `{0}`: `pyvenv.cfg` is missing")]
57 MissingPyVenvCfg(PathBuf),
58 #[error("Broken virtual environment `{0}`: `pyvenv.cfg` could not be parsed")]
59 ParsePyVenvCfg(PathBuf, #[source] io::Error),
60}
61
62pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
66 if let Some(dir) = env::var_os(EnvVars::VIRTUAL_ENV).filter(|value| !value.is_empty()) {
67 return Some(PathBuf::from(dir));
68 }
69
70 None
71}
72
73#[derive(Debug, PartialEq, Eq, Copy, Clone)]
74pub(crate) enum CondaEnvironmentKind {
75 Base,
77 Child,
79}
80
81impl CondaEnvironmentKind {
82 fn from_prefix_path(path: &Path) -> Self {
91 if is_pixi_environment(path) {
94 return Self::Child;
95 }
96
97 if let Ok(conda_root) = env::var(EnvVars::CONDA_ROOT) {
99 if path == Path::new(&conda_root) {
100 return Self::Base;
101 }
102 }
103
104 let Ok(current_env) = env::var(EnvVars::CONDA_DEFAULT_ENV) else {
106 return Self::Child;
107 };
108
109 if path == Path::new(¤t_env) {
112 return Self::Child;
113 }
114
115 let Some(name) = path.file_name() else {
117 return Self::Child;
118 };
119
120 if name.to_str().is_some_and(|name| name == current_env) {
123 Self::Child
124 } else {
125 Self::Base
126 }
127 }
128}
129
130fn is_pixi_environment(path: &Path) -> bool {
132 path.join("conda-meta").join("pixi").is_file()
133}
134
135pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option<PathBuf> {
140 let dir = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty())?;
141 let path = PathBuf::from(dir);
142
143 if kind != CondaEnvironmentKind::from_prefix_path(&path) {
144 return None;
145 }
146
147 Some(path)
148}
149
150pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
156 let current_dir = crate::current_dir()?;
157
158 for dir in current_dir.ancestors() {
159 if uv_fs::is_virtualenv_base(dir) {
161 return Ok(Some(dir.to_path_buf()));
162 }
163
164 let dot_venv = dir.join(".venv");
166 let metadata = match fs::symlink_metadata(&dot_venv) {
167 Ok(metadata) => metadata,
168 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
169 Err(err) => return Err(err.into()),
170 };
171 if metadata.is_dir() || metadata.file_type().is_symlink() {
172 if !uv_fs::is_virtualenv_base(&dot_venv) {
173 return Err(Error::MissingPyVenvCfg(dot_venv));
174 }
175 return Ok(Some(dot_venv));
176 }
177 }
178
179 Ok(None)
180}
181
182pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
184 let venv = venv.as_ref();
185 if cfg!(windows) {
186 let default_executable = venv.join("Scripts").join("python.exe");
188 if default_executable.exists() {
189 return default_executable;
190 }
191
192 let executable = venv.join("bin").join("python.exe");
195 if executable.exists() {
196 return executable;
197 }
198
199 let executable = venv.join("python.exe");
201 if executable.exists() {
202 return executable;
203 }
204
205 default_executable
207 } else {
208 let default_executable = venv.join("bin").join("python3");
210 if default_executable.exists() {
211 return default_executable;
212 }
213
214 let executable = venv.join("bin").join("python");
215 if executable.exists() {
216 return executable;
217 }
218
219 default_executable
221 }
222}
223
224impl PyVenvConfiguration {
225 pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
227 let mut home = None;
228 let mut virtualenv = false;
229 let mut uv = false;
230 let mut relocatable = false;
231 let mut seed = false;
232 let mut include_system_site_packages = true;
233 let mut version = None;
234
235 let content = fs::read_to_string(&cfg)
239 .map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
240 for line in content.lines() {
241 let Some((key, value)) = line.split_once('=') else {
242 continue;
243 };
244 match key.trim() {
245 "home" => {
246 home = Some(PathBuf::from(value.trim()));
247 }
248 "virtualenv" => {
249 virtualenv = true;
250 }
251 "uv" => {
252 uv = true;
253 }
254 "relocatable" => {
255 relocatable = value.trim().to_lowercase() == "true";
256 }
257 "seed" => {
258 seed = value.trim().to_lowercase() == "true";
259 }
260 "include-system-site-packages" => {
261 include_system_site_packages = value.trim().to_lowercase() == "true";
262 }
263 "version" | "version_info" => {
264 version = Some(
265 PythonVersion::from_str(value.trim())
266 .map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
267 );
268 }
269 _ => {}
270 }
271 }
272
273 Ok(Self {
274 home,
275 virtualenv,
276 uv,
277 relocatable,
278 seed,
279 include_system_site_packages,
280 version,
281 })
282 }
283
284 pub fn is_virtualenv(&self) -> bool {
286 self.virtualenv
287 }
288
289 pub fn is_uv(&self) -> bool {
291 self.uv
292 }
293
294 pub(crate) fn is_relocatable(&self) -> bool {
296 self.relocatable
297 }
298
299 pub fn is_seed(&self) -> bool {
301 self.seed
302 }
303
304 pub fn include_system_site_packages(&self) -> bool {
306 self.include_system_site_packages
307 }
308
309 pub fn set(content: &str, key: &str, value: &str) -> String {
311 let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
312 let mut found = false;
313 for line in &mut lines {
314 if let Some((lhs, _)) = line.split_once('=')
315 && lhs.trim() == key
316 {
317 *line = Cow::Owned(format!("{key} = {value}"));
318 found = true;
319 break;
320 }
321 }
322 if !found {
323 lines.push(Cow::Owned(format!("{key} = {value}")));
324 }
325 if lines.is_empty() {
326 String::new()
327 } else {
328 format!("{}\n", lines.join("\n"))
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use std::ffi::OsStr;
336
337 use indoc::indoc;
338 use temp_env::with_vars;
339 use tempfile::tempdir;
340
341 use super::*;
342
343 #[test]
344 fn pixi_environment_is_treated_as_child() {
345 let tempdir = tempdir().unwrap();
346 let prefix = tempdir.path();
347 let conda_meta = prefix.join("conda-meta");
348
349 fs::create_dir_all(&conda_meta).unwrap();
350 fs::write(conda_meta.join("pixi"), []).unwrap();
351
352 let vars = [
353 (EnvVars::CONDA_ROOT, None),
354 (EnvVars::CONDA_PREFIX, Some(prefix.as_os_str())),
355 (EnvVars::CONDA_DEFAULT_ENV, Some(OsStr::new("example"))),
356 ];
357
358 with_vars(vars, || {
359 assert_eq!(
360 CondaEnvironmentKind::from_prefix_path(prefix),
361 CondaEnvironmentKind::Child
362 );
363 });
364 }
365
366 #[test]
367 fn test_set_existing_key() {
368 let content = indoc! {"
369 home = /path/to/python
370 version = 3.8.0
371 include-system-site-packages = false
372 "};
373 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
374 assert_eq!(
375 result,
376 indoc! {"
377 home = /path/to/python
378 version = 3.9.0
379 include-system-site-packages = false
380 "}
381 );
382 }
383
384 #[test]
385 fn test_set_new_key() {
386 let content = indoc! {"
387 home = /path/to/python
388 version = 3.8.0
389 "};
390 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
391 assert_eq!(
392 result,
393 indoc! {"
394 home = /path/to/python
395 version = 3.8.0
396 include-system-site-packages = false
397 "}
398 );
399 }
400
401 #[test]
402 fn test_set_key_no_spaces() {
403 let content = indoc! {"
404 home=/path/to/python
405 version=3.8.0
406 "};
407 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
408 assert_eq!(
409 result,
410 indoc! {"
411 home=/path/to/python
412 version=3.8.0
413 include-system-site-packages = false
414 "}
415 );
416 }
417
418 #[test]
419 fn test_set_key_prefix() {
420 let content = indoc! {"
421 home = /path/to/python
422 home_dir = /other/path
423 "};
424 let result = PyVenvConfiguration::set(content, "home", "new/path");
425 assert_eq!(
426 result,
427 indoc! {"
428 home = new/path
429 home_dir = /other/path
430 "}
431 );
432 }
433
434 #[test]
435 fn test_set_empty_content() {
436 let content = "";
437 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
438 assert_eq!(
439 result,
440 indoc! {"
441 version = 3.9.0
442 "}
443 );
444 }
445}