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(crate) virtualenv: bool,
38 pub(crate) uv: bool,
40 pub(crate) relocatable: bool,
42 pub(crate) seed: bool,
44 pub(crate) include_system_site_packages: bool,
46 pub(crate) version: Option<PythonVersion>,
48}
49
50#[derive(Debug, Error)]
51pub enum Error {
52 #[error(transparent)]
53 Io(#[from] io::Error),
54 #[error("Broken virtual environment `{0}`: `pyvenv.cfg` is missing")]
55 MissingPyVenvCfg(PathBuf),
56 #[error("Broken virtual environment `{0}`: `pyvenv.cfg` could not be parsed")]
57 ParsePyVenvCfg(PathBuf, #[source] io::Error),
58}
59
60pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
64 if let Some(dir) = env::var_os(EnvVars::VIRTUAL_ENV).filter(|value| !value.is_empty()) {
65 return Some(PathBuf::from(dir));
66 }
67
68 None
69}
70
71#[derive(Debug, PartialEq, Eq, Copy, Clone)]
72pub(crate) enum CondaEnvironmentKind {
73 Base,
75 Child,
77}
78
79impl CondaEnvironmentKind {
80 fn from_prefix_path(path: &Path) -> Self {
89 if is_pixi_environment(path) {
92 return Self::Child;
93 }
94
95 if let Ok(conda_root) = env::var(EnvVars::CONDA_ROOT) {
97 if path == Path::new(&conda_root) {
98 return Self::Base;
99 }
100 }
101
102 let Ok(current_env) = env::var(EnvVars::CONDA_DEFAULT_ENV) else {
104 return Self::Child;
105 };
106
107 if path == Path::new(¤t_env) {
110 return Self::Child;
111 }
112
113 if current_env == "base" || current_env == "root" {
118 return Self::Base;
119 }
120
121 let Some(name) = path.file_name() else {
123 return Self::Child;
124 };
125
126 if name.to_str().is_some_and(|name| name == current_env) {
129 Self::Child
130 } else {
131 Self::Base
132 }
133 }
134}
135
136fn is_pixi_environment(path: &Path) -> bool {
138 path.join("conda-meta").join("pixi").is_file()
139}
140
141pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option<PathBuf> {
146 let dir = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty())?;
147 let path = PathBuf::from(dir);
148
149 if kind != CondaEnvironmentKind::from_prefix_path(&path) {
150 return None;
151 }
152
153 Some(path)
154}
155
156pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
162 let current_dir = crate::current_dir()?;
163
164 for dir in current_dir.ancestors() {
165 if uv_fs::is_virtualenv_base(dir) {
167 return Ok(Some(dir.to_path_buf()));
168 }
169
170 let dot_venv = dir.join(".venv");
172 if dot_venv.is_dir() {
173 if !uv_fs::is_virtualenv_base(&dot_venv) {
174 return Err(Error::MissingPyVenvCfg(dot_venv));
175 }
176 return Ok(Some(dot_venv));
177 }
178 }
179
180 Ok(None)
181}
182
183pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
185 let venv = venv.as_ref();
186 if cfg!(windows) {
187 let default_executable = venv.join("Scripts").join("python.exe");
189 if default_executable.exists() {
190 return default_executable;
191 }
192
193 let executable = venv.join("bin").join("python.exe");
196 if executable.exists() {
197 return executable;
198 }
199
200 let executable = venv.join("python.exe");
202 if executable.exists() {
203 return executable;
204 }
205
206 default_executable
208 } else {
209 let default_executable = venv.join("bin").join("python3");
211 if default_executable.exists() {
212 return default_executable;
213 }
214
215 let executable = venv.join("bin").join("python");
216 if executable.exists() {
217 return executable;
218 }
219
220 default_executable
222 }
223}
224
225impl PyVenvConfiguration {
226 pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
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 "virtualenv" => {
246 virtualenv = true;
247 }
248 "uv" => {
249 uv = true;
250 }
251 "relocatable" => {
252 relocatable = value.trim().to_lowercase() == "true";
253 }
254 "seed" => {
255 seed = value.trim().to_lowercase() == "true";
256 }
257 "include-system-site-packages" => {
258 include_system_site_packages = value.trim().to_lowercase() == "true";
259 }
260 "version" | "version_info" => {
261 version = Some(
262 PythonVersion::from_str(value.trim())
263 .map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
264 );
265 }
266 _ => {}
267 }
268 }
269
270 Ok(Self {
271 virtualenv,
272 uv,
273 relocatable,
274 seed,
275 include_system_site_packages,
276 version,
277 })
278 }
279
280 pub fn is_virtualenv(&self) -> bool {
282 self.virtualenv
283 }
284
285 pub fn is_uv(&self) -> bool {
287 self.uv
288 }
289
290 pub fn is_relocatable(&self) -> bool {
292 self.relocatable
293 }
294
295 pub fn is_seed(&self) -> bool {
297 self.seed
298 }
299
300 pub fn include_system_site_packages(&self) -> bool {
302 self.include_system_site_packages
303 }
304
305 pub fn set(content: &str, key: &str, value: &str) -> String {
307 let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
308 let mut found = false;
309 for line in &mut lines {
310 if let Some((lhs, _)) = line.split_once('=') {
311 if lhs.trim() == key {
312 *line = Cow::Owned(format!("{key} = {value}"));
313 found = true;
314 break;
315 }
316 }
317 }
318 if !found {
319 lines.push(Cow::Owned(format!("{key} = {value}")));
320 }
321 if lines.is_empty() {
322 String::new()
323 } else {
324 format!("{}\n", lines.join("\n"))
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use std::ffi::OsStr;
332
333 use indoc::indoc;
334 use temp_env::with_vars;
335 use tempfile::tempdir;
336
337 use super::*;
338
339 #[test]
340 fn pixi_environment_is_treated_as_child() {
341 let tempdir = tempdir().unwrap();
342 let prefix = tempdir.path();
343 let conda_meta = prefix.join("conda-meta");
344
345 fs::create_dir_all(&conda_meta).unwrap();
346 fs::write(conda_meta.join("pixi"), []).unwrap();
347
348 let vars = [
349 (EnvVars::CONDA_ROOT, None),
350 (EnvVars::CONDA_PREFIX, Some(prefix.as_os_str())),
351 (EnvVars::CONDA_DEFAULT_ENV, Some(OsStr::new("example"))),
352 ];
353
354 with_vars(vars, || {
355 assert_eq!(
356 CondaEnvironmentKind::from_prefix_path(prefix),
357 CondaEnvironmentKind::Child
358 );
359 });
360 }
361
362 #[test]
363 fn test_set_existing_key() {
364 let content = indoc! {"
365 home = /path/to/python
366 version = 3.8.0
367 include-system-site-packages = false
368 "};
369 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
370 assert_eq!(
371 result,
372 indoc! {"
373 home = /path/to/python
374 version = 3.9.0
375 include-system-site-packages = false
376 "}
377 );
378 }
379
380 #[test]
381 fn test_set_new_key() {
382 let content = indoc! {"
383 home = /path/to/python
384 version = 3.8.0
385 "};
386 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
387 assert_eq!(
388 result,
389 indoc! {"
390 home = /path/to/python
391 version = 3.8.0
392 include-system-site-packages = false
393 "}
394 );
395 }
396
397 #[test]
398 fn test_set_key_no_spaces() {
399 let content = indoc! {"
400 home=/path/to/python
401 version=3.8.0
402 "};
403 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
404 assert_eq!(
405 result,
406 indoc! {"
407 home=/path/to/python
408 version=3.8.0
409 include-system-site-packages = false
410 "}
411 );
412 }
413
414 #[test]
415 fn test_set_key_prefix() {
416 let content = indoc! {"
417 home = /path/to/python
418 home_dir = /other/path
419 "};
420 let result = PyVenvConfiguration::set(content, "home", "new/path");
421 assert_eq!(
422 result,
423 indoc! {"
424 home = new/path
425 home_dir = /other/path
426 "}
427 );
428 }
429
430 #[test]
431 fn test_set_empty_content() {
432 let content = "";
433 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
434 assert_eq!(
435 result,
436 indoc! {"
437 version = 3.9.0
438 "}
439 );
440 }
441}