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()?;