From ad1fdf9e0e58f7d9bf7f7141644847e5c176eb61 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:30:03 +0800 Subject: [PATCH] fix(tools): find Python on Windows without python3.exe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installing debugpy on Windows failed with "cannot run python3.exe … (os error 3)": the tool manager looked for `python3` first, and on Windows that only finds the App Execution Alias in WindowsApps, which can't start without the Store's Python. Windows installs `python.exe` and the `py` launcher instead. On Windows the interpreter is now looked up as `python`, `py`, then `python3`, and a WindowsApps alias only counts when it runs. The debugpy adapter's default interpreter is `python` on Windows too. macOS and Linux keep `python3` first. --- crates/ide-dap/src/adapter.rs | 8 +++++++- crates/ide-tools/src/lib.rs | 20 +++++++++++++++++++- crates/ide-tools/src/tests.rs | 22 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/ide-dap/src/adapter.rs b/crates/ide-dap/src/adapter.rs index 2f4f88fc1..25c44cdd6 100644 --- a/crates/ide-dap/src/adapter.rs +++ b/crates/ide-dap/src/adapter.rs @@ -72,7 +72,13 @@ fn builtin(adapter: &str) -> Option { "stdio", "Install the Kotlin Debug Adapter from Settings > Language Servers", ), - "debugpy" => cfg("python3", &["-m", "debugpy.adapter"], "stdio", "pip install debugpy"), + // Windows installs `python.exe`; `python3.exe` there is at most a Store alias. + "debugpy" => cfg( + if cfg!(windows) { "python" } else { "python3" }, + &["-m", "debugpy.adapter"], + "stdio", + "pip install debugpy", + ), "js-debug" => cfg( "js-debug-adapter", // Without a host it listens on `localhost`, which may resolve to ::1 only. diff --git a/crates/ide-tools/src/lib.rs b/crates/ide-tools/src/lib.rs index 99c82ed25..6dbb19abb 100644 --- a/crates/ide-tools/src/lib.rs +++ b/crates/ide-tools/src/lib.rs @@ -395,6 +395,11 @@ pub fn parse_version(text: &str) -> Option { None } +/// A Windows App Execution Alias (`%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe`). +fn is_app_execution_alias(path: &Path) -> bool { + path.parent().and_then(Path::file_name).is_some_and(|d| d.eq_ignore_ascii_case("WindowsApps")) +} + /// A tool whose "command" is a plugin jar (e.g. a jdtls bundle) rather than an executable. fn is_bundle(command: &str) -> bool { command.ends_with(".jar") @@ -491,8 +496,21 @@ impl Tools { cmd } + /// The interpreter that installs and probes Python tools. Windows installs `python.exe` + /// and the `py` launcher, not `python3.exe`; its `WindowsApps` entries are App Execution + /// Aliases that only open the Store unless the Store's Python is installed, so they count + /// only when they run. fn python(&self) -> Option { - self.which_any(&["python3", "python"]) + 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) + } + }) } /// Executable `cmd` inside `dir`, honouring Windows suffixes. diff --git a/crates/ide-tools/src/tests.rs b/crates/ide-tools/src/tests.rs index b992f2186..95ea24513 100644 --- a/crates/ide-tools/src/tests.rs +++ b/crates/ide-tools/src/tests.rs @@ -700,6 +700,28 @@ fn rust_analyzer_managed_copy_survives_rustup_becoming_available() { assert!(!env.root.join("rust-analyzer").exists()); } +/// 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(); + let aliases = env.sys.parent().unwrap().join("Microsoft").join("WindowsApps"); + env.script(&aliases, "python", "exit 9009"); + env.script(&aliases, "python3", "exit 9009"); + env.script(&env.sys, "py", "echo Python 3.12.0"); + 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()) + }; + assert_eq!(found(), "sysbin/py"); + env.script(&aliases, "python", "echo Python 3.13.0"); + assert_eq!(found(), "WindowsApps/python", "the Store's Python runs"); +} + #[cfg(unix)] #[test] fn python_probes_run_in_an_empty_private_directory() {