Files
uv/crates/puffin-interpreter/src/virtual_env.rs
T
Zanie Blue ea4ab29bad Prefer target Python version over current version for builds (#1040)
Extends #1029 
Closes https://github.com/astral-sh/puffin/issues/1038

Instead of always using the current Python version for builds when a
target version is provided, we will do our best to use a compatible
Python version for builds.

Removes behavior where Python versions without patch versions were
always assumed to be the latest known patch version (previously
discussed in https://github.com/astral-sh/puffin/pull/534). While this
was convenient for resolutions which include packages which require
minimum patch versions e.g. `requires-python=">=3.7.4"`, it conflicts
with the idea that the target Python version you provide is the
_minimum_ compatible version. Additionally, it complicates interpreter
lookup as we cannot tell if the user has asked for that specific patch
version or not.
2024-01-24 11:12:02 -06:00

153 lines
4.7 KiB
Rust

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<Self, Error> {
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<Configuration, Error> {
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, std::io::Error> {
LockedFile::acquire(self.root.join(".lock"), self.root.display())
}
}
/// Locate the current virtual environment.
pub(crate) fn detect_virtual_env(target: &PythonPlatform) -> Result<Option<PathBuf>, 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)
}