Skip to content

Commit 2ac99e7

Browse files
authored
debugger: Fix attaching with DebugPy (#34706)
@cole-miller found a root cause of our struggles with attach scenarios; we did not fetch .so files necessary for attaching to work, as we were downloading DebugPy source tarballs from GitHub. This PR does away with it by setting up a virtualenv instead that has debugpy installed. Closes #34660 Closes #34575 Release Notes: - debugger: Fixed attaching with DebugPy. DebugPy is now installed automatically from pip (instead of GitHub), unless it is present in active virtual environment. Additionally this should resolve any startup issues with missing .so on Linux.
1 parent d604b3b commit 2ac99e7

File tree

4 files changed

+98
-94
lines changed

4 files changed

+98
-94
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dap_adapters/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ paths.workspace = true
3636
serde.workspace = true
3737
serde_json.workspace = true
3838
shlex.workspace = true
39+
smol.workspace = true
3940
task.workspace = true
4041
util.workspace = true
4142
workspace-hack.workspace = true

crates/dap_adapters/src/dap_adapters.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use dap::{
1313
DapRegistry,
1414
adapters::{
1515
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
16-
GithubRepo,
1716
},
1817
configure_tcp_connection,
1918
};

crates/dap_adapters/src/python.rs

Lines changed: 96 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
use crate::*;
22
use anyhow::Context as _;
3-
use dap::adapters::latest_github_release;
43
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
5-
use gpui::{AppContext, AsyncApp, SharedString};
4+
use gpui::{AsyncApp, SharedString};
65
use json_dotpath::DotPaths;
7-
use language::{LanguageName, Toolchain};
6+
use language::LanguageName;
7+
use paths::debug_adapters_dir;
88
use serde_json::Value;
9+
use smol::lock::OnceCell;
910
use std::net::Ipv4Addr;
1011
use std::{
1112
collections::HashMap,
1213
ffi::OsStr,
1314
path::{Path, PathBuf},
14-
sync::OnceLock,
1515
};
16-
use util::ResultExt;
1716

1817
#[derive(Default)]
1918
pub(crate) struct PythonDebugAdapter {
20-
checked: OnceLock<()>,
19+
python_venv_base: OnceCell<Result<Arc<Path>, String>>,
2120
}
2221

2322
impl PythonDebugAdapter {
2423
const ADAPTER_NAME: &'static str = "Debugpy";
2524
const DEBUG_ADAPTER_NAME: DebugAdapterName =
2625
DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));
27-
const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
28-
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
26+
const PYTHON_ADAPTER_IN_VENV: &'static str = if cfg!(target_os = "windows") {
27+
"Scripts/python3"
28+
} else {
29+
"bin/python3"
30+
};
31+
const ADAPTER_PATH: &'static str = if cfg!(target_os = "windows") {
32+
"debugpy-venv/Scripts/debugpy-adapter"
33+
} else {
34+
"debugpy-venv/bin/debugpy-adapter"
35+
};
36+
2937
const LANGUAGE_NAME: &'static str = "Python";
3038

3139
async fn generate_debugpy_arguments(
@@ -46,25 +54,12 @@ impl PythonDebugAdapter {
4654
vec!["-m".to_string(), "debugpy.adapter".to_string()]
4755
} else {
4856
let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
49-
let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
50-
51-
let debugpy_dir =
52-
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
53-
file_name.starts_with(&file_name_prefix)
54-
})
55-
.await
56-
.context("Debugpy directory not found")?;
57-
58-
log::debug!(
59-
"Using GitHub-downloaded debugpy adapter from: {}",
60-
debugpy_dir.display()
61-
);
62-
vec![
63-
debugpy_dir
64-
.join(Self::ADAPTER_PATH)
65-
.to_string_lossy()
66-
.to_string(),
67-
]
57+
let path = adapter_path
58+
.join(Self::ADAPTER_PATH)
59+
.to_string_lossy()
60+
.into_owned();
61+
log::debug!("Using pip debugpy adapter from: {path}");
62+
vec![path]
6863
};
6964

7065
args.extend(if let Some(args) = user_args {
@@ -100,44 +95,67 @@ impl PythonDebugAdapter {
10095
request,
10196
})
10297
}
103-
async fn fetch_latest_adapter_version(
104-
&self,
105-
delegate: &Arc<dyn DapDelegate>,
106-
) -> Result<AdapterVersion> {
107-
let github_repo = GithubRepo {
108-
repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
109-
repo_owner: "microsoft".into(),
110-
};
11198

112-
fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
113-
}
114-
115-
async fn install_binary(
116-
adapter_name: DebugAdapterName,
117-
version: AdapterVersion,
118-
delegate: Arc<dyn DapDelegate>,
119-
) -> Result<()> {
120-
let version_path = adapters::download_adapter_from_github(
121-
adapter_name,
122-
version,
123-
adapters::DownloadedFileType::GzipTar,
124-
delegate.as_ref(),
125-
)
126-
.await?;
127-
// only needed when you install the latest version for the first time
128-
if let Some(debugpy_dir) =
129-
util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
130-
file_name.starts_with("microsoft-debugpy-")
131-
})
99+
async fn ensure_venv(delegate: &dyn DapDelegate) -> Result<Arc<Path>> {
100+
let python_path = Self::find_base_python(delegate)
132101
.await
133-
{
134-
// TODO Debugger: Rename folder instead of moving all files to another folder
135-
// We're doing unnecessary IO work right now
136-
util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
102+
.context("Could not find Python installation for DebugPy")?;
103+
let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
104+
let mut path = work_dir.clone();
105+
path.push("debugpy-venv");
106+
if !path.exists() {
107+
util::command::new_smol_command(python_path)
108+
.arg("-m")
109+
.arg("venv")
110+
.arg("debugpy-venv")
111+
.current_dir(work_dir)
112+
.spawn()?
113+
.output()
137114
.await?;
138115
}
139116

140-
Ok(())
117+
Ok(path.into())
118+
}
119+
120+
// Find "baseline", user python version from which we'll create our own venv.
121+
async fn find_base_python(delegate: &dyn DapDelegate) -> Option<PathBuf> {
122+
for path in ["python3", "python"] {
123+
if let Some(path) = delegate.which(path.as_ref()).await {
124+
return Some(path);
125+
}
126+
}
127+
None
128+
}
129+
130+
async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result<Arc<Path>, String> {
131+
const BINARY_DIR: &str = if cfg!(target_os = "windows") {
132+
"Scripts"
133+
} else {
134+
"bin"
135+
};
136+
self.python_venv_base
137+
.get_or_init(move || async move {
138+
let venv_base = Self::ensure_venv(delegate)
139+
.await
140+
.map_err(|e| format!("{e}"))?;
141+
let pip_path = venv_base.join(BINARY_DIR).join("pip3");
142+
let installation_succeeded = util::command::new_smol_command(pip_path.as_path())
143+
.arg("install")
144+
.arg("debugpy")
145+
.arg("-U")
146+
.output()
147+
.await
148+
.map_err(|e| format!("{e}"))?
149+
.status
150+
.success();
151+
if !installation_succeeded {
152+
return Err("debugpy installation failed".into());
153+
}
154+
155+
Ok(venv_base)
156+
})
157+
.await
158+
.clone()
141159
}
142160

143161
async fn get_installed_binary(
@@ -146,15 +164,15 @@ impl PythonDebugAdapter {
146164
config: &DebugTaskDefinition,
147165
user_installed_path: Option<PathBuf>,
148166
user_args: Option<Vec<String>>,
149-
toolchain: Option<Toolchain>,
167+
python_from_toolchain: Option<String>,
150168
installed_in_venv: bool,
151169
) -> Result<DebugAdapterBinary> {
152170
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
153171
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
154172
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
155173

156-
let python_path = if let Some(toolchain) = toolchain {
157-
Some(toolchain.path.to_string())
174+
let python_path = if let Some(toolchain) = python_from_toolchain {
175+
Some(toolchain)
158176
} else {
159177
let mut name = None;
160178

@@ -635,25 +653,28 @@ impl DebugAdapter for PythonDebugAdapter {
635653
&config,
636654
None,
637655
user_args,
638-
Some(toolchain.clone()),
656+
Some(toolchain.path.to_string()),
639657
true,
640658
)
641659
.await;
642660
}
643661
}
644662
}
645-
646-
if self.checked.set(()).is_ok() {
647-
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
648-
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
649-
cx.background_spawn(Self::install_binary(self.name(), version, delegate.clone()))
650-
.await
651-
.context("Failed to install debugpy")?;
652-
}
653-
}
654-
655-
self.get_installed_binary(delegate, &config, None, user_args, toolchain, false)
663+
let toolchain = self
664+
.base_venv(&**delegate)
656665
.await
666+
.map_err(|e| anyhow::anyhow!(e))?
667+
.join(Self::PYTHON_ADAPTER_IN_VENV);
668+
669+
self.get_installed_binary(
670+
delegate,
671+
&config,
672+
None,
673+
user_args,
674+
Some(toolchain.to_string_lossy().into_owned()),
675+
false,
676+
)
677+
.await
657678
}
658679

659680
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
@@ -666,24 +687,6 @@ impl DebugAdapter for PythonDebugAdapter {
666687
}
667688
}
668689

669-
async fn fetch_latest_adapter_version_from_github(
670-
github_repo: GithubRepo,
671-
delegate: &dyn DapDelegate,
672-
) -> Result<AdapterVersion> {
673-
let release = latest_github_release(
674-
&format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
675-
false,
676-
false,
677-
delegate.http_client(),
678-
)
679-
.await?;
680-
681-
Ok(AdapterVersion {
682-
tag_name: release.tag_name,
683-
url: release.tarball_url,
684-
})
685-
}
686-
687690
#[cfg(test)]
688691
mod tests {
689692
use super::*;

0 commit comments

Comments
 (0)