From d3950f8a6668bdaa6b5d72b39536d4b0d9701478 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:59:36 +0800 Subject: [PATCH] fix(tools): keep searching past broken Python aliases --- crates/ide-tools/src/lib.rs | 25 ++++++++++++++++++------- crates/ide-tools/src/tests.rs | 24 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/crates/ide-tools/src/lib.rs b/crates/ide-tools/src/lib.rs index 6dbb19abb..881393d6c 100644 --- a/crates/ide-tools/src/lib.rs +++ b/crates/ide-tools/src/lib.rs @@ -504,13 +504,24 @@ impl Tools { if self.platform.os != Os::Windows { return self.which_any(&["python3", "python"]); } - ["python", "py", "python3"].into_iter().filter_map(|c| self.which(c)).find(|p| { - !is_app_execution_alias(p) || { - let mut c = self.python_command(p); - c.arg("--version"); - run_quiet(c, Duration::from_secs(5)).is_some_and(|(ok, _)| ok) - } - }) + ["python", "py", "python3"] + .into_iter() + .flat_map(|command| { + std::env::split_paths(&self.system_path).filter_map(move |dir| { + if dir.as_os_str().is_empty() { + return None; + } + let search = std::env::join_paths([dir]).ok()?; + ide_core::tool_path::find_command(command, &search, &self.root) + }) + }) + .find(|p| { + !is_app_execution_alias(p) || { + let mut c = self.python_command(p); + c.arg("--version"); + run_quiet(c, Duration::from_secs(5)).is_some_and(|(ok, _)| ok) + } + }) } /// Executable `cmd` inside `dir`, honouring Windows suffixes. diff --git a/crates/ide-tools/src/tests.rs b/crates/ide-tools/src/tests.rs index 95ea24513..de4ff670d 100644 --- a/crates/ide-tools/src/tests.rs +++ b/crates/ide-tools/src/tests.rs @@ -37,6 +37,12 @@ impl Env { fs::write(&p, format!("#!/bin/sh\n{body}\n")).unwrap(); archive::make_executable(&p).unwrap(); } + + #[cfg(windows)] + fn script(&self, dir: &Path, name: &str, body: &str) { + fs::create_dir_all(dir).unwrap(); + fs::write(dir.join(format!("{name}.cmd")), format!("@echo off\r\n{body}\r\n")).unwrap(); + } } fn find<'a>(list: &'a [ToolInfo], name: &str) -> &'a ToolInfo { @@ -702,7 +708,6 @@ fn rust_analyzer_managed_copy_survives_rustup_becoming_available() { /// Windows has `python.exe` and `py`, not `python3.exe`; the `WindowsApps` aliases only count /// when the Store's Python behind them runs. -#[cfg(unix)] #[test] fn windows_python_skips_store_aliases_that_do_not_run() { let env = Env::new(); @@ -713,13 +718,20 @@ fn windows_python_skips_store_aliases_that_do_not_run() { let windows = Platform { os: Os::Windows, arch: platform::Arch::X64 }; let found = || { let dirs = std::env::join_paths([aliases.clone(), env.sys.clone()]).unwrap(); - let python = Tools::with_paths(env.root.clone(), dirs, windows).python().unwrap(); - let dir = python.parent().unwrap().file_name().unwrap().to_string_lossy().into_owned(); - format!("{dir}/{}", python.file_name().unwrap().to_string_lossy()) + Tools::with_paths(env.root.clone(), dirs, windows).python().map(|python| { + let dir = python.parent().unwrap().file_name().unwrap().to_string_lossy().into_owned(); + format!("{dir}/{}", python.file_stem().unwrap().to_string_lossy()) + }) }; - assert_eq!(found(), "sysbin/py"); + assert_eq!(found().as_deref(), Some("sysbin/py")); + fs::remove_file(env.sys.join(if cfg!(windows) { "py.cmd" } else { "py" })).unwrap(); + assert_eq!(found(), None, "only broken aliases are available"); + env.script(&env.sys, "python3", "echo Python 3.12.0"); + assert_eq!(found().as_deref(), Some("sysbin/python3"), "python3 is the final fallback"); + env.script(&env.sys, "python", "echo Python 3.12.0"); + assert_eq!(found().as_deref(), Some("sysbin/python"), "a broken alias must not hide a later interpreter"); env.script(&aliases, "python", "echo Python 3.13.0"); - assert_eq!(found(), "WindowsApps/python", "the Store's Python runs"); + assert_eq!(found().as_deref(), Some("WindowsApps/python"), "the Store's Python runs"); } #[cfg(unix)]