diff --git a/Cargo.lock b/Cargo.lock
index c64c6bb28..24ad8d511 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4524,6 +4524,7 @@ version = "0.0.1"
dependencies = [
"anyhow",
"cache-key",
+ "configparser",
"fs-err",
"indoc",
"insta",
diff --git a/crates/uv-interpreter/Cargo.toml b/crates/uv-interpreter/Cargo.toml
index f1d992416..b649d6f46 100644
--- a/crates/uv-interpreter/Cargo.toml
+++ b/crates/uv-interpreter/Cargo.toml
@@ -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 }
diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs
index eb2fe2827..8df520e39 100644
--- a/crates/uv-interpreter/src/interpreter.rs
+++ b/crates/uv-interpreter/src/interpreter.rs
@@ -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:
- pub fn externally_managed(&self) -> bool {
+ pub fn is_externally_managed(&self) -> Option {
// 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:
+#[derive(Debug, Default, Clone)]
+pub struct ExternallyManaged {
+ error: Option,
+}
+
+impl ExternallyManaged {
+ /// Return the `EXTERNALLY-MANAGED` error message, if any.
+ pub fn into_error(self) -> Option {
+ self.error
+ }
+}
+
/// The installation paths returned by `sysconfig.get_paths()`.
///
/// See:
diff --git a/crates/uv-interpreter/src/python_query.rs b/crates/uv-interpreter/src/python_query.rs
index 9768857ad..b562145fd 100644
--- a/crates/uv-interpreter/src/python_query.rs
+++ b/crates/uv-interpreter/src/python_query.rs
@@ -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),
diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs
index 9fd1240a1..e760c6268 100644
--- a/crates/uv/src/commands/pip_install.rs
+++ b/crates/uv/src/commands/pip_install.rs
@@ -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()?;
diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs
index 684bf7197..bf351ecc7 100644
--- a/crates/uv/src/commands/pip_sync.rs
+++ b/crates/uv/src/commands/pip_sync.rs
@@ -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()?;
diff --git a/crates/uv/src/commands/pip_uninstall.rs b/crates/uv/src/commands/pip_uninstall.rs
index dd18a4100..15a982547 100644
--- a/crates/uv/src/commands/pip_uninstall.rs
+++ b/crates/uv/src/commands/pip_uninstall.rs
@@ -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()?;