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