Skip invalid interpreters when searching for requested interpreter executable name (#4308)

Previously, we took the first executable on the `PATH` but if it was not
a usable interpreter we'd fail. Now, we'll continue searching in the
path until we find an interpreter as we do with the standard executable
names.
This commit is contained in:
Zanie Blue 2024-06-13 13:36:12 -04:00 committed by GitHub
parent 30126950fe
commit b07c132ede
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -9,7 +9,7 @@ use pep440_rs::{Version, VersionSpecifiers};
use same_file::is_same_file; use same_file::is_same_file;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
use which::which; use which::{which, which_all};
use uv_cache::Cache; use uv_cache::Cache;
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
@ -361,8 +361,22 @@ fn python_interpreters<'a>(
sources: &ToolchainSources, sources: &ToolchainSources,
cache: &'a Cache, cache: &'a Cache,
) -> impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a { ) -> impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a {
python_executables(version, implementation, sources) // TODO(zanieb): Move filtering to callers
.map(|result| match result { filter_by_system_python(
python_interpreters_from_executables(
python_executables(version, implementation, sources),
cache,
),
system,
)
}
/// Lazily convert Python executables into interpreters.
fn python_interpreters_from_executables<'a>(
executables: impl Iterator<Item = Result<(ToolchainSource, PathBuf), Error>> + 'a,
cache: &'a Cache,
) -> impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a {
executables.map(|result| match result {
Ok((source, path)) => Interpreter::query(&path, cache) Ok((source, path)) => Interpreter::query(&path, cache)
.map(|interpreter| (source, interpreter)) .map(|interpreter| (source, interpreter))
.inspect(|(source, interpreter)| { .inspect(|(source, interpreter)| {
@ -377,7 +391,13 @@ fn python_interpreters<'a>(
.inspect_err(|err| debug!("{err}")), .inspect_err(|err| debug!("{err}")),
Err(err) => Err(err), Err(err) => Err(err),
}) })
.filter(move |result| match result { }
fn filter_by_system_python<'a>(
interpreters: impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a,
system: SystemPython,
) -> impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a {
interpreters.filter(move |result| match result {
// Filter the returned interpreters to conform to the system request // Filter the returned interpreters to conform to the system request
Ok((source, interpreter)) => match ( Ok((source, interpreter)) => match (
system, system,
@ -482,19 +502,17 @@ fn find_toolchain_at_directory(path: &PathBuf, cache: &Cache) -> Result<Toolchai
})) }))
} }
fn find_toolchain_with_executable_name( /// Lazily iterate over all Python interpreters on the path with the given executable name.
name: &str, fn python_interpreters_with_executable_name<'a>(
cache: &Cache, name: &'a str,
) -> Result<ToolchainResult, Error> { cache: &'a Cache,
let Some(executable) = which(name).ok() else { ) -> impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a {
return Ok(ToolchainResult::Err( python_interpreters_from_executables(
ToolchainNotFound::ExecutableNotFoundInSearchPath(name.to_string()), which_all(name)
)); .into_iter()
}; .flat_map(|inner| inner.map(|path| Ok((ToolchainSource::SearchPath, path)))),
Ok(ToolchainResult::Ok(Toolchain { cache,
source: ToolchainSource::SearchPath, )
interpreter: Interpreter::query(executable, cache)?,
}))
} }
/// Iterate over all toolchains that satisfy the given request. /// Iterate over all toolchains that satisfy the given request.
@ -530,19 +548,22 @@ pub fn find_toolchains<'a>(
)) ))
} }
})), })),
ToolchainRequest::ExecutableName(name) => Box::new(std::iter::once({ ToolchainRequest::ExecutableName(name) => {
debug!("Searching for Python interpreter with {request}"); debug!("Searching for Python interpreter with {request}");
if sources.contains(ToolchainSource::SearchPath) { if sources.contains(ToolchainSource::SearchPath) {
debug!("Checking for Python interpreter at {request}"); debug!("Checking for Python interpreter at {request}");
find_toolchain_with_executable_name(name, cache) Box::new(
python_interpreters_with_executable_name(name, cache)
.map(|result| result.map(Toolchain::from_tuple).map(ToolchainResult::Ok)),
)
} else { } else {
Err(Error::SourceNotSelected( Box::new(std::iter::once(Err(Error::SourceNotSelected(
request.clone(), request.clone(),
ToolchainSource::SearchPath, ToolchainSource::SearchPath,
sources.clone(), sources.clone(),
)) ))))
}
} }
})),
ToolchainRequest::Any => Box::new({ ToolchainRequest::Any => Box::new({
debug!("Searching for Python interpreter in {sources}"); debug!("Searching for Python interpreter in {sources}");
python_interpreters(None, None, system, sources, cache) python_interpreters(None, None, system, sources, cache)
@ -614,13 +635,13 @@ pub(crate) fn find_toolchain(
ToolchainRequest::Version(version) => { ToolchainRequest::Version(version) => {
ToolchainNotFound::NoMatchingVersion(sources.clone(), version.clone()) ToolchainNotFound::NoMatchingVersion(sources.clone(), version.clone())
} }
ToolchainRequest::ExecutableName(name) => {
ToolchainNotFound::ExecutableNotFoundInSearchPath(name.clone())
}
// TODO(zanieb): As currently implemented, these are unreachable as they are handled in `find_toolchains` // TODO(zanieb): As currently implemented, these are unreachable as they are handled in `find_toolchains`
// We should avoid this duplication // We should avoid this duplication
ToolchainRequest::Directory(path) => ToolchainNotFound::DirectoryNotFound(path.clone()), ToolchainRequest::Directory(path) => ToolchainNotFound::DirectoryNotFound(path.clone()),
ToolchainRequest::File(path) => ToolchainNotFound::FileNotFound(path.clone()), ToolchainRequest::File(path) => ToolchainNotFound::FileNotFound(path.clone()),
ToolchainRequest::ExecutableName(name) => {
ToolchainNotFound::ExecutableNotFoundInSearchPath(name.clone())
}
ToolchainRequest::Any => ToolchainNotFound::NoPythonInstallation(sources.clone(), None), ToolchainRequest::Any => ToolchainNotFound::NoPythonInstallation(sources.clone(), None),
}; };
Ok(ToolchainResult::Err(err)) Ok(ToolchainResult::Err(err))