Centralize virtualenv path construction (#2102)

## Summary

Right now, we have virtualenv construction encoded in a few different
places. Namely, it happens in both `gourgeist` and
`virtualenv_layout.rs` -- _and_ `interpreter.rs` also encodes some
knowledge about how they work, by way of reconstructing the
`SysconfigPaths`.

Instead, `gourgeist` now returns the complete layout, enumerating all
the directories it created. So, rather than returning a root directory,
and re-creating all those paths in `uv-interpreter`, we pass the data
directly back to it.
This commit is contained in:
Charlie Marsh 2024-03-01 10:52:48 -05:00 committed by GitHub
parent c579e6f6bf
commit c9ffe976f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 159 additions and 207 deletions

View file

@ -18,10 +18,10 @@ use platform_tags::{Tags, TagsError};
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
use uv_fs::write_atomic_sync;
use crate::python_environment::detect_virtual_env;
use crate::python_environment::{detect_python_executable, detect_virtual_env};
use crate::python_query::try_find_default_python;
use crate::virtualenv_layout::VirtualenvLayout;
use crate::{find_requested_python, Error, PythonVersion};
use crate::sysconfig::SysconfigPaths;
use crate::{find_requested_python, Error, PythonVersion, Virtualenv};
/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
@ -84,36 +84,11 @@ impl Interpreter {
/// Return a new [`Interpreter`] with the given virtual environment root.
#[must_use]
pub(crate) fn with_venv_root(self, venv_root: PathBuf) -> Self {
let layout = VirtualenvLayout::from_platform(&self.platform);
pub fn with_virtualenv(self, virtualenv: Virtualenv) -> Self {
Self {
// Given that we know `venv_root` is a virtualenv, and not an arbitrary Python
// interpreter, we can safely assume that the platform is the same as the host
// platform. Further, we can safely assume that the paths follow a predictable
// structure, which allows us to avoid querying the interpreter for the `sysconfig`
// paths.
sysconfig_paths: SysconfigPaths {
purelib: layout.site_packages(
&venv_root,
self.site_packages_python(),
self.python_tuple(),
),
platlib: layout.site_packages(
&venv_root,
self.site_packages_python(),
self.python_tuple(),
),
platstdlib: layout.platstdlib(
&venv_root,
self.site_packages_python(),
self.python_tuple(),
),
scripts: layout.scripts(&venv_root),
data: layout.data(&venv_root),
..self.sysconfig_paths
},
sys_executable: layout.python_executable(&venv_root),
prefix: venv_root,
sysconfig_paths: virtualenv.sysconfig_paths,
sys_executable: virtualenv.executable,
prefix: virtualenv.root,
..self
}
}
@ -199,9 +174,8 @@ impl Interpreter {
};
// Check if the venv Python matches.
let python_platform = VirtualenvLayout::from_platform(platform);
if let Some(venv) = detect_virtual_env(&python_platform)? {
let executable = python_platform.python_executable(venv);
if let Some(venv) = detect_virtual_env()? {
let executable = detect_python_executable(venv);
let interpreter = Self::query(&executable, platform.clone(), cache)?;
if version_matches(&interpreter) {
@ -376,6 +350,11 @@ impl Interpreter {
&self.base_prefix
}
/// Return the `sys.prefix` path for this Python interpreter.
pub fn prefix(&self) -> &Path {
&self.prefix
}
/// Return the `sys.executable` path for this Python interpreter.
pub fn sys_executable(&self) -> &Path {
&self.sys_executable
@ -406,6 +385,11 @@ impl Interpreter {
&self.sysconfig_paths.include
}
/// Return the `platinclude` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn platinclude(&self) -> &Path {
&self.sysconfig_paths.platinclude
}
/// Return the `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn stdlib(&self) -> &Path {
&self.sysconfig_paths.stdlib
@ -464,21 +448,6 @@ impl ExternallyManaged {
}
}
/// The installation paths returned by `sysconfig.get_paths()`.
///
/// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths>
#[derive(Debug, Deserialize, Serialize, Clone)]
struct SysconfigPaths {
stdlib: PathBuf,
platstdlib: PathBuf,
purelib: PathBuf,
platlib: PathBuf,
include: PathBuf,
platinclude: PathBuf,
scripts: PathBuf,
data: PathBuf,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct InterpreterInfo {
markers: MarkerEnvironment,

View file

@ -9,20 +9,21 @@ pub use crate::interpreter::Interpreter;
pub use crate::python_environment::PythonEnvironment;
pub use crate::python_query::{find_default_python, find_requested_python};
pub use crate::python_version::PythonVersion;
pub use crate::sysconfig::SysconfigPaths;
pub use crate::virtualenv::Virtualenv;
mod cfg;
mod interpreter;
mod python_environment;
mod python_query;
mod python_version;
mod virtualenv_layout;
mod sysconfig;
mod virtualenv;
#[derive(Debug, Error)]
pub enum Error {
#[error("Expected `{0}` to be a virtualenv, but pyvenv.cfg is missing")]
#[error("Expected `{0}` to be a virtualenv, but `pyvenv.cfg` is missing")]
MissingPyVenvCfg(PathBuf),
#[error("Broken virtualenv `{0}`, it contains a pyvenv.cfg but no Python binary at `{1}`")]
BrokenVenv(PathBuf, PathBuf),
#[error("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.")]
Conflict,
#[error("No versions of Python could be found. Is Python installed?")]

View file

@ -8,7 +8,6 @@ use uv_cache::Cache;
use uv_fs::{LockedFile, Simplified};
use crate::cfg::PyVenvConfiguration;
use crate::virtualenv_layout::VirtualenvLayout;
use crate::{find_default_python, find_requested_python, Error, Interpreter};
/// A Python environment, consisting of a Python [`Interpreter`] and a root directory.
@ -21,12 +20,11 @@ pub struct PythonEnvironment {
impl PythonEnvironment {
/// Create a [`PythonEnvironment`] for an existing virtual environment.
pub fn from_virtualenv(platform: Platform, cache: &Cache) -> Result<Self, Error> {
let layout = VirtualenvLayout::from_platform(&platform);
let Some(venv) = detect_virtual_env(&layout)? else {
let Some(venv) = detect_virtual_env()? else {
return Err(Error::VenvNotFound);
};
let venv = fs_err::canonicalize(venv)?;
let executable = layout.python_executable(&venv);
let executable = detect_python_executable(&venv);
let interpreter = Interpreter::query(&executable, platform, cache)?;
debug_assert!(
@ -42,14 +40,6 @@ impl PythonEnvironment {
})
}
/// Create a [`PythonEnvironment`] for a new virtual environment, created with the given interpreter.
pub fn from_interpreter(interpreter: Interpreter, venv: &Path) -> Self {
Self {
interpreter: interpreter.with_venv_root(venv.to_path_buf()),
root: venv.to_path_buf(),
}
}
/// Create a [`PythonEnvironment`] for a Python interpreter specifier (e.g., a path or a binary name).
pub fn from_requested_python(
python: &str,
@ -74,6 +64,11 @@ impl PythonEnvironment {
})
}
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory.
pub fn from_interpreter(interpreter: Interpreter, root: PathBuf) -> Self {
Self { root, interpreter }
}
/// Returns the location of the Python interpreter.
pub fn root(&self) -> &Path {
&self.root
@ -121,7 +116,7 @@ impl PythonEnvironment {
}
/// Locate the current virtual environment.
pub(crate) fn detect_virtual_env(layout: &VirtualenvLayout) -> Result<Option<PathBuf>, Error> {
pub(crate) fn detect_virtual_env() -> 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()),
@ -157,10 +152,6 @@ pub(crate) fn detect_virtual_env(layout: &VirtualenvLayout) -> Result<Option<Pat
if !dot_venv.join("pyvenv.cfg").is_file() {
return Err(Error::MissingPyVenvCfg(dot_venv));
}
let python = layout.python_executable(&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));
}
@ -168,3 +159,28 @@ pub(crate) fn detect_virtual_env(layout: &VirtualenvLayout) -> Result<Option<Pat
Ok(None)
}
/// Returns the path to the `python` executable inside a virtual environment.
pub(crate) fn detect_python_executable(venv: impl AsRef<Path>) -> 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.to_path_buf()
} else {
// Search for `python` in the `bin` directory.
venv.join("bin").join("python")
}
}

View file

@ -0,0 +1,18 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// The installation paths returned by `sysconfig.get_paths()`.
///
/// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths>
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SysconfigPaths {
pub stdlib: PathBuf,
pub platstdlib: PathBuf,
pub purelib: PathBuf,
pub platlib: PathBuf,
pub include: PathBuf,
pub platinclude: PathBuf,
pub scripts: PathBuf,
pub data: PathBuf,
}

View file

@ -0,0 +1,17 @@
use std::path::PathBuf;
use crate::sysconfig::SysconfigPaths;
/// 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 `sysconfig` paths for the virtualenv, as returned by `sysconfig.get_paths()`.
pub sysconfig_paths: SysconfigPaths,
}

View file

@ -1,83 +0,0 @@
use std::env::consts::EXE_SUFFIX;
use std::path::Path;
use std::path::PathBuf;
use platform_host::{Os, Platform};
/// Construct paths to various locations inside a virtual environment based on the platform.
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct VirtualenvLayout<'a>(&'a Platform);
impl<'a> VirtualenvLayout<'a> {
/// Create a new [`VirtualenvLayout`] for the given platform.
pub(crate) fn from_platform(platform: &'a Platform) -> Self {
Self(platform)
}
/// Returns the path to the `python` executable inside a virtual environment.
pub(crate) fn python_executable(&self, venv_root: impl AsRef<Path>) -> PathBuf {
self.scripts(venv_root).join(format!("python{EXE_SUFFIX}"))
}
/// Returns the directory in which the binaries are stored inside a virtual environment.
pub(crate) fn scripts(&self, venv_root: impl AsRef<Path>) -> PathBuf {
let venv = venv_root.as_ref();
if matches!(self.0.os(), Os::Windows) {
let bin_dir = venv.join("Scripts");
if bin_dir.join("python.exe").exists() {
return bin_dir;
}
// Python installed via msys2 on Windows might produce a POSIX-like venv
// See https://github.com/PyO3/maturin/issues/1108
let bin_dir = venv.join("bin");
if bin_dir.join("python.exe").exists() {
return bin_dir;
}
// for conda environment
venv.to_path_buf()
} else {
venv.join("bin")
}
}
/// Returns the path to the `site-packages` directory inside a virtual environment.
pub(crate) fn site_packages(
&self,
venv_root: impl AsRef<Path>,
site_packages_python: &str,
version: (u8, u8),
) -> PathBuf {
let venv = venv_root.as_ref();
if matches!(self.0.os(), Os::Windows) {
venv.join("Lib").join("site-packages")
} else {
venv.join("lib")
.join(format!("{site_packages_python}{}.{}", version.0, version.1))
.join("site-packages")
}
}
/// Returns the path to the `data` directory inside a virtual environment.
#[allow(clippy::unused_self)]
pub(crate) fn data(&self, venv_root: impl AsRef<Path>) -> PathBuf {
venv_root.as_ref().to_path_buf()
}
/// Returns the path to the `platstdlib` directory inside a virtual environment.
#[allow(clippy::unused_self)]
pub(crate) fn platstdlib(
&self,
venv_root: impl AsRef<Path>,
site_packages_python: &str,
version: (u8, u8),
) -> PathBuf {
let venv = venv_root.as_ref();
if matches!(self.0.os(), Os::Windows) {
venv.join("Lib")
} else {
venv.join("lib")
.join(format!("{site_packages_python}{}.{}", version.0, version.1))
.join("site-packages")
}
}
}