Allow for using config specific `gdb` binaries in gdb adapter (#37193)

Bo Lorentsen and Anthony created

I really need this for embedded development in IDF and Zephyr, where the
chip vendors sometimes provide there own specialized version of the
tools, and I need to direct zed to use these.

The current GDB adapter only supports the gdb it find in the normal
search path, and it also seems like we where not able to transfer gdb
specific startup arguments (only `-i=dap` is set) .

In order to fix this I (semi wipe using GPT-4.1) expanded the GDB
adapter with 2 new config options :

* **gdb_path** holds a full path to the gdb executable, for now only
full path or relative to cwd
* **gdb_args** an array holding additional arguments given to gdb on
startup
 
It seemed to me, like the `env` config did not transferred to gdb, so
this is added to.
 
 I have tested this locally, and it seems to work not only compile :-)

Release Notes:

debugger: Adds gdb_path and gdb_args to gdb debug adapter options 
debugger: Fix bug where gdb debug sessions wouldn't inherit the shell
environment from Zed

---------

Co-authored-by: Anthony <anthony@zed.dev>

Change summary

crates/dap_adapters/src/gdb.rs | 99 +++++++++++++++++++++++++++++------
1 file changed, 81 insertions(+), 18 deletions(-)

Detailed changes

crates/dap_adapters/src/gdb.rs 🔗

@@ -1,10 +1,9 @@
-use std::ffi::OsStr;
-
 use anyhow::{Context as _, Result, bail};
 use async_trait::async_trait;
 use collections::HashMap;
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
+use std::ffi::OsStr;
 use task::{DebugScenario, ZedDebugConfig};
 
 use crate::*;
@@ -16,6 +15,14 @@ impl GdbDebugAdapter {
     const ADAPTER_NAME: &'static str = "GDB";
 }
 
+/// Ensures that "-i=dap" is present in the GDB argument list.
+fn ensure_dap_interface(mut gdb_args: Vec<String>) -> Vec<String> {
+    if !gdb_args.iter().any(|arg| arg.trim() == "-i=dap") {
+        gdb_args.insert(0, "-i=dap".to_string());
+    }
+    gdb_args
+}
+
 #[async_trait(?Send)]
 impl DebugAdapter for GdbDebugAdapter {
     fn name(&self) -> DebugAdapterName {
@@ -99,6 +106,18 @@ impl DebugAdapter for GdbDebugAdapter {
                                     "type": "string",
                                     "description": "Working directory for the debugged program. GDB will change its working directory to this directory."
                                 },
+                                "gdb_path": {
+                                    "type": "string",
+                                    "description": "Alternative path to the GDB executable, if the one in standard path is not desirable"
+                                },
+                                "gdb_args": {
+                                    "type": "array",
+                                    "items": {
+                                        "type":"string"
+                                    },
+                                    "description": "additional arguments given to GDB at startup, not the program debugged",
+                                    "default": []
+                                },
                                 "env": {
                                     "type": "object",
                                     "description": "Environment variables for the debugged program. Each key is the name of an environment variable; each value is the value of that variable."
@@ -164,21 +183,49 @@ impl DebugAdapter for GdbDebugAdapter {
         user_env: Option<HashMap<String, String>>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
-        let user_setting_path = user_installed_path
-            .filter(|p| p.exists())
-            .and_then(|p| p.to_str().map(|s| s.to_string()));
-
-        let gdb_path = delegate
-            .which(OsStr::new("gdb"))
-            .await
-            .and_then(|p| p.to_str().map(|s| s.to_string()))
-            .context("Could not find gdb in path");
-
-        if gdb_path.is_err() && user_setting_path.is_none() {
-            bail!("Could not find gdb path or it's not installed");
-        }
+        // Try to get gdb_path from config
+        let gdb_path_from_config = config
+            .config
+            .get("gdb_path")
+            .and_then(|v| v.as_str())
+            .map(|s| s.to_string());
 
-        let gdb_path = user_setting_path.unwrap_or(gdb_path?);
+        let gdb_path = if let Some(path) = gdb_path_from_config {
+            path
+        } else {
+            // Original logic: use user_installed_path or search in system path
+            let user_setting_path = user_installed_path
+                .filter(|p| p.exists())
+                .and_then(|p| p.to_str().map(|s| s.to_string()));
+
+            let gdb_path_result = delegate
+                .which(OsStr::new("gdb"))
+                .await
+                .and_then(|p| p.to_str().map(|s| s.to_string()))
+                .context("Could not find gdb in path");
+
+            if gdb_path_result.is_err() && user_setting_path.is_none() {
+                bail!("Could not find gdb path or it's not installed");
+            }
+
+            user_setting_path.unwrap_or_else(|| gdb_path_result.unwrap())
+        };
+
+        // Arguments: use gdb_args from config if present, else user_args, else default
+        let gdb_args = {
+            let args = config
+                .config
+                .get("gdb_args")
+                .and_then(|v| v.as_array())
+                .map(|arr| {
+                    arr.iter()
+                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
+                        .collect::<Vec<_>>()
+                })
+                .or(user_args.clone())
+                .unwrap_or_else(|| vec!["-i=dap".into()]);
+            ensure_dap_interface(args)
+        };
 
         let mut configuration = config.config.clone();
         if let Some(configuration) = configuration.as_object_mut() {
@@ -187,10 +234,26 @@ impl DebugAdapter for GdbDebugAdapter {
                 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
         }
 
+        let mut base_env = delegate.shell_env().await;
+        base_env.extend(user_env.unwrap_or_default());
+
+        let config_env: HashMap<String, String> = config
+            .config
+            .get("env")
+            .and_then(|v| v.as_object())
+            .map(|obj| {
+                obj.iter()
+                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
+                    .collect::<HashMap<String, String>>()
+            })
+            .unwrap_or_else(HashMap::default);
+
+        base_env.extend(config_env);
+
         Ok(DebugAdapterBinary {
             command: Some(gdb_path),
-            arguments: user_args.unwrap_or_else(|| vec!["-i=dap".into()]),
-            envs: user_env.unwrap_or_default(),
+            arguments: gdb_args,
+            envs: base_env,
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
             connection: None,
             request_args: StartDebuggingRequestArguments {