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
This commit is contained in:
Raphael Lüthy 2025-05-27 11:45:55 +02:00 committed by GitHub
parent 7ec61ceec9
commit 05763b2fe3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 138 additions and 27 deletions

2
Cargo.lock generated
View file

@ -4058,10 +4058,10 @@ dependencies = [
"gpui",
"json_dotpath",
"language",
"log",
"paths",
"serde",
"serde_json",
"smol",
"task",
"util",
"workspace-hack",

View file

@ -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

View file

@ -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<Vec<String>> {
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<PathBuf>,
toolchain: Option<Toolchain>,
installed_in_venv: bool,
) -> Result<DebugAdapterBinary> {
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<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
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");
}
}