1# Copyright (c) 2025 STMicroelectronics
2#
3# SPDX-License-Identifier: Apache-2.0
4
5"""
6Runner for debugging applications using the ST-LINK GDB server
7from STMicroelectronics, provided as part of the STM32CubeCLT.
8"""
9
10import argparse
11import platform
12import re
13import shutil
14from pathlib import Path
15
16from runners.core import MissingProgram, RunnerCaps, RunnerConfig, ZephyrBinaryRunner
17
18STLINK_GDB_SERVER_DEFAULT_PORT = 61234
19
20
21class STLinkGDBServerRunner(ZephyrBinaryRunner):
22    @classmethod
23    def _get_stm32cubeclt_paths(cls) -> tuple[Path, Path]:
24        """
25        Returns a tuple of two elements of class pathlib.Path:
26            [0]: path to the ST-LINK_gdbserver executable
27            [1]: path to the "STM32CubeProgrammer/bin" folder
28        """
29
30        def find_highest_clt_version(tools_folder: Path) -> Path | None:
31            if not tools_folder.is_dir():
32                return None
33
34            # List all CubeCLT installations present in tools folder
35            CUBECLT_FLDR_RE = re.compile(r"stm32cubeclt_([1-9]).(\d+).(\d+)", re.IGNORECASE)
36            installations: list[tuple[int, Path]] = []
37            for f in tools_folder.iterdir():
38                m = CUBECLT_FLDR_RE.match(f.name)
39                if m is not None:
40                    # Compute a number that can be easily compared
41                    # from the STM32CubeCLT version number
42                    major, minor, revis = int(m[1]), int(m[2]), int(m[3])
43                    ver_num = major * 1000000 + minor * 1000 + revis
44                    installations.append((ver_num, f))
45
46            if len(installations) == 0:
47                return None
48
49            # Sort candidates and return the path to the most recent version
50            most_recent_install = sorted(installations, key=lambda e: e[0], reverse=True)[0]
51            return most_recent_install[1]
52
53        cur_platform = platform.system()
54
55        # Attempt to find via shutil.which()
56        if cur_platform in ["Linux", "Windows"]:
57            gdbserv = shutil.which("ST-LINK_gdbserver")
58            cubeprg = shutil.which("STM32_Programmer_CLI")
59            if gdbserv and cubeprg:
60                # Return the parent of cubeprg as [1] should be the path
61                # to the folder containing STM32_Programmer_CLI, not the
62                # path to the executable itself
63                return (Path(gdbserv), Path(cubeprg).parent)
64
65        # Search in OS-specific paths
66        search_path: str
67        tool_suffix = ""
68        if cur_platform == "Linux":
69            search_path = "/opt/st/"
70        elif cur_platform == "Windows":
71            search_path = "C:\\ST\\"
72            tool_suffix = ".exe"
73        elif cur_platform == "Darwin":
74            search_path = "/opt/ST/"
75        else:
76            raise RuntimeError("Unsupported OS")
77
78        clt = find_highest_clt_version(Path(search_path))
79        if clt is None:
80            raise MissingProgram("ST-LINK_gdbserver (from STM32CubeCLT)")
81
82        gdbserver_path = clt / "STLink-gdb-server" / "bin" / f"ST-LINK_gdbserver{tool_suffix}"
83        cubeprg_bin_path = clt / "STM32CubeProgrammer" / "bin"
84
85        return (gdbserver_path, cubeprg_bin_path)
86
87    @classmethod
88    def name(cls) -> str:
89        return "stlink_gdbserver"
90
91    @classmethod
92    def capabilities(cls) -> RunnerCaps:
93        return RunnerCaps(commands={"attach", "debug", "debugserver"}, dev_id=True, extload=True)
94
95    @classmethod
96    def extload_help(cls) -> str:
97        return "External Loader for ST-Link GDB server"
98
99    @classmethod
100    def do_add_parser(cls, parser: argparse.ArgumentParser):
101        # Expose a subset of the ST-LINK GDB server arguments
102        parser.add_argument(
103            "--swd", action='store_true', default=True, help="Enable SWD debug mode"
104        )
105        parser.add_argument("--apid", type=int, default=0, help="Target DAP ID")
106        parser.add_argument(
107            "--port-number",
108            type=int,
109            default=STLINK_GDB_SERVER_DEFAULT_PORT,
110            help="Port number for GDB client",
111        )
112
113    @classmethod
114    def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> "STLinkGDBServerRunner":
115        return STLinkGDBServerRunner(
116            cfg, args.swd, args.apid, args.dev_id, args.port_number, args.extload
117        )
118
119    def __init__(
120        self,
121        cfg: RunnerConfig,
122        swd: bool,
123        ap_id: int | None,
124        stlink_serial: str | None,
125        gdb_port: int,
126        external_loader: str | None,
127    ):
128        super().__init__(cfg)
129        self.ensure_output('elf')
130
131        self._swd = swd
132        self._gdb_port = gdb_port
133        self._stlink_serial = stlink_serial
134        self._ap_id = ap_id
135        self._external_loader = external_loader
136
137    def do_run(self, command: str, **kwargs):
138        if command in ["attach", "debug", "debugserver"]:
139            self.do_attach_debug_debugserver(command)
140        else:
141            raise ValueError(f"{command} not supported")
142
143    def do_attach_debug_debugserver(self, command: str):
144        # self.ensure_output('elf') is called in constructor
145        # and validated that self.cfg.elf_file is non-null.
146        # This assertion is required for the test framework,
147        # which doesn't have this insight - it should never
148        # trigger in real-world scenarios.
149        assert self.cfg.elf_file is not None
150        elf_path = Path(self.cfg.elf_file).as_posix()
151
152        gdb_args = ["-ex", f"target remote :{self._gdb_port}", elf_path]
153
154        (gdbserver_path, cubeprg_path) = STLinkGDBServerRunner._get_stm32cubeclt_paths()
155        gdbserver_cmd = [gdbserver_path.as_posix()]
156        gdbserver_cmd += ["--stm32cubeprogrammer-path", str(cubeprg_path.absolute())]
157        gdbserver_cmd += ["--port-number", str(self._gdb_port)]
158        gdbserver_cmd += ["--apid", str(self._ap_id)]
159        gdbserver_cmd += ["--halt"]
160
161        if self._swd:
162            gdbserver_cmd.append("--swd")
163
164        if command == "attach":
165            gdbserver_cmd += ["--attach"]
166        else:  # debug/debugserver
167            gdbserver_cmd += ["--initialize-reset"]
168            gdb_args += ["-ex", f"load {elf_path}"]
169
170        if self._stlink_serial:
171            gdbserver_cmd += ["--serial-number", self._stlink_serial]
172
173        if self._external_loader:
174            extldr_path = cubeprg_path / "ExternalLoader" / self._external_loader
175            if not extldr_path.exists():
176                raise RuntimeError(f"External loader {self._external_loader} does not exist")
177            gdbserver_cmd += ["--extload", str(extldr_path)]
178
179        self.require(gdbserver_cmd[0])
180
181        if command == "debugserver":
182            self.check_call(gdbserver_cmd)
183        elif self.cfg.gdb is None:  # attach/debug
184            raise RuntimeError("GDB is required for attach/debug")
185        else:  # attach/debug
186            gdb_cmd = [self.cfg.gdb] + gdb_args
187            self.require(gdb_cmd[0])
188            self.run_server_and_client(gdbserver_cmd, gdb_cmd)
189