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

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": 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 

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: 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 

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: 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]}") 

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).")