Skip to content

Commit 7336bbb

Browse files
committed
debugger: Fix attaching with DebugPy
@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.
1 parent fd05f17 commit 7336bbb

File tree

3 files changed

+98
-86
lines changed

3 files changed

+98
-86
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/python.rs

Lines changed: 96 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use crate::*;
2-
use anyhow::Context as _;
3-
use dap::adapters::latest_github_release;
2+
use anyhow::{Context as _, ensure};
43
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
54
use gpui::{AppContext, AsyncApp, SharedString};
65
use json_dotpath::DotPaths;
76
use language::{LanguageName, Toolchain};
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,
@@ -17,15 +18,24 @@ use util::ResultExt;
1718

1819
#[derive(Default)]
1920
pub(crate) struct PythonDebugAdapter {
20-
checked: OnceLock<()>,
21+
python_venv_base: OnceCell<Result<Arc<Path>, String>>,
2122
}
2223

2324
impl PythonDebugAdapter {
2425
const ADAPTER_NAME: &'static str = "Debugpy";
2526
const DEBUG_ADAPTER_NAME: DebugAdapterName =
2627
DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));
27-
const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
28-
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
28+
const PYTHON_ADAPTER_IN_VENV: &'static str = if cfg!(target_os = "windows") {
29+
"Scripts/python3"
30+
} else {
31+
"bin/python3"
32+
};
33+
const ADAPTER_PATH: &'static str = if cfg!(target_os = "windows") {
34+
"debugpy-venv/Scripts/debugpy-adapter"
35+
} else {
36+
"debugpy-venv/bin/debugpy-adapter"
37+
};
38+
2939
const LANGUAGE_NAME: &'static str = "Python";
3040

3141
async fn generate_debugpy_arguments(
@@ -46,25 +56,12 @@ impl PythonDebugAdapter {
4656
vec!["-m".to_string(), "debugpy.adapter".to_string()]
4757
} else {
4858
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-
]
59+
let path = adapter_path
60+
.join(Self::ADAPTER_PATH)
61+
.to_string_lossy()
62+
.into_owned();
63+
log::debug!("Using pip debugpy adapter from: {path}");
64+
vec![path]
6865
};
6966

7067
args.extend(if let Some(args) = user_args {
@@ -100,44 +97,67 @@ impl PythonDebugAdapter {
10097
request,
10198
})
10299
}
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-
};
111-
112-
fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
113-
}
114100

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-
})
101+
async fn ensure_venv(delegate: &dyn DapDelegate) -> Result<Arc<Path>> {
102+
let python_path = Self::find_base_python(delegate)
132103
.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())
104+
.context("Could not find Python installation for DebugPy")?;
105+
let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
106+
let mut path = work_dir.clone();
107+
path.push("debugpy-venv");
108+
if !path.exists() {
109+
util::command::new_smol_command(python_path)
110+
.arg("-m")
111+
.arg("venv")
112+
.arg("debugpy-venv")
113+
.current_dir(work_dir)
114+
.spawn()?
115+
.output()
137116
.await?;
138117
}
139118

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

143163
async fn get_installed_binary(
@@ -642,18 +662,26 @@ impl DebugAdapter for PythonDebugAdapter {
642662
}
643663
}
644664
}
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)
665+
let toolchain = self
666+
.base_venv(&**delegate)
656667
.await
668+
.map_err(|e| anyhow::anyhow!(e))?
669+
.join(Self::PYTHON_ADAPTER_IN_VENV);
670+
671+
self.get_installed_binary(
672+
delegate,
673+
&config,
674+
None,
675+
user_args,
676+
Some(Toolchain {
677+
name: SharedString::new_static("Zed-provided DebugPy"),
678+
path: SharedString::from(toolchain.to_string_lossy().into_owned()),
679+
language_name: LanguageName::new("Python"),
680+
as_json: Value::Null,
681+
}),
682+
false,
683+
)
684+
.await
657685
}
658686

659687
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
@@ -666,24 +694,6 @@ impl DebugAdapter for PythonDebugAdapter {
666694
}
667695
}
668696

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-
687697
#[cfg(test)]
688698
mod tests {
689699
use super::*;

0 commit comments

Comments
 (0)