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