mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-17 13:58:29 +00:00
Surface the EXTERNALLY-MANAGED
message to users (#2032)
## Summary
Per the
[spec](https://packaging.python.org/en/latest/specifications/externally-managed-environments/),
this message should be surfaced to users:

This commit is contained in:
parent
3116c371a7
commit
995fba8fec
7 changed files with 88 additions and 30 deletions
|
@ -22,6 +22,7 @@ uv-cache = { path = "../uv-cache" }
|
|||
uv-fs = { path = "../uv-fs" }
|
||||
install-wheel-rs = { path = "../install-wheel-rs" }
|
||||
|
||||
configparser = { workspace = true }
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::io::Write;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use configparser::ini::Ini;
|
||||
use fs_err as fs;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -267,20 +268,41 @@ impl Interpreter {
|
|||
self.prefix != self.base_prefix
|
||||
}
|
||||
|
||||
/// Returns `true` if the environment is externally managed.
|
||||
/// Returns `Some` if the environment is externally managed, optionally including an error
|
||||
/// message from the `EXTERNALLY-MANAGED` file.
|
||||
///
|
||||
/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
|
||||
pub fn externally_managed(&self) -> bool {
|
||||
pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
|
||||
// Per the spec, a virtual environment is never externally managed.
|
||||
if self.is_virtualenv() {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Per the spec, the existence of the file is the only requirement.
|
||||
self.sysconfig_paths
|
||||
.stdlib
|
||||
.join("EXTERNALLY-MANAGED")
|
||||
.is_file()
|
||||
let Ok(contents) =
|
||||
fs::read_to_string(self.sysconfig_paths.stdlib.join("EXTERNALLY-MANAGED"))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(mut ini) = Ini::new_cs().read(contents) else {
|
||||
// If a file exists but is not a valid INI file, we assume the environment is
|
||||
// externally managed.
|
||||
return Some(ExternallyManaged::default());
|
||||
};
|
||||
|
||||
let Some(section) = ini.get_mut("externally-managed") else {
|
||||
// If the file exists but does not contain an "externally-managed" section, we assume
|
||||
// the environment is externally managed.
|
||||
return Some(ExternallyManaged::default());
|
||||
};
|
||||
|
||||
let Some(error) = section.remove("Error") else {
|
||||
// If the file exists but does not contain an "Error" key, we assume the environment is
|
||||
// externally managed.
|
||||
return Some(ExternallyManaged::default());
|
||||
};
|
||||
|
||||
Some(ExternallyManaged { error })
|
||||
}
|
||||
|
||||
/// Returns the Python version.
|
||||
|
@ -403,6 +425,21 @@ impl Interpreter {
|
|||
}
|
||||
}
|
||||
|
||||
/// The `EXTERNALLY-MANAGED` file in a Python installation.
|
||||
///
|
||||
/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ExternallyManaged {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl ExternallyManaged {
|
||||
/// Return the `EXTERNALLY-MANAGED` error message, if any.
|
||||
pub fn into_error(self) -> Option<String> {
|
||||
self.error
|
||||
}
|
||||
}
|
||||
|
||||
/// The installation paths returned by `sysconfig.get_paths()`.
|
||||
///
|
||||
/// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths>
|
||||
|
|
|
@ -121,9 +121,7 @@ fn find_python(
|
|||
}
|
||||
Err(Error::PyList(error)) => {
|
||||
if error.kind() == std::io::ErrorKind::NotFound {
|
||||
tracing::debug!(
|
||||
"`py` is not installed. Falling back to searching Python on the path"
|
||||
);
|
||||
debug!("`py` is not installed. Falling back to searching Python on the path");
|
||||
// Continue searching for python installations on the path.
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +154,7 @@ fn find_python(
|
|||
return Err(Error::Python2OrOlder);
|
||||
}
|
||||
// Skip over Python 2 or older installation when querying for a recent python installation.
|
||||
tracing::debug!("Found a Python 2 installation that isn't supported by uv, skipping.");
|
||||
debug!("Found a Python 2 installation that isn't supported by uv, skipping.");
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
|
|
|
@ -118,12 +118,19 @@ pub(crate) async fn pip_install(
|
|||
);
|
||||
|
||||
// If the environment is externally managed, abort.
|
||||
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file.
|
||||
if venv.interpreter().externally_managed() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The environment at {} is externally managed",
|
||||
venv.root().normalized_display()
|
||||
));
|
||||
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||
return if let Some(error) = externally_managed.into_error() {
|
||||
Err(anyhow::anyhow!(
|
||||
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
|
||||
venv.root().normalized_display().cyan(),
|
||||
textwrap::indent(&error, " ").green(),
|
||||
))
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
|
||||
venv.root().normalized_display().cyan()
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
let _lock = venv.lock()?;
|
||||
|
|
|
@ -86,12 +86,19 @@ pub(crate) async fn pip_sync(
|
|||
);
|
||||
|
||||
// If the environment is externally managed, abort.
|
||||
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file.
|
||||
if venv.interpreter().externally_managed() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The environment at {} is externally managed",
|
||||
venv.root().normalized_display()
|
||||
));
|
||||
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||
return if let Some(error) = externally_managed.into_error() {
|
||||
Err(anyhow::anyhow!(
|
||||
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
|
||||
venv.root().normalized_display().cyan(),
|
||||
textwrap::indent(&error, " ").green(),
|
||||
))
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
|
||||
venv.root().normalized_display().cyan()
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
let _lock = venv.lock()?;
|
||||
|
|
|
@ -52,12 +52,19 @@ pub(crate) async fn pip_uninstall(
|
|||
);
|
||||
|
||||
// If the environment is externally managed, abort.
|
||||
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file.
|
||||
if venv.interpreter().externally_managed() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The environment at {} is externally managed",
|
||||
venv.root().normalized_display()
|
||||
));
|
||||
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||
return if let Some(error) = externally_managed.into_error() {
|
||||
Err(anyhow::anyhow!(
|
||||
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
|
||||
venv.root().normalized_display().cyan(),
|
||||
textwrap::indent(&error, " ").green(),
|
||||
))
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
|
||||
venv.root().normalized_display().cyan()
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
let _lock = venv.lock()?;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue