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("python");
210 if default_executable.exists() {
211 return default_executable;
212 }
213
214 let executable = venv.join("bin").join("python3");
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 home = None;
229 let mut virtualenv = false;
230 let mut uv = false;
231 let mut relocatable = false;
232 let mut seed = false;
233 let mut include_system_site_packages = true;
234 let mut version = None;
235
236 let content = fs::read_to_string(&cfg)
240 .map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
241 for line in content.lines() {
242 let Some((key, value)) = line.split_once('=') else {
243 continue;
244 };
245 match key.trim() {
246 "home" => {
247 home = Some(PathBuf::from(value.trim()));
248 }
249 "virtualenv" => {
250 virtualenv = true;
251 }
252 "uv" => {
253 uv = true;
254 }
255 "relocatable" => {
256 relocatable = value.trim().to_lowercase() == "true";
257 }
258 "seed" => {
259 seed = value.trim().to_lowercase() == "true";
260 }
261 "include-system-site-packages" => {
262 include_system_site_packages = value.trim().to_lowercase() == "true";
263 }
264 "version" | "version_info" => {
265 version = Some(
266 PythonVersion::from_str(value.trim())
267 .map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
268 );
269 }
270 _ => {}
271 }
272 }
273
274 Ok(Self {
275 home,
276 virtualenv,
277 uv,
278 relocatable,
279 seed,
280 include_system_site_packages,
281 version,
282 })
283 }
284
285 pub fn is_virtualenv(&self) -> bool {
287 self.virtualenv
288 }
289
290 pub fn is_uv(&self) -> bool {
292 self.uv
293 }
294
295 pub(crate) fn is_relocatable(&self) -> bool {
297 self.relocatable
298 }
299
300 pub fn is_seed(&self) -> bool {
302 self.seed
303 }
304
305 pub fn include_system_site_packages(&self) -> bool {
307 self.include_system_site_packages
308 }
309
310 pub fn set(content: &str, key: &str, value: &str) -> String {
312 let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
313 let mut found = false;
314 for line in &mut lines {
315 if let Some((lhs, _)) = line.split_once('=')
316 && lhs.trim() == key
317 {
318 *line = Cow::Owned(format!("{key} = {value}"));
319 found = true;
320 break;
321 }
322 }
323 if !found {
324 lines.push(Cow::Owned(format!("{key} = {value}")));
325 }
326 if lines.is_empty() {
327 String::new()
328 } else {
329 format!("{}\n", lines.join("\n"))
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use std::ffi::OsStr;
337
338 use indoc::indoc;
339 use temp_env::with_vars;
340 use tempfile::tempdir;
341
342 use super::*;
343
344 #[test]
345 fn pixi_environment_is_treated_as_child() {
346 let tempdir = tempdir().unwrap();
347 let prefix = tempdir.path();
348 let conda_meta = prefix.join("conda-meta");
349
350 fs::create_dir_all(&conda_meta).unwrap();
351 fs::write(conda_meta.join("pixi"), []).unwrap();
352
353 let vars = [
354 (EnvVars::CONDA_ROOT, None),
355 (EnvVars::CONDA_PREFIX, Some(prefix.as_os_str())),
356 (EnvVars::CONDA_DEFAULT_ENV, Some(OsStr::new("example"))),
357 ];
358
359 with_vars(vars, || {
360 assert_eq!(
361 CondaEnvironmentKind::from_prefix_path(prefix),
362 CondaEnvironmentKind::Child
363 );
364 });
365 }
366
367 #[test]
368 fn test_set_existing_key() {
369 let content = indoc! {"
370 home = /path/to/python
371 version = 3.8.0
372 include-system-site-packages = false
373 "};
374 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
375 assert_eq!(
376 result,
377 indoc! {"
378 home = /path/to/python
379 version = 3.9.0
380 include-system-site-packages = false
381 "}
382 );
383 }
384
385 #[test]
386 fn test_set_new_key() {
387 let content = indoc! {"
388 home = /path/to/python
389 version = 3.8.0
390 "};
391 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
392 assert_eq!(
393 result,
394 indoc! {"
395 home = /path/to/python
396 version = 3.8.0
397 include-system-site-packages = false
398 "}
399 );
400 }
401
402 #[test]
403 fn test_set_key_no_spaces() {
404 let content = indoc! {"
405 home=/path/to/python
406 version=3.8.0
407 "};
408 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
409 assert_eq!(
410 result,
411 indoc! {"
412 home=/path/to/python
413 version=3.8.0
414 include-system-site-packages = false
415 "}
416 );
417 }
418
419 #[test]
420 fn test_set_key_prefix() {
421 let content = indoc! {"
422 home = /path/to/python
423 home_dir = /other/path
424 "};
425 let result = PyVenvConfiguration::set(content, "home", "new/path");
426 assert_eq!(
427 result,
428 indoc! {"
429 home = new/path
430 home_dir = /other/path
431 "}
432 );
433 }
434
435 #[test]
436 fn test_set_empty_content() {
437 let content = "";
438 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
439 assert_eq!(
440 result,
441 indoc! {"
442 version = 3.9.0
443 "}
444 );
445 }
446}