diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 644215f100394..52776668dd810 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -779,7 +779,7 @@ impl Display for StatusCodeError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "Querying Python at `{}` failed with exit status {}", + "Querying Python at `{}` failed with {}", self.path.display(), self.code )?; @@ -903,6 +903,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)] @@ -979,10 +1000,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(), }); } @@ -999,20 +1019,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, - path: interpreter.to_path_buf(), - } - } else { - Error::UnexpectedResponse(UnexpectedResponseError { - err, - stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(), - stderr, + if let Some(query_error) = InterpreterInfoError::from_query_stderr(&stderr) { + return Error::QueryScript { + err: query_error, 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 ada7c727a9017..1ac51170c94e1 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -3650,3 +3650,48 @@ fn python_install_build_version_pypy() { error: No download found for request: pypy-3.10-[PLATFORM] "); } + +/// Show a fitting error message for +/// . +#[cfg(unix)] +#[test] +fn missing_python_home_error_message() -> anyhow::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(()) +}