diff --git a/crates/uv-interpreter/src/environment/python_environment.rs b/crates/uv-interpreter/src/environment.rs similarity index 70% rename from crates/uv-interpreter/src/environment/python_environment.rs rename to crates/uv-interpreter/src/environment.rs index 2518c5b3e..33b785ce3 100644 --- a/crates/uv-interpreter/src/environment/python_environment.rs +++ b/crates/uv-interpreter/src/environment.rs @@ -3,12 +3,11 @@ use std::env; use std::path::{Path, PathBuf}; use same_file::is_same_file; -use tracing::{debug, info}; use uv_cache::Cache; use uv_fs::{LockedFile, Simplified}; -use crate::environment::cfg::PyVenvConfiguration; +use crate::virtualenv::{detect_virtualenv, virtualenv_python_executable, PyVenvConfiguration}; use crate::{find_default_python, find_requested_python, Error, Interpreter, Target}; /// A Python environment, consisting of a Python [`Interpreter`] and its associated paths. @@ -21,11 +20,11 @@ pub struct PythonEnvironment { impl PythonEnvironment { /// Create a [`PythonEnvironment`] for an existing virtual environment. pub fn from_virtualenv(cache: &Cache) -> Result { - let Some(venv) = detect_virtual_env()? else { + let Some(venv) = detect_virtualenv()? else { return Err(Error::VenvNotFound); }; let venv = fs_err::canonicalize(venv)?; - let executable = detect_python_executable(&venv); + let executable = virtualenv_python_executable(&venv); let interpreter = Interpreter::query(&executable, cache)?; debug_assert!( @@ -152,61 +151,3 @@ impl PythonEnvironment { self.interpreter } } - -/// Locate the current virtual environment. -pub(crate) fn detect_virtual_env() -> Result, Error> { - if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) { - info!( - "Found a virtualenv through VIRTUAL_ENV at: {}", - Path::new(&dir).display() - ); - return Ok(Some(PathBuf::from(dir))); - } - if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) { - info!( - "Found a virtualenv through CONDA_PREFIX at: {}", - Path::new(&dir).display() - ); - return Ok(Some(PathBuf::from(dir))); - } - - // 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)); - } - debug!("Found a virtualenv named .venv at: {}", dot_venv.display()); - return Ok(Some(dot_venv)); - } - } - - Ok(None) -} - -/// Returns the path to the `python` executable inside a virtual environment. -pub(crate) fn detect_python_executable(venv: impl AsRef) -> PathBuf { - let venv = venv.as_ref(); - if cfg!(windows) { - // Search for `python.exe` in the `Scripts` directory. - let executable = venv.join("Scripts").join("python.exe"); - if executable.exists() { - return executable; - } - - // Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout. - // See: https://github.com/PyO3/maturin/issues/1108 - let executable = venv.join("bin").join("python.exe"); - if executable.exists() { - return executable; - } - - // Fallback for Conda environments. - venv.join("python.exe") - } else { - // Search for `python` in the `bin` directory. - venv.join("bin").join("python") - } -} diff --git a/crates/uv-interpreter/src/environment/cfg.rs b/crates/uv-interpreter/src/environment/cfg.rs deleted file mode 100644 index 0cf15a96e..000000000 --- a/crates/uv-interpreter/src/environment/cfg.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::path::Path; - -use fs_err as fs; -use thiserror::Error; - -/// A parsed `pyvenv.cfg` -#[derive(Debug, Clone)] -pub struct PyVenvConfiguration { - /// The version of the `virtualenv` package used to create the virtual environment, if any. - pub(crate) virtualenv: bool, - /// The version of the `uv` package used to create the virtual environment, if any. - pub(crate) uv: bool, -} - -impl PyVenvConfiguration { - /// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`]. - pub fn parse(cfg: impl AsRef) -> Result { - let mut virtualenv = false; - let mut uv = false; - - // Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a - // valid INI file, and is instead expected to be parsed by partitioning each line on the - // first equals sign. - let content = fs::read_to_string(&cfg)?; - for line in content.lines() { - let Some((key, _value)) = line.split_once('=') else { - continue; - }; - match key.trim() { - "virtualenv" => { - virtualenv = true; - } - "uv" => { - uv = true; - } - _ => {} - } - } - - Ok(Self { virtualenv, uv }) - } - - /// Returns true if the virtual environment was created with the `virtualenv` package. - pub fn is_virtualenv(&self) -> bool { - self.virtualenv - } - - /// Returns true if the virtual environment was created with the `uv` package. - pub fn is_uv(&self) -> bool { - self.uv - } -} - -#[derive(Debug, Error)] -pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), -} diff --git a/crates/uv-interpreter/src/environment/mod.rs b/crates/uv-interpreter/src/environment/mod.rs deleted file mode 100644 index 004e68269..000000000 --- a/crates/uv-interpreter/src/environment/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod cfg; -pub(crate) mod python_environment; -pub(crate) mod virtualenv; diff --git a/crates/uv-interpreter/src/environment/virtualenv.rs b/crates/uv-interpreter/src/environment/virtualenv.rs deleted file mode 100644 index 64f74003d..000000000 --- a/crates/uv-interpreter/src/environment/virtualenv.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::path::PathBuf; - -use pypi_types::Scheme; - -/// The layout of a virtual environment. -#[derive(Debug)] -pub struct Virtualenv { - /// The absolute path to the root of the virtualenv, e.g., `/path/to/.venv`. - pub root: PathBuf, - - /// The path to the Python interpreter inside the virtualenv, e.g., `.venv/bin/python` - /// (Unix, Python 3.11). - pub executable: PathBuf, - - /// The [`Scheme`] paths for the virtualenv, as returned by (e.g.) `sysconfig.get_paths()`. - pub scheme: Scheme, -} diff --git a/crates/uv-interpreter/src/find_python.rs b/crates/uv-interpreter/src/find_python.rs index 494fdd5cb..7b0cb7c27 100644 --- a/crates/uv-interpreter/src/find_python.rs +++ b/crates/uv-interpreter/src/find_python.rs @@ -8,9 +8,9 @@ use tracing::{debug, instrument}; use uv_cache::Cache; use uv_warnings::warn_user_once; -use crate::environment::python_environment::{detect_python_executable, detect_virtual_env}; use crate::interpreter::InterpreterInfoError; use crate::py_launcher::{py_list_paths, Error as PyLauncherError, PyListPath}; +use crate::virtualenv::{detect_virtualenv, virtualenv_python_executable}; use crate::PythonVersion; use crate::{Error, Interpreter}; @@ -506,8 +506,8 @@ fn find_version( // Check if the venv Python matches. if !system { - if let Some(venv) = detect_virtual_env()? { - let executable = detect_python_executable(venv); + if let Some(venv) = detect_virtualenv()? { + let executable = virtualenv_python_executable(venv); let interpreter = Interpreter::query(executable, cache)?; if version_matches(&interpreter) { diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index 0270424af..7a134d9da 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -17,7 +17,7 @@ use pypi_types::Scheme; use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp}; use uv_fs::{write_atomic_sync, PythonExt, Simplified}; -use crate::{Error, PythonVersion, Target, Virtualenv}; +use crate::{Error, PythonVersion, Target, VirtualEnvironment}; /// A Python executable and its associated platform markers. #[derive(Debug, Clone)] @@ -98,7 +98,7 @@ impl Interpreter { /// Return a new [`Interpreter`] with the given virtual environment root. #[must_use] - pub fn with_virtualenv(self, virtualenv: Virtualenv) -> Self { + pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self { Self { scheme: virtualenv.scheme, sys_executable: virtualenv.executable, diff --git a/crates/uv-interpreter/src/lib.rs b/crates/uv-interpreter/src/lib.rs index b382c4d4d..10a29b4ac 100644 --- a/crates/uv-interpreter/src/lib.rs +++ b/crates/uv-interpreter/src/lib.rs @@ -14,14 +14,13 @@ use std::process::ExitStatus; use thiserror::Error; -pub use crate::environment::cfg::PyVenvConfiguration; -pub use crate::environment::python_environment::PythonEnvironment; -pub use crate::environment::virtualenv::Virtualenv; +pub use crate::environment::PythonEnvironment; pub use crate::find_python::{find_best_python, find_default_python, find_requested_python}; pub use crate::interpreter::Interpreter; use crate::interpreter::InterpreterInfoError; pub use crate::python_version::PythonVersion; pub use crate::target::Target; +pub use crate::virtualenv::{PyVenvConfiguration, VirtualEnvironment}; mod environment; mod find_python; @@ -32,6 +31,7 @@ pub mod platform; mod py_launcher; mod python_version; mod target; +mod virtualenv; #[derive(Debug, Error)] pub enum Error { @@ -77,7 +77,7 @@ pub enum Error { #[error("Failed to write to cache")] Encode(#[from] rmp_serde::encode::Error), #[error("Broken virtualenv: Failed to parse pyvenv.cfg")] - Cfg(#[from] environment::cfg::Error), + Cfg(#[from] virtualenv::Error), #[error("Error finding `{}` in PATH", _0.to_string_lossy())] WhichError(OsString, #[source] which::Error), #[error("Can't use Python at `{interpreter}`")] diff --git a/crates/uv-interpreter/src/virtualenv.rs b/crates/uv-interpreter/src/virtualenv.rs new file mode 100644 index 000000000..e87d248b4 --- /dev/null +++ b/crates/uv-interpreter/src/virtualenv.rs @@ -0,0 +1,156 @@ +use std::{ + env, io, + path::{Path, PathBuf}, +}; + +use fs_err as fs; +use pypi_types::Scheme; +use thiserror::Error; +use tracing::{debug, info}; + +/// The layout of a virtual environment. +#[derive(Debug)] +pub struct VirtualEnvironment { + /// The absolute path to the root of the virtualenv, e.g., `/path/to/.venv`. + pub root: PathBuf, + + /// The path to the Python interpreter inside the virtualenv, e.g., `.venv/bin/python` + /// (Unix, Python 3.11). + pub executable: PathBuf, + + /// The [`Scheme`] paths for the virtualenv, as returned by (e.g.) `sysconfig.get_paths()`. + pub scheme: Scheme, +} + +/// A parsed `pyvenv.cfg` +#[derive(Debug, Clone)] +pub struct PyVenvConfiguration { + /// If the `virtualenv` package was used to create the virtual environment. + pub(crate) virtualenv: bool, + /// If the `uv` package was used to create the virtual environment. + pub(crate) uv: bool, +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Broken virtualenv `{0}`: `pyvenv.cfg` is missing")] + MissingPyVenvCfg(PathBuf), + #[error("Broken virtualenv `{0}`: `pyvenv.cfg` could not be parsed")] + ParsePyVenvCfg(PathBuf, #[source] io::Error), +} + +/// Locate the current virtual environment. +pub(crate) fn detect_virtualenv() -> Result, Error> { + let from_env = virtualenv_from_env(); + if from_env.is_some() { + return Ok(from_env); + } + virtualenv_from_working_dir() +} + +/// Locate an active virtual environment by inspecting environment variables. +/// +/// Supports `VIRTUAL_ENV` and `CONDA_PREFIX`. +pub(crate) fn virtualenv_from_env() -> Option { + if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) { + info!( + "Found a virtualenv through VIRTUAL_ENV at: {}", + Path::new(&dir).display() + ); + return Some(PathBuf::from(dir)); + } + + if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) { + info!( + "Found a virtualenv through CONDA_PREFIX at: {}", + Path::new(&dir).display() + ); + return Some(PathBuf::from(dir)); + } + + None +} + +/// Locate a virtual environment by searching the file system. +/// +/// Finds a `.venv` directory in the current or any parent directory. +pub(crate) fn virtualenv_from_working_dir() -> Result, Error> { + 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)); + } + debug!("Found a virtualenv named .venv at: {}", dot_venv.display()); + return Ok(Some(dot_venv)); + } + } + + Ok(None) +} + +/// Returns the path to the `python` executable inside a virtual environment. +pub(crate) fn virtualenv_python_executable(venv: impl AsRef) -> PathBuf { + let venv = venv.as_ref(); + if cfg!(windows) { + // Search for `python.exe` in the `Scripts` directory. + let executable = venv.join("Scripts").join("python.exe"); + if executable.exists() { + return executable; + } + + // Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout. + // See: https://github.com/PyO3/maturin/issues/1108 + let executable = venv.join("bin").join("python.exe"); + if executable.exists() { + return executable; + } + + // Fallback for Conda environments. + venv.join("python.exe") + } else { + // Search for `python` in the `bin` directory. + venv.join("bin").join("python") + } +} + +impl PyVenvConfiguration { + /// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`]. + pub fn parse(cfg: impl AsRef) -> Result { + let mut virtualenv = false; + let mut uv = false; + + // Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a + // valid INI file, and is instead expected to be parsed by partitioning each line on the + // first equals sign. + 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() { + "virtualenv" => { + virtualenv = true; + } + "uv" => { + uv = true; + } + _ => {} + } + } + + Ok(Self { virtualenv, uv }) + } + + /// Returns true if the virtual environment was created with the `virtualenv` package. + pub fn is_virtualenv(&self) -> bool { + self.virtualenv + } + + /// Returns true if the virtual environment was created with the `uv` package. + pub fn is_uv(&self) -> bool { + self.uv + } +} diff --git a/crates/uv-virtualenv/src/bare.rs b/crates/uv-virtualenv/src/bare.rs index 429a62de5..1e912c87b 100644 --- a/crates/uv-virtualenv/src/bare.rs +++ b/crates/uv-virtualenv/src/bare.rs @@ -13,7 +13,7 @@ use tracing::info; use crate::{Error, Prompt}; use uv_fs::{cachedir, Simplified}; -use uv_interpreter::{Interpreter, Virtualenv}; +use uv_interpreter::{Interpreter, VirtualEnvironment}; use uv_version::version; /// The bash activate scripts with the venv dependent paths patches out @@ -48,7 +48,7 @@ pub fn create_bare_venv( prompt: Prompt, system_site_packages: bool, force: bool, -) -> Result { +) -> Result { // Determine the base Python executable; that is, the Python executable that should be // considered the "base" for the virtual environment. This is typically the Python executable // from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then @@ -258,7 +258,7 @@ pub fn create_bare_venv( fs::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?; fs::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?; - Ok(Virtualenv { + Ok(VirtualEnvironment { scheme: Scheme { purelib: location.join(&interpreter.virtualenv().purelib), platlib: location.join(&interpreter.virtualenv().platlib),