From 05763b2fe3960eb2303b8a7a51117f87ccfe56e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20L=C3=BCthy?= Date: Tue, 27 May 2025 11:45:55 +0200 Subject: [PATCH] debugger beta: Fix install detection for Debugpy in venv (#31339) Based on my report on discord when chatting with Anthony and Remco: https://discord.com/channels/869392257814519848/1375129714645012530 Root Cause: Zed was incorrectly trying to execute a directory path instead of properly invoking the debugpy module when debugpy was installed via package managers (pip, conda, etc.) rather than downloaded from GitHub releases. Solution: - Automatic Detection: Zed now automatically detects whether debugpy is installed via pip/conda or downloaded from GitHub - Correct Invocation: For pip-installed debugpy, Zed now uses python -m debugpy.adapter instead of trying to execute file paths - Added a `installed_in_venv` flag to differentiate the setup properly - Backward Compatibility: GitHub-downloaded debugpy releases continue to work as before - Enhanced Logging: Added logging to show which debugpy installation method is being used (I had to verify it somehow) I verified with the following setups (can be confirmed with the debug logs): - `conda` with installed debugpy, went to installed instance - `uv` with installed debugpy, went to installed instance - `uv` without installed debugpy, went to github releases - Homebrew global python install, went to github releases Release Notes: - Fix issue where debugpy from different environments won't load as intended --- Cargo.lock | 2 +- crates/dap_adapters/Cargo.toml | 2 +- crates/dap_adapters/src/python.rs | 161 +++++++++++++++++++++++++----- 3 files changed, 138 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fc2b73d23..1b95610d5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4058,10 +4058,10 @@ dependencies = [ "gpui", "json_dotpath", "language", + "log", "paths", "serde", "serde_json", - "smol", "task", "util", "workspace-hack", diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 9eafb6ef40..f669d781cd 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -28,10 +28,10 @@ futures.workspace = true gpui.workspace = true json_dotpath.workspace = true language.workspace = true +log.workspace = true paths.workspace = true serde.workspace = true serde_json.workspace = true -smol.workspace = true task.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 009a05938d..5213829f4f 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -8,6 +8,7 @@ use gpui::{AsyncApp, SharedString}; use json_dotpath::DotPaths; use language::{LanguageName, Toolchain}; use serde_json::Value; +use std::net::Ipv4Addr; use std::{ collections::HashMap, ffi::OsStr, @@ -27,6 +28,60 @@ impl PythonDebugAdapter { const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; const LANGUAGE_NAME: &'static str = "Python"; + async fn generate_debugpy_arguments( + &self, + host: &Ipv4Addr, + port: u16, + user_installed_path: Option<&Path>, + installed_in_venv: bool, + ) -> Result> { + if let Some(user_installed_path) = user_installed_path { + log::debug!( + "Using user-installed debugpy adapter from: {}", + user_installed_path.display() + ); + Ok(vec![ + user_installed_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .to_string(), + format!("--host={}", host), + format!("--port={}", port), + ]) + } else if installed_in_venv { + log::debug!("Using venv-installed debugpy"); + Ok(vec![ + "-m".to_string(), + "debugpy.adapter".to_string(), + format!("--host={}", host), + format!("--port={}", port), + ]) + } else { + let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); + let file_name_prefix = format!("{}_", Self::ADAPTER_NAME); + + let debugpy_dir = + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .context("Debugpy directory not found")?; + + log::debug!( + "Using GitHub-downloaded debugpy adapter from: {}", + debugpy_dir.display() + ); + Ok(vec![ + debugpy_dir + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .to_string(), + format!("--host={}", host), + format!("--port={}", port), + ]) + } + } + fn request_args( &self, task_definition: &DebugTaskDefinition, @@ -93,24 +148,12 @@ impl PythonDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, toolchain: Option, + installed_in_venv: bool, ) -> Result { const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; - let debugpy_dir = if let Some(user_installed_path) = user_installed_path { - user_installed_path - } else { - let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); - let file_name_prefix = format!("{}_", Self::ADAPTER_NAME); - - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .context("Debugpy directory not found")? - }; - let python_path = if let Some(toolchain) = toolchain { Some(toolchain.path.to_string()) } else { @@ -128,16 +171,27 @@ impl PythonDebugAdapter { name }; + let python_command = python_path.context("failed to find binary path for Python")?; + log::debug!("Using Python executable: {}", python_command); + + let arguments = self + .generate_debugpy_arguments( + &host, + port, + user_installed_path.as_deref(), + installed_in_venv, + ) + .await?; + + log::debug!( + "Starting debugpy adapter with command: {} {}", + python_command, + arguments.join(" ") + ); + Ok(DebugAdapterBinary { - command: python_path.context("failed to find binary path for Python")?, - arguments: vec![ - debugpy_dir - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - format!("--port={}", port), - format!("--host={}", host), - ], + command: python_command, + arguments, connection: Some(adapters::TcpArguments { host, port, @@ -558,6 +612,16 @@ impl DebugAdapter for PythonDebugAdapter { user_installed_path: Option, cx: &mut AsyncApp, ) -> Result { + if let Some(local_path) = &user_installed_path { + log::debug!( + "Using user-installed debugpy adapter from: {}", + local_path.display() + ); + return self + .get_installed_binary(delegate, &config, Some(local_path.clone()), None, false) + .await; + } + let toolchain = delegate .toolchain_store() .active_toolchain( @@ -571,13 +635,18 @@ impl DebugAdapter for PythonDebugAdapter { if let Some(toolchain) = &toolchain { if let Some(path) = Path::new(&toolchain.path.to_string()).parent() { let debugpy_path = path.join("debugpy"); - if smol::fs::metadata(&debugpy_path).await.is_ok() { + if delegate.fs().is_file(&debugpy_path).await { + log::debug!( + "Found debugpy in toolchain environment: {}", + debugpy_path.display() + ); return self .get_installed_binary( delegate, &config, - Some(debugpy_path.to_path_buf()), + None, Some(toolchain.clone()), + true, ) .await; } @@ -591,7 +660,49 @@ impl DebugAdapter for PythonDebugAdapter { } } - self.get_installed_binary(delegate, &config, user_installed_path, toolchain) + self.get_installed_binary(delegate, &config, None, None, false) .await } } + +#[cfg(test)] +mod tests { + use super::*; + use std::{net::Ipv4Addr, path::PathBuf}; + + #[gpui::test] + async fn test_debugpy_install_path_cases() { + let adapter = PythonDebugAdapter::default(); + let host = Ipv4Addr::new(127, 0, 0, 1); + let port = 5678; + + // Case 1: User-defined debugpy path (highest precedence) + let user_path = PathBuf::from("/custom/path/to/debugpy"); + let user_args = adapter + .generate_debugpy_arguments(&host, port, Some(&user_path), false) + .await + .unwrap(); + + // Case 2: Venv-installed debugpy (uses -m debugpy.adapter) + let venv_args = adapter + .generate_debugpy_arguments(&host, port, None, true) + .await + .unwrap(); + + assert!(user_args[0].ends_with("src/debugpy/adapter")); + assert_eq!(user_args[1], "--host=127.0.0.1"); + assert_eq!(user_args[2], "--port=5678"); + + assert_eq!(venv_args[0], "-m"); + assert_eq!(venv_args[1], "debugpy.adapter"); + assert_eq!(venv_args[2], "--host=127.0.0.1"); + assert_eq!(venv_args[3], "--port=5678"); + + // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API. + } + + #[test] + fn test_adapter_path_constant() { + assert_eq!(PythonDebugAdapter::ADAPTER_PATH, "src/debugpy/adapter"); + } +}