Coverage for vibefpmgui/debug_adapters.py: 70%
50 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""Which debugger (DAP adapter) to use, per platform (see plan/debugger.md):
3- **Linux:** gdb ≥ 14 (``gdb --interpreter=dap``): it knows Fortran (arrays
4 with their bounds, derived types, module variables). Else lldb-dap.
5- **macOS:** lldb-dap: Apple's (``xcrun -f lldb-dap``, its signed debug
6 server: no code-signing prompts), else one on the PATH. (gdb doesn't run on
7 Apple Silicon; lldb shows no variables of gfortran's programs.)
8- **Windows:** gdb ≥ 14 on the PATH, else lldb-dap.
10Finding them runs programs (``gdb --version``): done once, then remembered.
11"""
13from __future__ import annotations
15import re
16import shutil
17import sys
18from dataclasses import dataclass
20from . import console
22GDB_MINIMUM = 14 # the first gdb with DAP
25@dataclass(frozen=True)
26class Adapter:
27 kind: str # "gdb" or "lldb"
28 command: list[str] # the adapter: its program and arguments
29 version: str = ""
31 @property
32 def title(self) -> str:
33 """As shown (the Debug panel's header): "gdb 18.1"."""
34 return f"{self.kind} {self.version}".strip()
36 def launch_arguments(self, program: str, args: list[str], cwd: str) -> dict:
37 """The ``launch`` request's arguments, as this adapter names them."""
38 arguments = {"program": program, "args": list(args), "cwd": cwd}
39 if self.kind == "lldb": 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 arguments["stopOnEntry"] = False
41 return arguments
44def _gdb_version(gdb: str) -> tuple[int, int] | None:
45 code, output, _err = console.run([gdb, "--version"], timeout=10)
46 if code != 0 or not output: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 return None
48 match = re.search(rb"GNU gdb[^\n]*?(\d+)\.(\d+)", output)
49 return (int(match.group(1)), int(match.group(2))) if match else None
52def gdb_adapter() -> Adapter | None:
53 """gdb on the PATH, if it's new enough to speak DAP."""
54 gdb = shutil.which("gdb")
55 if gdb is None: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 return None
57 version = _gdb_version(gdb)
58 if version is None or version[0] < GDB_MINIMUM: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true
59 return None
60 # (-q: no banner, which would be shown as the program's output.)
61 return Adapter("gdb", [gdb, "-q", "--interpreter=dap"], f"{version[0]}.{version[1]}")
64def lldb_adapter() -> Adapter | None:
65 """lldb-dap: Apple's on macOS, else one on the PATH."""
66 if sys.platform == "darwin":
67 code, output, _err = console.run(["xcrun", "-f", "lldb-dap"], timeout=10)
68 if code == 0 and output and output.strip():
69 return Adapter("lldb", [output.decode().strip()])
70 program = shutil.which("lldb-dap")
71 return Adapter("lldb", [program]) if program else None
74_found: dict[str, Adapter | None] = {}
77def find_adapter() -> Adapter | None:
78 """The debugger for this platform (None: none found), looked for once."""
79 if "adapter" not in _found:
80 finders = (lldb_adapter, gdb_adapter) if sys.platform == "darwin" else (gdb_adapter, lldb_adapter)
81 _found["adapter"] = next((a for a in (f() for f in finders) if a is not None), None)
82 return _found["adapter"]
85def forget():
86 """Look for the debugger again next time (a new environment)."""
87 _found.clear()
90NOT_FOUND = ("No debugger was found. On Linux, install gdb 14 or newer (the project's pixi environment has it); "
91 "on macOS, lldb-dap comes with Xcode's Command Line Tools (xcode-select --install).")