vibefpmgui.debug_adapters

Which debugger (DAP adapter) to use, per platform (see plan/debugger.md):

  • Linux: gdb ≥ 14 (gdb --interpreter=dap): it knows Fortran (arrays with their bounds, derived types, module variables). Else lldb-dap.
  • macOS: lldb-dap: Apple's (xcrun -f lldb-dap, its signed debug server: no code-signing prompts), else one on the PATH. (gdb doesn't run on Apple Silicon; lldb shows no variables of gfortran's programs.)
  • Windows: gdb ≥ 14 on the PATH, else lldb-dap.

Finding them runs programs (gdb --version): done once, then remembered.

 1"""Which debugger (DAP adapter) to use, per platform (see plan/debugger.md):
 2
 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.
 9
10Finding them runs programs (``gdb --version``): done once, then remembered.
11"""
12
13from __future__ import annotations
14
15import re
16import shutil
17import sys
18from dataclasses import dataclass
19
20from . import console
21
22GDB_MINIMUM = 14  # the first gdb with DAP
23
24
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 = ""
30
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()
35
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":
40            arguments["stopOnEntry"] = False
41        return arguments
42
43
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:
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
50
51
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:
56        return None
57    version = _gdb_version(gdb)
58    if version is None or version[0] < GDB_MINIMUM:
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]}")
62
63
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
72
73
74_found: dict[str, Adapter | None] = {}
75
76
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"]
83
84
85def forget():
86    """Look for the debugger again next time (a new environment)."""
87    _found.clear()
88
89
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).")
GDB_MINIMUM = 14
@dataclass(frozen=True)
class Adapter:
26@dataclass(frozen=True)
27class Adapter:
28    kind: str  # "gdb" or "lldb"
29    command: list[str]  # the adapter: its program and arguments
30    version: str = ""
31
32    @property
33    def title(self) -> str:
34        """As shown (the Debug panel's header): "gdb 18.1"."""
35        return f"{self.kind} {self.version}".strip()
36
37    def launch_arguments(self, program: str, args: list[str], cwd: str) -> dict:
38        """The ``launch`` request's arguments, as this adapter names them."""
39        arguments = {"program": program, "args": list(args), "cwd": cwd}
40        if self.kind == "lldb":
41            arguments["stopOnEntry"] = False
42        return arguments
Adapter(kind: str, command: list[str], version: str = '')
kind: str
command: list[str]
version: str = ''
title: str
32    @property
33    def title(self) -> str:
34        """As shown (the Debug panel's header): "gdb 18.1"."""
35        return f"{self.kind} {self.version}".strip()

As shown (the Debug panel's header): "gdb 18.1".

def launch_arguments(self, program: str, args: list[str], cwd: str) -> dict:
37    def launch_arguments(self, program: str, args: list[str], cwd: str) -> dict:
38        """The ``launch`` request's arguments, as this adapter names them."""
39        arguments = {"program": program, "args": list(args), "cwd": cwd}
40        if self.kind == "lldb":
41            arguments["stopOnEntry"] = False
42        return arguments

The launch request's arguments, as this adapter names them.

def gdb_adapter() -> Adapter | None:
53def gdb_adapter() -> Adapter | None:
54    """gdb on the PATH, if it's new enough to speak DAP."""
55    gdb = shutil.which("gdb")
56    if gdb is None:
57        return None
58    version = _gdb_version(gdb)
59    if version is None or version[0] < GDB_MINIMUM:
60        return None
61    # (-q: no banner, which would be shown as the program's output.)
62    return Adapter("gdb", [gdb, "-q", "--interpreter=dap"], f"{version[0]}.{version[1]}")

gdb on the PATH, if it's new enough to speak DAP.

def lldb_adapter() -> Adapter | None:
65def lldb_adapter() -> Adapter | None:
66    """lldb-dap: Apple's on macOS, else one on the PATH."""
67    if sys.platform == "darwin":
68        code, output, _err = console.run(["xcrun", "-f", "lldb-dap"], timeout=10)
69        if code == 0 and output and output.strip():
70            return Adapter("lldb", [output.decode().strip()])
71    program = shutil.which("lldb-dap")
72    return Adapter("lldb", [program]) if program else None

lldb-dap: Apple's on macOS, else one on the PATH.

def find_adapter() -> Adapter | None:
78def find_adapter() -> Adapter | None:
79    """The debugger for this platform (None: none found), looked for once."""
80    if "adapter" not in _found:
81        finders = (lldb_adapter, gdb_adapter) if sys.platform == "darwin" else (gdb_adapter, lldb_adapter)
82        _found["adapter"] = next((a for a in (f() for f in finders) if a is not None), None)
83    return _found["adapter"]

The debugger for this platform (None: none found), looked for once.

def forget():
86def forget():
87    """Look for the debugger again next time (a new environment)."""
88    _found.clear()

Look for the debugger again next time (a new environment).

NOT_FOUND = "No debugger was found. On Linux, install gdb 14 or newer (the project's pixi environment has it); on macOS, lldb-dap comes with Xcode's Command Line Tools (xcode-select --install)."