use std::env; use std::path::{Path, PathBuf}; use tracing::debug; use platform_host::Platform; use puffin_cache::Cache; use puffin_fs::LockedFile; use crate::cfg::Configuration; use crate::python_platform::PythonPlatform; use crate::{Error, Interpreter}; /// A Python executable and its associated platform markers. #[derive(Debug, Clone)] pub struct Virtualenv { root: PathBuf, interpreter: Interpreter, } impl Virtualenv { /// Venv the current Python executable from the host environment. pub fn from_env(platform: Platform, cache: &Cache) -> Result { let platform = PythonPlatform::from(platform); let Some(venv) = detect_virtual_env(&platform)? else { return Err(Error::NotFound); }; let venv = fs_err::canonicalize(venv)?; let executable = platform.venv_python(&venv); let interpreter = Interpreter::query(&executable, &platform.0, cache)?; Ok(Self { root: venv, interpreter, }) } /// Creating a new venv from a Python interpreter changes this. pub fn from_interpreter(interpreter: Interpreter, venv: &Path) -> Self { Self { interpreter: interpreter.with_base_prefix(venv.to_path_buf()), root: venv.to_path_buf(), } } /// Returns the location of the python interpreter pub fn python_executable(&self) -> PathBuf { #[cfg(unix)] { self.root.join("bin").join("python") } #[cfg(windows)] { self.root.join("Scripts").join("python.exe") } #[cfg(not(any(unix, windows)))] { compile_error!("Only windows and unix (linux, mac os, etc.) are supported") } } pub fn root(&self) -> &Path { &self.root } /// Return the [`Interpreter`] for this virtual environment. pub fn interpreter(&self) -> &Interpreter { &self.interpreter } /// Return the [`Configuration`] for this virtual environment, as extracted from the /// `pyvenv.cfg` file. pub fn cfg(&self) -> Result { Ok(Configuration::parse(self.root.join("pyvenv.cfg"))?) } /// Returns the path to the `site-packages` directory inside a virtual environment. pub fn site_packages(&self) -> PathBuf { self.interpreter .platform .venv_site_packages(&self.root, self.interpreter().python_tuple()) } pub fn bin_dir(&self) -> PathBuf { #[cfg(unix)] { self.root().join("bin") } #[cfg(windows)] { self.root().join("Scripts") } #[cfg(not(any(unix, windows)))] { compile_error!("only unix (like mac and linux) and windows are supported") } } /// Lock the virtual environment to prevent concurrent writes. pub fn lock(&self) -> Result { LockedFile::acquire(self.root.join(".lock"), self.root.display()) } } /// Locate the current virtual environment. pub(crate) fn detect_virtual_env(target: &PythonPlatform) -> Result, Error> { match ( env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()), env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()), ) { (Some(dir), None) => { debug!( "Found a virtualenv through VIRTUAL_ENV at: {}", Path::new(&dir).display() ); return Ok(Some(PathBuf::from(dir))); } (None, Some(dir)) => { debug!( "Found a virtualenv through CONDA_PREFIX at: {}", Path::new(&dir).display() ); return Ok(Some(PathBuf::from(dir))); } (Some(venv), Some(conda)) if venv == conda => return Ok(Some(PathBuf::from(venv))), (Some(_), Some(_)) => { return Err(Error::Conflict); } (None, None) => { // No environment variables set. Try to find a virtualenv in the current directory. } }; // Search for a `.venv` directory in the current or any parent directory. let current_dir = env::current_dir().expect("Failed to detect current directory"); for dir in current_dir.ancestors() { let dot_venv = dir.join(".venv"); if dot_venv.is_dir() { if !dot_venv.join("pyvenv.cfg").is_file() { return Err(Error::MissingPyVenvCfg(dot_venv)); } let python = target.venv_python(&dot_venv); if !python.is_file() { return Err(Error::BrokenVenv(dot_venv, python)); } debug!("Found a virtualenv named .venv at: {}", dot_venv.display()); return Ok(Some(dot_venv)); } } Ok(None) }