diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index 22c42f670..9e38dd2fb 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -1,4 +1,3 @@ -use std::ffi::{OsStr, OsString}; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; @@ -200,25 +199,6 @@ impl Interpreter { } } - /// Find the Python interpreter in `PATH`, respecting `UV_PYTHON_PATH`. - /// - /// Returns `Ok(None)` if not found. - pub(crate) fn find_executable + Into + Copy>( - requested: R, - ) -> Result, Error> { - let result = if let Some(isolated) = std::env::var_os("UV_TEST_PYTHON_PATH") { - which::which_in(requested, Some(isolated), std::env::current_dir()?) - } else { - which::which(requested) - }; - - match result { - Err(which::Error::CannotFindBinaryPath) => Ok(None), - Err(err) => Err(Error::WhichError(requested.into(), err)), - Ok(path) => Ok(Some(path)), - } - } - /// Returns the path to the Python virtual environment. #[inline] pub fn platform(&self) -> &Platform { diff --git a/crates/uv-interpreter/src/python_query.rs b/crates/uv-interpreter/src/python_query.rs index 549a559d3..d6a9c90ba 100644 --- a/crates/uv-interpreter/src/python_query.rs +++ b/crates/uv-interpreter/src/python_query.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::env; +use std::ffi::{OsStr, OsString}; use std::path::PathBuf; use tracing::{debug, instrument}; @@ -58,7 +59,7 @@ pub fn find_requested_python( } } else if !request.contains(std::path::MAIN_SEPARATOR) { // `-p python3.10`; Generally not used on windows because all Python are `python.exe`. - let Some(executable) = Interpreter::find_executable(request)? else { + let Some(executable) = find_executable(request)? else { return Ok(None); }; Interpreter::query(&executable, platform.clone(), cache).map(Some) @@ -198,6 +199,41 @@ fn find_python( Ok(None) } +/// Find the Python interpreter in `PATH` matching the given name (e.g., `python3`, respecting +/// `UV_PYTHON_PATH`. +/// +/// Returns `Ok(None)` if not found. +fn find_executable + Into + Copy>( + requested: R, +) -> Result, Error> { + #[allow(non_snake_case)] + let UV_TEST_PYTHON_PATH = env::var_os("UV_TEST_PYTHON_PATH"); + + #[allow(non_snake_case)] + let PATH = UV_TEST_PYTHON_PATH + .or(env::var_os("PATH")) + .unwrap_or_default(); + + // We use `which` here instead of joining the paths ourselves because `which` checks for us if the python + // binary is executable and exists. It also has some extra logic that handles inconsistent casing on Windows + // and expands `~`. + for path in env::split_paths(&PATH) { + let paths = match which::which_in_global(requested, Some(&path)) { + Ok(paths) => paths, + Err(which::Error::CannotFindBinaryPath) => continue, + Err(err) => return Err(Error::WhichError(requested.into(), err)), + }; + for path in paths { + if cfg!(windows) && windows::is_windows_store_shim(&path) { + continue; + } + return Ok(Some(path)); + } + } + + Ok(None) +} + #[derive(Debug, Clone)] enum PythonInstallation { PyListPath {