From 2ac99e7a1158e1e6d3bfd44016fa01af000d5ad7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:28:03 +0200 Subject: [PATCH] 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. --- Cargo.lock | 1 + crates/dap_adapters/Cargo.toml | 1 + crates/dap_adapters/src/dap_adapters.rs | 1 - crates/dap_adapters/src/python.rs | 189 ++++++++++++------------ 4 files changed, 98 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d84249b00..aad5349a87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,6 +4248,7 @@ dependencies = [ "serde", "serde_json", "shlex", + "smol", "task", "util", "workspace-hack", diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 65544fbb6a..e7366785c8 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -36,6 +36,7 @@ paths.workspace = true serde.workspace = true serde_json.workspace = true shlex.workspace = true +smol.workspace = true task.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index a147861f8d..a4e6beb249 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -13,7 +13,6 @@ use dap::{ DapRegistry, adapters::{ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, - GithubRepo, }, configure_tcp_connection, }; diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index eb541bde8e..aa64fea6ed 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,31 +1,39 @@ use crate::*; use anyhow::Context as _; -use dap::adapters::latest_github_release; use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; -use gpui::{AppContext, AsyncApp, SharedString}; +use gpui::{AsyncApp, SharedString}; use json_dotpath::DotPaths; -use language::{LanguageName, Toolchain}; +use language::LanguageName; +use paths::debug_adapters_dir; use serde_json::Value; +use smol::lock::OnceCell; use std::net::Ipv4Addr; use std::{ collections::HashMap, ffi::OsStr, path::{Path, PathBuf}, - sync::OnceLock, }; -use util::ResultExt; #[derive(Default)] pub(crate) struct PythonDebugAdapter { - checked: OnceLock<()>, + python_venv_base: OnceCell, String>>, } impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "Debugpy"; const DEBUG_ADAPTER_NAME: DebugAdapterName = DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME)); - const ADAPTER_PACKAGE_NAME: &'static str = "debugpy"; - const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; + const PYTHON_ADAPTER_IN_VENV: &'static str = if cfg!(target_os = "windows") { + "Scripts/python3" + } else { + "bin/python3" + }; + const ADAPTER_PATH: &'static str = if cfg!(target_os = "windows") { + "debugpy-venv/Scripts/debugpy-adapter" + } else { + "debugpy-venv/bin/debugpy-adapter" + }; + const LANGUAGE_NAME: &'static str = "Python"; async fn generate_debugpy_arguments( @@ -46,25 +54,12 @@ impl PythonDebugAdapter { vec!["-m".to_string(), "debugpy.adapter".to_string()] } else { let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_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() - ); - vec![ - debugpy_dir - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - ] + let path = adapter_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .into_owned(); + log::debug!("Using pip debugpy adapter from: {path}"); + vec![path] }; args.extend(if let Some(args) = user_args { @@ -100,44 +95,67 @@ impl PythonDebugAdapter { request, }) } - async fn fetch_latest_adapter_version( - &self, - delegate: &Arc, - ) -> Result { - let github_repo = GithubRepo { - repo_name: Self::ADAPTER_PACKAGE_NAME.into(), - repo_owner: "microsoft".into(), - }; - fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await - } - - async fn install_binary( - adapter_name: DebugAdapterName, - version: AdapterVersion, - delegate: Arc, - ) -> Result<()> { - let version_path = adapters::download_adapter_from_github( - adapter_name, - version, - adapters::DownloadedFileType::GzipTar, - delegate.as_ref(), - ) - .await?; - // only needed when you install the latest version for the first time - if let Some(debugpy_dir) = - util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| { - file_name.starts_with("microsoft-debugpy-") - }) + async fn ensure_venv(delegate: &dyn DapDelegate) -> Result> { + let python_path = Self::find_base_python(delegate) .await - { - // TODO Debugger: Rename folder instead of moving all files to another folder - // We're doing unnecessary IO work right now - util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path()) + .context("Could not find Python installation for DebugPy")?; + let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); + let mut path = work_dir.clone(); + path.push("debugpy-venv"); + if !path.exists() { + util::command::new_smol_command(python_path) + .arg("-m") + .arg("venv") + .arg("debugpy-venv") + .current_dir(work_dir) + .spawn()? + .output() .await?; } - Ok(()) + Ok(path.into()) + } + + // Find "baseline", user python version from which we'll create our own venv. + async fn find_base_python(delegate: &dyn DapDelegate) -> Option { + for path in ["python3", "python"] { + if let Some(path) = delegate.which(path.as_ref()).await { + return Some(path); + } + } + None + } + + async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result, String> { + const BINARY_DIR: &str = if cfg!(target_os = "windows") { + "Scripts" + } else { + "bin" + }; + self.python_venv_base + .get_or_init(move || async move { + let venv_base = Self::ensure_venv(delegate) + .await + .map_err(|e| format!("{e}"))?; + let pip_path = venv_base.join(BINARY_DIR).join("pip3"); + let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) + .arg("install") + .arg("debugpy") + .arg("-U") + .output() + .await + .map_err(|e| format!("{e}"))? + .status + .success(); + if !installation_succeeded { + return Err("debugpy installation failed".into()); + } + + Ok(venv_base) + }) + .await + .clone() } async fn get_installed_binary( @@ -146,15 +164,15 @@ impl PythonDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, - toolchain: Option, + python_from_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 python_path = if let Some(toolchain) = toolchain { - Some(toolchain.path.to_string()) + let python_path = if let Some(toolchain) = python_from_toolchain { + Some(toolchain) } else { let mut name = None; @@ -635,25 +653,28 @@ impl DebugAdapter for PythonDebugAdapter { &config, None, user_args, - Some(toolchain.clone()), + Some(toolchain.path.to_string()), true, ) .await; } } } - - if self.checked.set(()).is_ok() { - delegate.output_to_console(format!("Checking latest version of {}...", self.name())); - if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { - cx.background_spawn(Self::install_binary(self.name(), version, delegate.clone())) - .await - .context("Failed to install debugpy")?; - } - } - - self.get_installed_binary(delegate, &config, None, user_args, toolchain, false) + let toolchain = self + .base_venv(&**delegate) .await + .map_err(|e| anyhow::anyhow!(e))? + .join(Self::PYTHON_ADAPTER_IN_VENV); + + self.get_installed_binary( + delegate, + &config, + None, + user_args, + Some(toolchain.to_string_lossy().into_owned()), + false, + ) + .await } fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option { @@ -666,24 +687,6 @@ impl DebugAdapter for PythonDebugAdapter { } } -async fn fetch_latest_adapter_version_from_github( - github_repo: GithubRepo, - delegate: &dyn DapDelegate, -) -> Result { - let release = latest_github_release( - &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), - false, - false, - delegate.http_client(), - ) - .await?; - - Ok(AdapterVersion { - tag_name: release.tag_name, - url: release.tarball_url, - }) -} - #[cfg(test)] mod tests { use super::*;