use std::borrow::Cow;
use std::str::FromStr;
use std::{
env, io,
path::{Path, PathBuf},
};
use fs_err as fs;
use thiserror::Error;
use uv_preview::{Preview, PreviewFeature};
use uv_pypi_types::Scheme;
use uv_static::EnvVars;
use crate::PythonVersion;
#[derive(Debug)]
pub struct VirtualEnvironment {
pub root: PathBuf,
pub executable: PathBuf,
pub base_executable: PathBuf,
pub scheme: Scheme,
}
#[derive(Debug, Clone)]
pub struct PyVenvConfiguration {
pub(crate) home: Option<PathBuf>,
pub(crate) virtualenv: bool,
pub(crate) uv: bool,
pub(crate) relocatable: bool,
pub(crate) seed: bool,
pub(crate) include_system_site_packages: bool,
pub(crate) version: Option<PythonVersion>,
}
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error("Broken virtual environment `{0}`: `pyvenv.cfg` is missing")]
MissingPyVenvCfg(PathBuf),
#[error("Broken virtual environment `{0}`: `pyvenv.cfg` could not be parsed")]
ParsePyVenvCfg(PathBuf, #[source] io::Error),
}
pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
if let Some(dir) = env::var_os(EnvVars::VIRTUAL_ENV).filter(|value| !value.is_empty()) {
return Some(PathBuf::from(dir));
}
None
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum CondaEnvironmentKind {
Base,
Child,
}
impl CondaEnvironmentKind {
fn from_prefix_path(path: &Path, preview: Preview) -> Self {
if is_pixi_environment(path) {
return Self::Child;
}
if let Ok(conda_root) = env::var(EnvVars::CONDA_ROOT) {
if path == Path::new(&conda_root) {
return Self::Base;
}
}
let Ok(current_env) = env::var(EnvVars::CONDA_DEFAULT_ENV) else {
return Self::Child;
};
if path == Path::new(¤t_env) {
return Self::Child;
}
if !preview.is_enabled(PreviewFeature::SpecialCondaEnvNames)
&& (current_env == "base" || current_env == "root")
{
return Self::Base;
}
let Some(name) = path.file_name() else {
return Self::Child;
};
if name.to_str().is_some_and(|name| name == current_env) {
Self::Child
} else {
Self::Base
}
}
}
fn is_pixi_environment(path: &Path) -> bool {
path.join("conda-meta").join("pixi").is_file()
}
pub(crate) fn conda_environment_from_env(
kind: CondaEnvironmentKind,
preview: Preview,
) -> Option<PathBuf> {
let dir = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty())?;
let path = PathBuf::from(dir);
if kind != CondaEnvironmentKind::from_prefix_path(&path, preview) {
return None;
}
Some(path)
}
pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
let current_dir = crate::current_dir()?;
for dir in current_dir.ancestors() {
if uv_fs::is_virtualenv_base(dir) {
return Ok(Some(dir.to_path_buf()));
}
let dot_venv = dir.join(".venv");
if dot_venv.is_dir() {
if !uv_fs::is_virtualenv_base(&dot_venv) {
return Err(Error::MissingPyVenvCfg(dot_venv));
}
return Ok(Some(dot_venv));
}
}
Ok(None)
}
pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
let venv = venv.as_ref();
if cfg!(windows) {
let default_executable = venv.join("Scripts").join("python.exe");
if default_executable.exists() {
return default_executable;
}
let executable = venv.join("bin").join("python.exe");
if executable.exists() {
return executable;
}
let executable = venv.join("python.exe");
if executable.exists() {
return executable;
}
default_executable
} else {
let default_executable = venv.join("bin").join("python3");
if default_executable.exists() {
return default_executable;
}
let executable = venv.join("bin").join("python");
if executable.exists() {
return executable;
}
default_executable
}
}
impl PyVenvConfiguration {
pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
let mut home = None;
let mut virtualenv = false;
let mut uv = false;
let mut relocatable = false;
let mut seed = false;
let mut include_system_site_packages = true;
let mut version = None;
let content = fs::read_to_string(&cfg)
.map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
for line in content.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key.trim() {
"home" => {
home = Some(PathBuf::from(value.trim()));
}
"virtualenv" => {
virtualenv = true;
}
"uv" => {
uv = true;
}
"relocatable" => {
relocatable = value.trim().to_lowercase() == "true";
}
"seed" => {
seed = value.trim().to_lowercase() == "true";
}
"include-system-site-packages" => {
include_system_site_packages = value.trim().to_lowercase() == "true";
}
"version" | "version_info" => {
version = Some(
PythonVersion::from_str(value.trim())
.map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
);
}
_ => {}
}
}
Ok(Self {
home,
virtualenv,
uv,
relocatable,
seed,
include_system_site_packages,
version,
})
}
pub fn is_virtualenv(&self) -> bool {
self.virtualenv
}
pub fn is_uv(&self) -> bool {
self.uv
}
pub fn is_relocatable(&self) -> bool {
self.relocatable
}
pub fn is_seed(&self) -> bool {
self.seed
}
pub fn include_system_site_packages(&self) -> bool {
self.include_system_site_packages
}
pub fn set(content: &str, key: &str, value: &str) -> String {
let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
let mut found = false;
for line in &mut lines {
if let Some((lhs, _)) = line.split_once('=') {
if lhs.trim() == key {
*line = Cow::Owned(format!("{key} = {value}"));
found = true;
break;
}
}
}
if !found {
lines.push(Cow::Owned(format!("{key} = {value}")));
}
if lines.is_empty() {
String::new()
} else {
format!("{}\n", lines.join("\n"))
}
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use indoc::indoc;
use temp_env::with_vars;
use tempfile::tempdir;
use super::*;
#[test]
fn pixi_environment_is_treated_as_child() {
let tempdir = tempdir().unwrap();
let prefix = tempdir.path();
let conda_meta = prefix.join("conda-meta");
fs::create_dir_all(&conda_meta).unwrap();
fs::write(conda_meta.join("pixi"), []).unwrap();
let vars = [
(EnvVars::CONDA_ROOT, None),
(EnvVars::CONDA_PREFIX, Some(prefix.as_os_str())),
(EnvVars::CONDA_DEFAULT_ENV, Some(OsStr::new("example"))),
];
with_vars(vars, || {
assert_eq!(
CondaEnvironmentKind::from_prefix_path(prefix, Preview::default()),
CondaEnvironmentKind::Child
);
});
}
#[test]
fn test_set_existing_key() {
let content = indoc! {"
home = /path/to/python
version = 3.8.0
include-system-site-packages = false
"};
let result = PyVenvConfiguration::set(content, "version", "3.9.0");
assert_eq!(
result,
indoc! {"
home = /path/to/python
version = 3.9.0
include-system-site-packages = false
"}
);
}
#[test]
fn test_set_new_key() {
let content = indoc! {"
home = /path/to/python
version = 3.8.0
"};
let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
assert_eq!(
result,
indoc! {"
home = /path/to/python
version = 3.8.0
include-system-site-packages = false
"}
);
}
#[test]
fn test_set_key_no_spaces() {
let content = indoc! {"
home=/path/to/python
version=3.8.0
"};
let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
assert_eq!(
result,
indoc! {"
home=/path/to/python
version=3.8.0
include-system-site-packages = false
"}
);
}
#[test]
fn test_set_key_prefix() {
let content = indoc! {"
home = /path/to/python
home_dir = /other/path
"};
let result = PyVenvConfiguration::set(content, "home", "new/path");
assert_eq!(
result,
indoc! {"
home = new/path
home_dir = /other/path
"}
);
}
#[test]
fn test_set_empty_content() {
let content = "";
let result = PyVenvConfiguration::set(content, "version", "3.9.0");
assert_eq!(
result,
indoc! {"
version = 3.9.0
"}
);
}
}