diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 80f36dff1..2e3f9762c 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -918,6 +918,27 @@ pub enum InterpreterInfoError { }, #[error("Only Pyodide is support for Emscripten Python")] EmscriptenNotPyodide, + #[error("Python is missing PYTHONHOME. If you are using a managed Python interpreter, this is a known bug (https://github.com/astral-sh/python-build-standalone/issues/380). You can recreate the virtual environment with `{}`.", "uv venv".green())] + PythonHomeNotFound, +} + +impl InterpreterInfoError { + /// Check whether the stderr of `python` matches a known pattern. + pub(crate) fn from_query_stderr(stderr: &str) -> Option { + // If the Python version is too old, we may not even be able to invoke the query script + if stderr.contains("Unknown option: -I") { + return Some(Self::UnsupportedPython); + } + + // Until we fixed the PBS bug, inform the user that this is bug on our side and can be fixed + // with `uv venv`. + // https://github.com/astral-sh/python-build-standalone/issues/380 + if stderr.contains("ModuleNotFoundError: No module named 'encodings'") { + return Some(Self::PythonHomeNotFound); + } + + None + } } #[allow(clippy::struct_excessive_bools)] @@ -994,10 +1015,9 @@ impl InterpreterInfo { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - // If the Python version is too old, we may not even be able to invoke the query script - if stderr.contains("Unknown option: -I") { + if let Some(query_error) = InterpreterInfoError::from_query_stderr(&stderr) { return Err(Error::QueryScript { - err: InterpreterInfoError::UnsupportedPython, + err: query_error, path: interpreter.to_path_buf(), }); } @@ -1014,20 +1034,19 @@ impl InterpreterInfo { serde_json::from_slice(&output.stdout).map_err(|err| { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - // If the Python version is too old, we may not even be able to invoke the query script - if stderr.contains("Unknown option: -I") { - Error::QueryScript { - err: InterpreterInfoError::UnsupportedPython, + if let Some(query_error) = InterpreterInfoError::from_query_stderr(&stderr) { + return Error::QueryScript { + err: query_error, path: interpreter.to_path_buf(), - } - } else { - Error::UnexpectedResponse(UnexpectedResponseError { - err, - stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(), - stderr, - path: interpreter.to_path_buf(), - }) + }; } + + Error::UnexpectedResponse(UnexpectedResponseError { + err, + stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(), + stderr, + path: interpreter.to_path_buf(), + }) })?; match result { diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 98ca7756b..ddb331d91 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::{env, path::Path, process::Command}; use crate::common::{TestContext, uv_snapshot}; +use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::{ assert::PathAssert, @@ -3950,3 +3951,48 @@ fn python_install_upgrade_version_file() { hint: The version request came from a `.python-version` file; change the patch version in the file to upgrade instead "); } + +/// Show a fitting error message for +/// . +#[cfg(unix)] +#[test] +fn missing_python_home_error_message() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create a Python project so we can use `uv sync` + context.init().assert().success(); + + // Create a broken venv from a symlink. + let sys_executable = fs_err::canonicalize(context.venv.join("bin").join("python"))?; + fs_err::os::unix::fs::symlink(sys_executable, context.temp_dir.join("python-link"))?; + fs_err::remove_dir_all(context.venv.as_ref())?; + Command::new(context.temp_dir.join("python-link")) + .arg("-m") + .arg("venv") + .arg("--without-pip") + .arg(context.venv.as_ref()) + .assert() + .success(); + + uv_snapshot!(context.filters(), context.pip_list().arg("-p").arg(context.venv.join("bin").join("python")), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to inspect Python interpreter from provided path at `.venv/bin/python` + Caused by: Can't use Python at `[VENV]/bin/python` + Caused by: Python is missing PYTHONHOME. If you are using a managed Python interpreter, this is a known bug (https://github.com/astral-sh/python-build-standalone/issues/380). You can recreate the virtual environment with `uv venv`. + "); + + // By default, we skip broken interpreters + uv_snapshot!(context.filters(), context.pip_list(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + Ok(()) +}