@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user