mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-30 07:17:26 +00:00

## Summary This is causing some cyclic dependencies issues for me, because these can be used in virtually _any_ crate (like `uv-install-wheel`), which then means that all of `uv-configuration` becomes a dependency, etc. I think this should be a leaf crate so that we can safely depend on it anywhere.
368 lines
13 KiB
Rust
368 lines
13 KiB
Rust
use std::borrow::Cow;
|
|
use std::fmt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
|
|
use owo_colors::OwoColorize;
|
|
use tracing::debug;
|
|
|
|
use uv_cache::Cache;
|
|
use uv_fs::{LockedFile, Simplified};
|
|
use uv_pep440::Version;
|
|
use uv_preview::Preview;
|
|
|
|
use crate::discovery::find_python_installation;
|
|
use crate::installation::PythonInstallation;
|
|
use crate::virtualenv::{PyVenvConfiguration, virtualenv_python_executable};
|
|
use crate::{
|
|
EnvironmentPreference, Error, Interpreter, Prefix, PythonNotFound, PythonPreference,
|
|
PythonRequest, Target,
|
|
};
|
|
|
|
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
|
|
#[derive(Debug, Clone)]
|
|
pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct PythonEnvironmentShared {
|
|
root: PathBuf,
|
|
interpreter: Interpreter,
|
|
}
|
|
|
|
/// The result of failed environment discovery.
|
|
///
|
|
/// Generally this is cast from [`PythonNotFound`] by [`PythonEnvironment::find`].
|
|
#[derive(Clone, Debug, Error)]
|
|
pub struct EnvironmentNotFound {
|
|
request: PythonRequest,
|
|
preference: EnvironmentPreference,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Error)]
|
|
pub struct InvalidEnvironment {
|
|
path: PathBuf,
|
|
pub kind: InvalidEnvironmentKind,
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum InvalidEnvironmentKind {
|
|
NotDirectory,
|
|
Empty,
|
|
MissingExecutable(PathBuf),
|
|
}
|
|
|
|
impl From<PythonNotFound> for EnvironmentNotFound {
|
|
fn from(value: PythonNotFound) -> Self {
|
|
Self {
|
|
request: value.request,
|
|
preference: value.environment_preference,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for EnvironmentNotFound {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
#[derive(Debug, Copy, Clone)]
|
|
enum SearchType {
|
|
/// Only virtual environments were searched.
|
|
Virtual,
|
|
/// Only system installations were searched.
|
|
System,
|
|
/// Both virtual and system installations were searched.
|
|
VirtualOrSystem,
|
|
}
|
|
|
|
impl fmt::Display for SearchType {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
Self::Virtual => write!(f, "virtual environment"),
|
|
Self::System => write!(f, "system Python installation"),
|
|
Self::VirtualOrSystem => {
|
|
write!(f, "virtual environment or system Python installation")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let search_type = match self.preference {
|
|
EnvironmentPreference::Any => SearchType::VirtualOrSystem,
|
|
EnvironmentPreference::ExplicitSystem => {
|
|
if self.request.is_explicit_system() {
|
|
SearchType::VirtualOrSystem
|
|
} else {
|
|
SearchType::Virtual
|
|
}
|
|
}
|
|
EnvironmentPreference::OnlySystem => SearchType::System,
|
|
EnvironmentPreference::OnlyVirtual => SearchType::Virtual,
|
|
};
|
|
|
|
if matches!(self.request, PythonRequest::Default | PythonRequest::Any) {
|
|
write!(f, "No {search_type} found")?;
|
|
} else {
|
|
write!(f, "No {search_type} found for {}", self.request)?;
|
|
}
|
|
|
|
match search_type {
|
|
// This error message assumes that the relevant API accepts the `--system` flag. This
|
|
// is true of the callsites today, since the project APIs never surface this error.
|
|
SearchType::Virtual => write!(
|
|
f,
|
|
"; run `{}` to create an environment, or pass `{}` to install into a non-virtual environment",
|
|
"uv venv".green(),
|
|
"--system".green()
|
|
)?,
|
|
SearchType::VirtualOrSystem => {
|
|
write!(f, "; run `{}` to create an environment", "uv venv".green())?;
|
|
}
|
|
SearchType::System => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for InvalidEnvironment {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"Invalid environment at `{}`: {}",
|
|
self.path.user_display(),
|
|
self.kind
|
|
)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for InvalidEnvironmentKind {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
Self::NotDirectory => write!(f, "expected directory but found a file"),
|
|
Self::MissingExecutable(path) => {
|
|
write!(f, "missing Python executable at `{}`", path.user_display())
|
|
}
|
|
Self::Empty => write!(f, "directory is empty"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PythonEnvironment {
|
|
/// Find a [`PythonEnvironment`] matching the given request and preference.
|
|
///
|
|
/// If looking for a Python interpreter to create a new environment, use [`PythonInstallation::find`]
|
|
/// instead.
|
|
pub fn find(
|
|
request: &PythonRequest,
|
|
preference: EnvironmentPreference,
|
|
python_preference: PythonPreference,
|
|
cache: &Cache,
|
|
preview: Preview,
|
|
) -> Result<Self, Error> {
|
|
let installation =
|
|
match find_python_installation(request, preference, python_preference, cache, preview)?
|
|
{
|
|
Ok(installation) => installation,
|
|
Err(err) => return Err(EnvironmentNotFound::from(err).into()),
|
|
};
|
|
Ok(Self::from_installation(installation))
|
|
}
|
|
|
|
/// Create a [`PythonEnvironment`] from the virtual environment at the given root.
|
|
///
|
|
/// N.B. This function also works for system Python environments and users depend on this.
|
|
pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
|
|
debug!(
|
|
"Checking for Python environment at: `{}`",
|
|
root.as_ref().user_display()
|
|
);
|
|
match root.as_ref().try_exists() {
|
|
Ok(true) => {}
|
|
Ok(false) => {
|
|
return Err(Error::MissingEnvironment(EnvironmentNotFound {
|
|
preference: EnvironmentPreference::Any,
|
|
request: PythonRequest::Directory(root.as_ref().to_owned()),
|
|
}));
|
|
}
|
|
Err(err) => return Err(Error::Discovery(err.into())),
|
|
}
|
|
|
|
if root.as_ref().is_file() {
|
|
return Err(InvalidEnvironment {
|
|
path: root.as_ref().to_path_buf(),
|
|
kind: InvalidEnvironmentKind::NotDirectory,
|
|
}
|
|
.into());
|
|
}
|
|
|
|
if root
|
|
.as_ref()
|
|
.read_dir()
|
|
.is_ok_and(|mut dir| dir.next().is_none())
|
|
{
|
|
return Err(InvalidEnvironment {
|
|
path: root.as_ref().to_path_buf(),
|
|
kind: InvalidEnvironmentKind::Empty,
|
|
}
|
|
.into());
|
|
}
|
|
|
|
// Note we do not canonicalize the root path or the executable path, this is important
|
|
// because the path the interpreter is invoked at can determine the value of
|
|
// `sys.executable`.
|
|
let executable = virtualenv_python_executable(&root);
|
|
|
|
// If we can't find an executable, exit before querying to provide a better error.
|
|
if !(executable.is_symlink() || executable.is_file()) {
|
|
return Err(InvalidEnvironment {
|
|
path: root.as_ref().to_path_buf(),
|
|
kind: InvalidEnvironmentKind::MissingExecutable(executable.clone()),
|
|
}
|
|
.into());
|
|
}
|
|
|
|
let interpreter = Interpreter::query(executable, cache)?;
|
|
|
|
Ok(Self(Arc::new(PythonEnvironmentShared {
|
|
root: interpreter.sys_prefix().to_path_buf(),
|
|
interpreter,
|
|
})))
|
|
}
|
|
|
|
/// Create a [`PythonEnvironment`] from an existing [`PythonInstallation`].
|
|
pub fn from_installation(installation: PythonInstallation) -> Self {
|
|
Self::from_interpreter(installation.into_interpreter())
|
|
}
|
|
|
|
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`].
|
|
pub fn from_interpreter(interpreter: Interpreter) -> Self {
|
|
Self(Arc::new(PythonEnvironmentShared {
|
|
root: interpreter.sys_prefix().to_path_buf(),
|
|
interpreter,
|
|
}))
|
|
}
|
|
|
|
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--target` directory.
|
|
pub fn with_target(self, target: Target) -> std::io::Result<Self> {
|
|
let inner = Arc::unwrap_or_clone(self.0);
|
|
Ok(Self(Arc::new(PythonEnvironmentShared {
|
|
interpreter: inner.interpreter.with_target(target)?,
|
|
..inner
|
|
})))
|
|
}
|
|
|
|
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--prefix` directory.
|
|
pub fn with_prefix(self, prefix: Prefix) -> std::io::Result<Self> {
|
|
let inner = Arc::unwrap_or_clone(self.0);
|
|
Ok(Self(Arc::new(PythonEnvironmentShared {
|
|
interpreter: inner.interpreter.with_prefix(prefix)?,
|
|
..inner
|
|
})))
|
|
}
|
|
|
|
/// Returns the root (i.e., `prefix`) of the Python interpreter.
|
|
pub fn root(&self) -> &Path {
|
|
&self.0.root
|
|
}
|
|
|
|
/// Return the [`Interpreter`] for this virtual environment.
|
|
///
|
|
/// See also [`PythonEnvironment::into_interpreter`].
|
|
pub fn interpreter(&self) -> &Interpreter {
|
|
&self.0.interpreter
|
|
}
|
|
|
|
/// Return the [`PyVenvConfiguration`] for this environment, as extracted from the
|
|
/// `pyvenv.cfg` file.
|
|
pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
|
|
Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
|
|
}
|
|
|
|
/// Set a key-value pair in the `pyvenv.cfg` file.
|
|
pub fn set_pyvenv_cfg(&self, key: &str, value: &str) -> Result<(), Error> {
|
|
let content = fs_err::read_to_string(self.0.root.join("pyvenv.cfg"))?;
|
|
fs_err::write(
|
|
self.0.root.join("pyvenv.cfg"),
|
|
PyVenvConfiguration::set(&content, key, value),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns `true` if the environment is "relocatable".
|
|
pub fn relocatable(&self) -> bool {
|
|
self.cfg().is_ok_and(|cfg| cfg.is_relocatable())
|
|
}
|
|
|
|
/// Returns the location of the Python executable.
|
|
pub fn python_executable(&self) -> &Path {
|
|
self.0.interpreter.sys_executable()
|
|
}
|
|
|
|
/// Returns an iterator over the `site-packages` directories inside the environment.
|
|
///
|
|
/// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
|
|
/// a single element; however, in some distributions, they may be different.
|
|
///
|
|
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
|
|
/// still deduplicate the entries, returning a single path.
|
|
pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
|
|
self.0.interpreter.site_packages()
|
|
}
|
|
|
|
/// Returns the path to the `bin` directory inside this environment.
|
|
pub fn scripts(&self) -> &Path {
|
|
self.0.interpreter.scripts()
|
|
}
|
|
|
|
/// Grab a file lock for the environment to prevent concurrent writes across processes.
|
|
pub async fn lock(&self) -> Result<LockedFile, std::io::Error> {
|
|
self.0.interpreter.lock().await
|
|
}
|
|
|
|
/// Return the [`Interpreter`] for this environment.
|
|
///
|
|
/// See also [`PythonEnvironment::interpreter`].
|
|
pub fn into_interpreter(self) -> Interpreter {
|
|
Arc::unwrap_or_clone(self.0).interpreter
|
|
}
|
|
|
|
/// Returns `true` if the [`PythonEnvironment`] uses the same underlying [`Interpreter`].
|
|
pub fn uses(&self, interpreter: &Interpreter) -> bool {
|
|
// TODO(zanieb): Consider using `sysconfig.get_path("stdlib")` instead, which
|
|
// should be generally robust.
|
|
if cfg!(windows) {
|
|
// On Windows, we can't canonicalize an interpreter based on its executable path
|
|
// because the executables are separate shim files (not links). Instead, we
|
|
// compare the `sys.base_prefix`.
|
|
let old_base_prefix = self.interpreter().sys_base_prefix();
|
|
let selected_base_prefix = interpreter.sys_base_prefix();
|
|
old_base_prefix == selected_base_prefix
|
|
} else {
|
|
// On Unix, we can see if the canonicalized executable is the same file.
|
|
self.interpreter().sys_executable() == interpreter.sys_executable()
|
|
|| same_file::is_same_file(
|
|
self.interpreter().sys_executable(),
|
|
interpreter.sys_executable(),
|
|
)
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
/// Check if the `pyvenv.cfg` version is the same as the interpreter's Python version.
|
|
///
|
|
/// Returns [`None`] if the versions are the consistent or there is no `pyvenv.cfg`. If the
|
|
/// versions do not match, returns a tuple of the `pyvenv.cfg` and interpreter's Python versions
|
|
/// for display.
|
|
pub fn get_pyvenv_version_conflict(&self) -> Option<(Version, Version)> {
|
|
let cfg = self.cfg().ok()?;
|
|
let cfg_version = cfg.version?.into_version();
|
|
|
|
// Determine if we should be checking for patch or pre-release equality
|
|
let exe_version = if cfg_version.release().get(2).is_none() {
|
|
self.interpreter().python_minor_version()
|
|
} else if cfg_version.pre().is_none() {
|
|
self.interpreter().python_patch_version()
|
|
} else {
|
|
self.interpreter().python_version().clone()
|
|
};
|
|
|
|
(cfg_version != exe_version).then_some((cfg_version, exe_version))
|
|
}
|
|
}
|