codelldb.rs

  1use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
  2
  3use anyhow::{Context as _, Result, anyhow};
  4use async_trait::async_trait;
  5use dap::{
  6    StartDebuggingRequestArgumentsRequest,
  7    adapters::{DebugTaskDefinition, latest_github_release},
  8};
  9use futures::StreamExt;
 10use gpui::AsyncApp;
 11use serde_json::Value;
 12use task::{DebugRequest, DebugScenario, ZedDebugConfig};
 13use util::fs::remove_matching;
 14
 15use crate::*;
 16
 17#[derive(Default)]
 18pub(crate) struct CodeLldbDebugAdapter {
 19    path_to_codelldb: OnceLock<String>,
 20}
 21
 22impl CodeLldbDebugAdapter {
 23    const ADAPTER_NAME: &'static str = "CodeLLDB";
 24
 25    fn request_args(
 26        &self,
 27        task_definition: &DebugTaskDefinition,
 28    ) -> Result<dap::StartDebuggingRequestArguments> {
 29        // CodeLLDB uses `name` for a terminal label.
 30        let mut configuration = task_definition.config.clone();
 31
 32        configuration
 33            .as_object_mut()
 34            .context("CodeLLDB is not a valid json object")?
 35            .insert(
 36                "name".into(),
 37                Value::String(String::from(task_definition.label.as_ref())),
 38            );
 39
 40        let request = self.validate_config(&configuration)?;
 41
 42        Ok(dap::StartDebuggingRequestArguments {
 43            request,
 44            configuration,
 45        })
 46    }
 47
 48    async fn fetch_latest_adapter_version(
 49        &self,
 50        delegate: &Arc<dyn DapDelegate>,
 51    ) -> Result<AdapterVersion> {
 52        let release =
 53            latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
 54
 55        let arch = match std::env::consts::ARCH {
 56            "aarch64" => "arm64",
 57            "x86_64" => "x64",
 58            unsupported => {
 59                anyhow::bail!("unsupported architecture {unsupported}");
 60            }
 61        };
 62        let platform = match std::env::consts::OS {
 63            "macos" => "darwin",
 64            "linux" => "linux",
 65            "windows" => "win32",
 66            unsupported => {
 67                anyhow::bail!("unsupported operating system {unsupported}");
 68            }
 69        };
 70        let asset_name = format!("codelldb-{platform}-{arch}.vsix");
 71        let ret = AdapterVersion {
 72            tag_name: release.tag_name,
 73            url: release
 74                .assets
 75                .iter()
 76                .find(|asset| asset.name == asset_name)
 77                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 78                .browser_download_url
 79                .clone(),
 80        };
 81
 82        Ok(ret)
 83    }
 84}
 85
 86#[async_trait(?Send)]
 87impl DebugAdapter for CodeLldbDebugAdapter {
 88    fn name(&self) -> DebugAdapterName {
 89        DebugAdapterName(Self::ADAPTER_NAME.into())
 90    }
 91
 92    fn validate_config(
 93        &self,
 94        config: &serde_json::Value,
 95    ) -> Result<StartDebuggingRequestArgumentsRequest> {
 96        let map = config
 97            .as_object()
 98            .ok_or_else(|| anyhow!("Config isn't an object"))?;
 99
100        let request_variant = map
101            .get("request")
102            .and_then(|r| r.as_str())
103            .ok_or_else(|| anyhow!("request field is required and must be a string"))?;
104
105        match request_variant {
106            "launch" => {
107                // For launch, verify that one of the required configs exists
108                if !(map.contains_key("program")
109                    || map.contains_key("targetCreateCommands")
110                    || map.contains_key("cargo"))
111                {
112                    return Err(anyhow!(
113                        "launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
114                    ));
115                }
116                Ok(StartDebuggingRequestArgumentsRequest::Launch)
117            }
118            "attach" => {
119                // For attach, verify that either pid or program exists
120                if !(map.contains_key("pid") || map.contains_key("program")) {
121                    return Err(anyhow!(
122                        "attach request requires either 'pid' or 'program' field"
123                    ));
124                }
125                Ok(StartDebuggingRequestArgumentsRequest::Attach)
126            }
127            _ => Err(anyhow!(
128                "request must be either 'launch' or 'attach', got '{}'",
129                request_variant
130            )),
131        }
132    }
133
134    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
135        let mut configuration = json!({
136            "request": match zed_scenario.request {
137                DebugRequest::Launch(_) => "launch",
138                DebugRequest::Attach(_) => "attach",
139            },
140        });
141        let map = configuration.as_object_mut().unwrap();
142        // CodeLLDB uses `name` for a terminal label.
143        map.insert(
144            "name".into(),
145            Value::String(String::from(zed_scenario.label.as_ref())),
146        );
147        match &zed_scenario.request {
148            DebugRequest::Attach(attach) => {
149                map.insert("pid".into(), attach.process_id.into());
150            }
151            DebugRequest::Launch(launch) => {
152                map.insert("program".into(), launch.program.clone().into());
153
154                if !launch.args.is_empty() {
155                    map.insert("args".into(), launch.args.clone().into());
156                }
157                if !launch.env.is_empty() {
158                    map.insert("env".into(), launch.env_json());
159                }
160                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
161                    map.insert("stopOnEntry".into(), stop_on_entry.into());
162                }
163                if let Some(cwd) = launch.cwd.as_ref() {
164                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
165                }
166            }
167        }
168
169        Ok(DebugScenario {
170            adapter: zed_scenario.adapter,
171            label: zed_scenario.label,
172            config: configuration,
173            build: None,
174            tcp_connection: None,
175        })
176    }
177
178    async fn dap_schema(&self) -> serde_json::Value {
179        json!({
180            "properties": {
181                "request": {
182                    "type": "string",
183                    "enum": ["attach", "launch"],
184                    "description": "Debug adapter request type"
185                },
186                "program": {
187                    "type": "string",
188                    "description": "Path to the program to debug or attach to"
189                },
190                "args": {
191                    "type": ["array", "string"],
192                    "description": "Program arguments"
193                },
194                "cwd": {
195                    "type": "string",
196                    "description": "Program working directory"
197                },
198                "env": {
199                    "type": "object",
200                    "description": "Additional environment variables",
201                    "patternProperties": {
202                        ".*": {
203                            "type": "string"
204                        }
205                    }
206                },
207                "envFile": {
208                    "type": "string",
209                    "description": "File to read the environment variables from"
210                },
211                "stdio": {
212                    "type": ["null", "string", "array", "object"],
213                    "description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
214                },
215                "terminal": {
216                    "type": "string",
217                    "enum": ["integrated", "console"],
218                    "description": "Terminal type to use",
219                    "default": "integrated"
220                },
221                "console": {
222                    "type": "string",
223                    "enum": ["integratedTerminal", "internalConsole"],
224                    "description": "Terminal type to use (compatibility alias of 'terminal')"
225                },
226                "stopOnEntry": {
227                    "type": "boolean",
228                    "description": "Automatically stop debuggee after launch",
229                    "default": false
230                },
231                "initCommands": {
232                    "type": "array",
233                    "description": "Initialization commands executed upon debugger startup",
234                    "items": {
235                        "type": "string"
236                    }
237                },
238                "targetCreateCommands": {
239                    "type": "array",
240                    "description": "Commands that create the debug target",
241                    "items": {
242                        "type": "string"
243                    }
244                },
245                "preRunCommands": {
246                    "type": "array",
247                    "description": "Commands executed just before the program is launched",
248                    "items": {
249                        "type": "string"
250                    }
251                },
252                "processCreateCommands": {
253                    "type": "array",
254                    "description": "Commands that create the debuggee process",
255                    "items": {
256                        "type": "string"
257                    }
258                },
259                "postRunCommands": {
260                    "type": "array",
261                    "description": "Commands executed just after the program has been launched",
262                    "items": {
263                        "type": "string"
264                    }
265                },
266                "preTerminateCommands": {
267                    "type": "array",
268                    "description": "Commands executed just before the debuggee is terminated or disconnected from",
269                    "items": {
270                        "type": "string"
271                    }
272                },
273                "exitCommands": {
274                    "type": "array",
275                    "description": "Commands executed at the end of debugging session",
276                    "items": {
277                        "type": "string"
278                    }
279                },
280                "expressions": {
281                    "type": "string",
282                    "enum": ["simple", "python", "native"],
283                    "description": "The default evaluator type used for expressions"
284                },
285                "sourceMap": {
286                    "type": "object",
287                    "description": "Source path remapping between the build machine and the local machine",
288                    "patternProperties": {
289                        ".*": {
290                            "type": ["string", "null"]
291                        }
292                    }
293                },
294                "relativePathBase": {
295                    "type": "string",
296                    "description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
297                },
298                "sourceLanguages": {
299                    "type": "array",
300                    "description": "A list of source languages to enable language-specific features for",
301                    "items": {
302                        "type": "string"
303                    }
304                },
305                "reverseDebugging": {
306                    "type": "boolean",
307                    "description": "Enable reverse debugging",
308                    "default": false
309                },
310                "breakpointMode": {
311                    "type": "string",
312                    "enum": ["path", "file"],
313                    "description": "Specifies how source breakpoints should be set"
314                },
315                "pid": {
316                    "type": ["integer", "string"],
317                    "description": "Process id to attach to"
318                },
319                "waitFor": {
320                    "type": "boolean",
321                    "description": "Wait for the process to launch (MacOS only)",
322                    "default": false
323                }
324            },
325            "required": ["request"],
326            "allOf": [
327                {
328                    "if": {
329                        "properties": {
330                            "request": {
331                                "enum": ["launch"]
332                            }
333                        }
334                    },
335                    "then": {
336                        "oneOf": [
337                            {
338                                "required": ["program"]
339                            },
340                            {
341                                "required": ["targetCreateCommands"]
342                            },
343                            {
344                                "required": ["cargo"]
345                            }
346                        ]
347                    }
348                },
349                {
350                    "if": {
351                        "properties": {
352                            "request": {
353                                "enum": ["attach"]
354                            }
355                        }
356                    },
357                    "then": {
358                        "oneOf": [
359                            {
360                                "required": ["pid"]
361                            },
362                            {
363                                "required": ["program"]
364                            }
365                        ]
366                    }
367                }
368            ]
369        })
370    }
371
372    async fn get_binary(
373        &self,
374        delegate: &Arc<dyn DapDelegate>,
375        config: &DebugTaskDefinition,
376        user_installed_path: Option<PathBuf>,
377        _: &mut AsyncApp,
378    ) -> Result<DebugAdapterBinary> {
379        let mut command = user_installed_path
380            .map(|p| p.to_string_lossy().to_string())
381            .or(self.path_to_codelldb.get().cloned());
382
383        if command.is_none() {
384            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
385            let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
386            let version_path =
387                if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
388                    adapters::download_adapter_from_github(
389                        self.name(),
390                        version.clone(),
391                        adapters::DownloadedFileType::Vsix,
392                        delegate.as_ref(),
393                    )
394                    .await?;
395                    let version_path =
396                        adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
397                    remove_matching(&adapter_path, |entry| entry != version_path).await;
398                    version_path
399                } else {
400                    let mut paths = delegate.fs().read_dir(&adapter_path).await?;
401                    paths.next().await.context("No adapter found")??
402                };
403            let adapter_dir = version_path.join("extension").join("adapter");
404            let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
405            // todo("windows")
406            #[cfg(not(windows))]
407            {
408                use smol::fs;
409
410                fs::set_permissions(
411                    &path,
412                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
413                )
414                .await
415                .with_context(|| format!("Settings executable permissions to {path:?}"))?;
416
417                let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
418                let mut lldb_binaries =
419                    fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
420                        format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
421                    })?;
422                while let Some(binary) = lldb_binaries.next().await {
423                    let binary_entry = binary?;
424                    let path = binary_entry.path();
425                    fs::set_permissions(
426                        &path,
427                        <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
428                    )
429                    .await
430                    .with_context(|| format!("Settings executable permissions to {path:?}"))?;
431                }
432            }
433            self.path_to_codelldb.set(path.clone()).ok();
434            command = Some(path);
435        };
436
437        Ok(DebugAdapterBinary {
438            command: command.unwrap(),
439            cwd: Some(delegate.worktree_root_path().to_path_buf()),
440            arguments: vec![
441                "--settings".into(),
442                json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
443            ],
444            request_args: self.request_args(&config)?,
445            envs: HashMap::default(),
446            connection: None,
447        })
448    }
449}