Skip over broken tool Python on Windows too (#17176)

Fixes #16252
This commit is contained in:
konsti
2026-03-05 10:02:49 +01:00
committed by GitHub
parent 02e92aed3b
commit 8efb5421bc
9 changed files with 177 additions and 38 deletions
+4 -4
View File
@@ -42,7 +42,7 @@ use crate::virtualenv::{
};
#[cfg(windows)]
use crate::windows_registry::{WindowsPython, registry_pythons};
use crate::{BrokenSymlink, Interpreter, PythonVersion};
use crate::{BrokenLink, Interpreter, PythonVersion};
/// A request to find a Python installation.
///
@@ -1055,7 +1055,7 @@ impl Error {
false
}
InterpreterError::NotFound(path)
| InterpreterError::BrokenSymlink(BrokenSymlink { path, .. }) => {
| InterpreterError::BrokenLink(BrokenLink { path, .. }) => {
// If the interpreter is from an active, valid virtual environment, we should
// fail because it's broken
if matches!(source, PythonSource::ActiveEnvironment)
@@ -1130,7 +1130,7 @@ pub fn find_python_installations<'a>(
debug!("Checking for Python interpreter at {request}");
match python_installation_from_executable(path, cache) {
Ok(installation) => Ok(Ok(installation)),
Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
Ok(Err(PythonNotFound {
request: request.clone(),
python_preference: preference,
@@ -1156,7 +1156,7 @@ pub fn find_python_installations<'a>(
debug!("Checking for Python interpreter in {request}");
match python_installation_from_directory(path, cache) {
Ok(installation) => Ok(Ok(installation)),
Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
Ok(Err(PythonNotFound {
request: request.clone(),
python_preference: preference,
+43 -11
View File
@@ -32,8 +32,8 @@ use crate::implementation::LenientImplementationName;
use crate::managed::ManagedPythonInstallations;
use crate::pointer_size::PointerSize;
use crate::{
Prefix, PythonInstallationKey, PythonVariant, PythonVersion, Target, VersionRequest,
VirtualEnvironment,
Prefix, PyVenvConfiguration, PythonInstallationKey, PythonVariant, PythonVersion, Target,
VersionRequest, VirtualEnvironment,
};
#[cfg(windows)]
@@ -824,7 +824,7 @@ pub enum Error {
#[error("Failed to query Python interpreter")]
Io(#[from] io::Error),
#[error(transparent)]
BrokenSymlink(BrokenSymlink),
BrokenLink(BrokenLink),
#[error("Python interpreter not found at `{0}`")]
NotFound(PathBuf),
#[error("Failed to query Python interpreter at `{path}`")]
@@ -861,19 +861,30 @@ pub enum Error {
}
#[derive(Debug, Error)]
pub struct BrokenSymlink {
pub struct BrokenLink {
pub path: PathBuf,
/// Whether we have a broken symlink (Unix) or whether the shim returned that the underlying
/// Python went away (Windows).
pub unix: bool,
/// Whether the interpreter path looks like a virtual environment.
pub venv: bool,
}
impl Display for BrokenSymlink {
impl Display for BrokenLink {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Broken symlink at `{}`, was the underlying Python interpreter removed?",
self.path.user_display()
)?;
if self.unix {
write!(
f,
"Broken symlink at `{}`, was the underlying Python interpreter removed?",
self.path.user_display()
)?;
} else {
write!(
f,
"Broken Python trampoline at `{}`, was the underlying Python interpreter removed?",
self.path.user_display()
)?;
}
if self.venv {
write!(
f,
@@ -994,6 +1005,19 @@ impl InterpreterInfo {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
// Handle uninstalled CPython interpreters on Windows.
//
// The IO error from the CPython trampoline is unstructured and localized, so we check
// whether the `home` from `pyvenv.cfg` still exists, it's missing if the Python
// interpreter was uninstalled.
if python_home(interpreter).is_some_and(|home| !home.exists()) {
return Err(Error::BrokenLink(BrokenLink {
path: interpreter.to_path_buf(),
unix: false,
venv: uv_fs::is_virtualenv_executable(interpreter),
}));
}
// 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 Err(Error::QueryScript {
@@ -1092,8 +1116,9 @@ impl InterpreterInfo {
.symlink_metadata()
.is_ok_and(|metadata| metadata.is_symlink())
{
Error::BrokenSymlink(BrokenSymlink {
Error::BrokenLink(BrokenLink {
path: executable.to_path_buf(),
unix: true,
venv: uv_fs::is_virtualenv_executable(executable),
})
} else {
@@ -1273,6 +1298,13 @@ fn find_base_python(
}
}
/// Parse the `home` key from `pyvenv.cfg`, if any.
fn python_home(interpreter: &Path) -> Option<PathBuf> {
let venv_root = interpreter.parent()?.parent()?;
let pyvenv_cfg = PyVenvConfiguration::parse(venv_root.join("pyvenv.cfg")).ok()?;
pyvenv_cfg.home
}
#[cfg(unix)]
#[cfg(test)]
mod tests {
+1 -1
View File
@@ -17,7 +17,7 @@ pub use crate::installation::{
PythonInstallation, PythonInstallationKey, PythonInstallationMinorVersionKey,
};
pub use crate::interpreter::{
BrokenSymlink, Error as InterpreterError, Interpreter, canonicalize_executable,
BrokenLink, Error as InterpreterError, Interpreter, canonicalize_executable,
};
pub use crate::pointer_size::PointerSize;
pub use crate::prefix::Prefix;
+7
View File
@@ -34,6 +34,8 @@ pub struct VirtualEnvironment {
/// A parsed `pyvenv.cfg`
#[derive(Debug, Clone)]
pub struct PyVenvConfiguration {
/// The `PYTHONHOME` directory containing the base Python executable.
pub(crate) home: Option<PathBuf>,
/// Was the virtual environment created with the `virtualenv` package?
pub(crate) virtualenv: bool,
/// Was the virtual environment created with the `uv` package?
@@ -231,6 +233,7 @@ pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
impl PyVenvConfiguration {
/// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`].
pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
let mut home = None;
let mut virtualenv = false;
let mut uv = false;
let mut relocatable = false;
@@ -248,6 +251,9 @@ impl PyVenvConfiguration {
continue;
};
match key.trim() {
"home" => {
home = Some(PathBuf::from(value.trim()));
}
"virtualenv" => {
virtualenv = true;
}
@@ -274,6 +280,7 @@ impl PyVenvConfiguration {
}
Ok(Self {
home,
virtualenv,
uv,
relocatable,