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() {