diff mbox series

[08/14] devtool: ide-sdk debugger back-end abstraction

Message ID 20260802195324.64533-9-adrian.freihofer@siemens.com
State New
Headers show
Series devtool ide-sdk: clang and lldb support | expand

Commit Message

AdrianF Aug. 2, 2026, 7:52 p.m. UTC
From: Adrian Freihofer <adrian.freihofer@siemens.com>

Refactor the devtool ide-sdk support for remote debugging with
gdbserver. The main goal is to cleanly separate the generation of the
host-side debugger configuration (gdbinit, wrapper scripts) from the
IDE-specific launch/task config generation, and to provide a common
interface for supporting multiple debug server back-ends (gdbserver,
lldb-server) in the future.

The following renames are applied throughout the devtool ide-sdk code:
- GdbServerModes              -> DebuggerServerModes (enum class)
- GdbCrossConfig              -> DebuggerCrossConfig (base) + GdbCrossConfig (subclass)
- RecipeModified.gdb_cross    -> debugger_cross (attribute)
- DebuggerCrossConfig.gdb_cross -> debugger_cross (attribute)
- gdbserver_default_mode      -> default_mode (attribute)
- gdbserver_port / port       -> debug_server_port (attribute)
- gdbserver_mode (parameter/variable) -> server_mode
- gdbserver_script_file / gdbserver_script -> server_script_file / server_script
- _target_gdbserver_start_cmd -> _target_start_cmd
- _target_gdbserver_kill_cmd  -> _target_kill_cmd
- gdb_cross_configs           -> cross_debug_configs (IdeBase attribute)
- initialize_gdb_cross_configs -> initialize_cross_debug_configs (IdeBase method)

Also: skip building gdb-cross for clang-toolchain recipes
(toolchain == 'gcc' guard). Clang recipes will use lldb-server instead
of gdbserver in the future. For gcc recipes the behaviour is unchanged.

Also fix a typo in the GDB configuration generator where the property
was named "is_c_ccp" instead of "is_c_cpp".

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_plugins/__init__.py | 187 +++++++++++---------
 scripts/lib/devtool/ide_plugins/ide_code.py | 155 ++++++++--------
 scripts/lib/devtool/ide_plugins/ide_none.py |  54 +++---
 scripts/lib/devtool/ide_sdk.py              |  25 +--
 4 files changed, 232 insertions(+), 189 deletions(-)
diff mbox series

Patch

diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py
index 94ed7c9e6d..4a1686a034 100644
--- a/scripts/lib/devtool/ide_plugins/__init__.py
+++ b/scripts/lib/devtool/ide_plugins/__init__.py
@@ -22,7 +22,7 @@  class BuildTool(Enum):
     KERNEL_MODULE = auto()
 
     @property
-    def is_c_ccp(self):
+    def is_c_cpp(self):
         if self is BuildTool.CMAKE:
             return True
         if self is BuildTool.MESON:
@@ -31,134 +31,157 @@  class BuildTool(Enum):
 
     @property
     def is_c_cpp_kernel(self):
-        if self.is_c_ccp or self is BuildTool.KERNEL_MODULE:
+        if self.is_c_cpp or self is BuildTool.KERNEL_MODULE:
             return True
         return False
 
 
-class GdbServerModes(Enum):
+class DebuggerServerModes(Enum):
     ONCE = auto()
     ATTACH = auto()
     MULTI = auto()
 
 
-class GdbCrossConfig:
-    """Base class defining the GDB configuration generator interface
+class DebuggerCrossConfig:
+    """Base class defining the cross-debugger configuration generator interface.
 
-    Generate a GDB configuration for a binary on the target device.
+    Manages the per-binary port assignment, script paths, and SSH argument
+    construction that are common to all debugger back-ends (GDB, LLDB).
+    Concrete subclasses provide the back-end-specific remote start/kill commands.
     """
-    _gdbserver_port_next = 1234
-    _gdb_cross_configs = {}
+    _port_next = 1234
+    _configs = {}
 
-    def __init__(self, image_recipe, modified_recipe, binary, gdbserver_default_mode):
+    def __init__(self, image_recipe, modified_recipe, binary, default_mode):
         self.image_recipe = image_recipe
         self.modified_recipe = modified_recipe
-        self.gdb_cross = modified_recipe.gdb_cross
+        self.debugger_cross = modified_recipe.debugger_cross
         self.binary = binary
-        self.gdbserver_default_mode = gdbserver_default_mode
+        self.default_mode = default_mode
         self.binary_pretty = self.binary.binary_path.replace(os.sep, '-').lstrip('-')
-        self.gdbserver_port = GdbCrossConfig._gdbserver_port_next
-        GdbCrossConfig._gdbserver_port_next += 1
-        self.id_pretty = "%d_%s" % (self.gdbserver_port, self.binary_pretty)
+        self.debug_server_port = DebuggerCrossConfig._port_next
+        DebuggerCrossConfig._port_next += 1
+        self.id_pretty = "%d_%s" % (self.debug_server_port, self.binary_pretty)
 
-        # Track all generated gdbserver configs to avoid duplicates
-        if self.id_pretty in GdbCrossConfig._gdb_cross_configs:
+        if self.id_pretty in DebuggerCrossConfig._configs:
             raise DevtoolError(
-                "gdbserver config for binary %s is already generated" % binary)
-        GdbCrossConfig._gdb_cross_configs[self.id_pretty] = self
+                "debugger config for binary %s is already generated" % binary)
+        DebuggerCrossConfig._configs[self.id_pretty] = self
 
-    def id_pretty_mode(self, gdbserver_mode):
-        return "%s_%s" % (self.id_pretty, gdbserver_mode.name.lower())
+    def id_pretty_mode(self, mode):
+        return "%s_%s" % (self.id_pretty, mode.name.lower())
 
-    # GDB and gdbserver script on the host
+    # Host-side script paths
     @property
     def script_dir(self):
         return self.modified_recipe.ide_sdk_scripts_dir
 
+    def server_script(self, mode):
+        raise NotImplementedError
+
+    # SSH argument helpers
+    def _target_ssh_args(self):
+        ssh_args = []
+        if self.debugger_cross.target_device.ssh_port:
+            ssh_args += self.debugger_cross.target_device.ssh_port
+        if self.debugger_cross.target_device.extraoptions:
+            ssh_args.extend(self.debugger_cross.target_device.extraoptions)
+        if self.debugger_cross.target_device.target:
+            ssh_args.append(self.debugger_cross.target_device.target)
+        return ssh_args
+
+    def server_modes(self):
+        """List of debug-server modes for which scripts are generated."""
+        modes = [self.default_mode]
+        if self.binary.runs_as_service and self.default_mode != DebuggerServerModes.ATTACH:
+            modes.append(DebuggerServerModes.ATTACH)
+        return modes
+
+    def initialize(self):
+        """Called after construction to generate any required config files."""
+        pass
+
+    # Abstract — subclasses must implement
+    def _target_start_cmd(self, mode):
+        raise NotImplementedError
+
+    def _target_kill_cmd(self):
+        raise NotImplementedError
+
+
+class GdbCrossConfig(DebuggerCrossConfig):
+    """GDB-specific cross-debugging configuration.
+
+    Manages gdbserver on the target and gdb-cross on the host.  Provides
+    gdbinit / gdb wrapper scripts used by ide=none as well as the
+    target-side tmp/pid/log paths consumed by the gdbserver start command.
+    """
+
+    def __init__(self, image_recipe, modified_recipe, binary,
+                 default_mode=DebuggerServerModes.MULTI):
+        super().__init__(image_recipe, modified_recipe, binary,
+                         default_mode)
+
+    # GDB-specific host paths
     @property
     def gdbinit_dir(self):
         return os.path.join(self.script_dir, 'gdbinit')
 
-    def gdbserver_script_file(self, gdbserver_mode):
-        return 'gdbserver_' + self.id_pretty_mode(gdbserver_mode)
-
-    def gdbserver_script(self, gdbserver_mode):
-        return os.path.join(self.script_dir, self.gdbserver_script_file(gdbserver_mode))
-
     @property
     def gdbinit(self):
-        return os.path.join(
-            self.gdbinit_dir, 'gdbinit_' + self.id_pretty)
+        return os.path.join(self.gdbinit_dir, 'gdbinit_' + self.id_pretty)
 
     @property
     def gdb_script(self):
-        return os.path.join(
-            self.script_dir, 'gdb_' + self.id_pretty)
+        return os.path.join(self.script_dir, 'gdb_' + self.id_pretty)
+
+    def server_script_file(self, mode):
+        return 'gdbserver_' + self.id_pretty_mode(mode)
+
+    def server_script(self, mode):
+        return os.path.join(self.script_dir, self.server_script_file(mode))
 
     # gdbserver files on the target
-    def gdbserver_tmp_dir(self, gdbserver_mode):
-        return os.path.join('/tmp', 'gdbserver_%s' % self.id_pretty_mode(gdbserver_mode))
+    def _gdbserver_tmp_dir(self, mode):
+        return os.path.join('/tmp', 'gdbserver_%s' % self.id_pretty_mode(mode))
 
-    def gdbserver_pid_file(self, gdbserver_mode):
-        return os.path.join(self.gdbserver_tmp_dir(gdbserver_mode), 'gdbserver.pid')
+    def _gdbserver_pid_file(self, mode):
+        return os.path.join(self._gdbserver_tmp_dir(mode), 'gdbserver.pid')
 
-    def gdbserver_log_file(self, gdbserver_mode):
-        return os.path.join(self.gdbserver_tmp_dir(gdbserver_mode), 'gdbserver.log')
+    def _gdbserver_log_file(self, mode):
+        return os.path.join(self._gdbserver_tmp_dir(mode), 'gdbserver.log')
 
-    def _target_gdbserver_start_cmd(self, gdbserver_mode):
-        """Get the ssh command to start gdbserver on the target device
+    def _target_start_cmd(self, server_mode):
+        """SSH command to start gdbserver on the target device.
 
-        returns something like:
+        Returns something like:
           "\"/bin/sh -c '/usr/bin/gdbserver --once :1234 /usr/bin/cmake-example'\""
-        or for multi mode:
-          "\"/bin/sh -c 'if [ \"$1\" = \"stop\" ]; then ... else ... fi'\""
         """
-        if gdbserver_mode == GdbServerModes.ONCE:
+        if server_mode == DebuggerServerModes.ONCE:
             gdbserver_cmd_start = "%s --once :%s %s" % (
-                self.gdb_cross.gdbserver_path, self.gdbserver_port, self.binary.binary_path)
-        elif gdbserver_mode == GdbServerModes.ATTACH:
+                self.debugger_cross.debug_server_path, self.debug_server_port, self.binary.binary_path)
+        elif server_mode == DebuggerServerModes.ATTACH:
             pid_command = self.binary.pid_command
             if pid_command:
                 gdbserver_cmd_start = "%s --attach :%s \\$(%s)" % (
-                    self.gdb_cross.gdbserver_path,
-                    self.gdbserver_port,
+                    self.debugger_cross.debug_server_path,
+                    self.debug_server_port,
                     pid_command)
             else:
                 raise DevtoolError("Cannot use gdbserver attach mode for binary %s. No PID found." % self.binary.binary_path)
-        elif gdbserver_mode == GdbServerModes.MULTI:
-            gdbserver_cmd_start = "test -f %s && exit 0; " % self.gdbserver_pid_file(gdbserver_mode)
-            gdbserver_cmd_start += "mkdir -p %s; " % self.gdbserver_tmp_dir(gdbserver_mode)
+        elif server_mode == DebuggerServerModes.MULTI:
+            gdbserver_cmd_start = "test -f %s && exit 0; " % self._gdbserver_pid_file(server_mode)
+            gdbserver_cmd_start += "mkdir -p %s; " % self._gdbserver_tmp_dir(server_mode)
             gdbserver_cmd_start += "%s --multi :%s > %s 2>&1 & " % (
-                self.gdb_cross.gdbserver_path, self.gdbserver_port, self.gdbserver_log_file(gdbserver_mode))
-            gdbserver_cmd_start += "echo \\$! > %s;" % self.gdbserver_pid_file(gdbserver_mode)
+                self.debugger_cross.debug_server_path, self.debug_server_port, self._gdbserver_log_file(server_mode))
+            gdbserver_cmd_start += "echo \\$! > %s;" % self._gdbserver_pid_file(server_mode)
         else:
-            raise DevtoolError("Unsupported gdbserver mode: %s" % gdbserver_mode)
+            raise DevtoolError("Unsupported gdbserver mode: %s" % server_mode)
         return "\"/bin/sh -c '" + gdbserver_cmd_start + "'\""
 
-    def _target_gdbserver_kill_cmd(self):
-        """Get the ssh command to kill gdbserver on the target device"""
-        return "\"kill \\$(pgrep -o -f 'gdbserver --attach :%s') 2>/dev/null || true\"" % self.gdbserver_port
-
-    def _target_ssh_gdbserver_args(self):
-        ssh_args = []
-        if self.gdb_cross.target_device.ssh_port:
-            ssh_args += self.gdb_cross.target_device.ssh_port
-        if self.gdb_cross.target_device.extraoptions:
-            ssh_args.extend(self.gdb_cross.target_device.extraoptions)
-        if self.gdb_cross.target_device.target:
-            ssh_args.append(self.gdb_cross.target_device.target)
-        return ssh_args
-
-    def gdbserver_modes(self):
-        """Get the list of gdbserver modes for which scripts are generated"""
-        modes = [self.gdbserver_default_mode]
-        if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH:
-            modes.append(GdbServerModes.ATTACH)
-        return modes
-
-    def initialize(self):
-        """Interface function to initialize the gdb config generation"""
-        pass
+    def _target_kill_cmd(self):
+        """SSH command to kill gdbserver on the target device."""
+        return "\"kill \\$(pgrep -o -f 'gdbserver --attach :%s') 2>/dev/null || true\"" % self.debug_server_port
 
 
 
@@ -167,7 +190,7 @@  class IdeBase:
 
     def __init__(self):
         self.ide_name = 'undefined'
-        self.gdb_cross_configs = []
+        self.cross_debug_configs = []
 
     @classmethod
     def ide_plugin_priority(cls):
@@ -182,12 +205,12 @@  class IdeBase:
         logger.warn("Modified recipe mode is not supported for IDE %s" %
                     self.ide_name)
 
-    def initialize_gdb_cross_configs(self, image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfig):
+    def initialize_cross_debug_configs(self, image_recipe, modified_recipe, cross_debug_config_class=GdbCrossConfig):
         for _, exec_bin in modified_recipe.installed_binaries.items():
-            gdb_cross_config = gdb_cross_config_class(
+            cross_debug_config = cross_debug_config_class(
                 image_recipe, modified_recipe, exec_bin)
-            gdb_cross_config.initialize()
-            self.gdb_cross_configs.append(gdb_cross_config)
+            cross_debug_config.initialize()
+            self.cross_debug_configs.append(cross_debug_config)
 
     @staticmethod
     def gen_oe_scripts_sym_link(modified_recipe):
diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py
index 38f73e355c..dfaba3cff6 100644
--- a/scripts/lib/devtool/ide_plugins/ide_code.py
+++ b/scripts/lib/devtool/ide_plugins/ide_code.py
@@ -9,27 +9,27 @@  import json
 import logging
 import os
 import shutil
-from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, GdbServerModes, get_devtool_deploy_opts
+from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, DebuggerServerModes, get_devtool_deploy_opts
 
 logger = logging.getLogger('devtool')
 
 
 class GdbCrossConfigVSCode(GdbCrossConfig):
     def __init__(self, image_recipe, modified_recipe, binary,
-                 gdbserver_default_mode=GdbServerModes.ONCE):
+                 default_mode=DebuggerServerModes.ONCE):
         super().__init__(image_recipe, modified_recipe, binary,
-                         gdbserver_default_mode)
+                         default_mode)
 
-    def target_ssh_gdbserver_start_args(self, gdbserver_mode=None):
+    def target_ssh_gdbserver_start_args(self, mode=None):
         """Get the ssh command arguments to start gdbserver on the target device
 
         returns something like:
           ['-p', '2222', 'root@target', '"/bin/sh -c \'/usr/bin/gdbserver --once :1234 /usr/bin/cmake-example\'"']
         """
-        if gdbserver_mode is None:
-            gdbserver_mode = self.gdbserver_default_mode
-        return self._target_ssh_gdbserver_args() + [
-            self._target_gdbserver_start_cmd(gdbserver_mode)
+        if mode is None:
+            mode = self.default_mode
+        return self._target_ssh_args() + [
+            self._target_start_cmd(mode)
         ]
 
     def target_ssh_gdbserver_kill_args(self):
@@ -38,13 +38,10 @@  class GdbCrossConfigVSCode(GdbCrossConfig):
         returns something like:
           ['-p', '2222', 'root@target', '"kill $(pgrep -o -f \'gdbserver --attach :1234\') 2>/dev/null || true"']
         """
-        return self._target_ssh_gdbserver_args() + [
-            self._target_gdbserver_kill_cmd()
+        return self._target_ssh_args() + [
+            self._target_kill_cmd()
         ]
 
-    def initialize(self):
-        pass
-
 
 class IdeVSCode(IdeBase):
     """Manage IDE configurations for VSCode
@@ -329,22 +326,27 @@  class IdeVSCode(IdeBase):
         IdeBase.update_json_file(
             self.dot_code_dir(modified_recipe), prop_file, properties_dicts)
 
-    def vscode_launch_bin_dbg(self, gdb_cross_config, gdbserver_mode):
-        modified_recipe = gdb_cross_config.modified_recipe
+    def vscode_launch_bin_dbg(self, cross_debug_config, server_mode):
+        """Dispatch to the GDB launch config generator."""
+        return self._vscode_launch_bin_dbg_gdb(cross_debug_config, server_mode)
+
+    def _vscode_launch_bin_dbg_gdb(self, cross_debug_config, server_mode):
+        """Generate a cppdbg (GDB) launch configuration entry for launch.json."""
+        modified_recipe = cross_debug_config.modified_recipe
 
         launch_config = {
-            "name": gdb_cross_config.id_pretty_mode(gdbserver_mode),
+            "name": cross_debug_config.id_pretty_mode(server_mode),
             "type": "cppdbg",
             "request": "launch",
-            "program": gdb_cross_config.binary.binary_host_path,
+            "program": cross_debug_config.binary.binary_host_path,
             "stopAtEntry": True,
             "cwd": "${workspaceFolder}",
             "environment": [],
             "externalConsole": False,
             "MIMode": "gdb",
-            "preLaunchTask": gdb_cross_config.id_pretty_mode(gdbserver_mode),
-            "miDebuggerPath": modified_recipe.gdb_cross.gdb,
-            "miDebuggerServerAddress": "%s:%d" % (modified_recipe.gdb_cross.host, gdb_cross_config.gdbserver_port)
+            "preLaunchTask": cross_debug_config.id_pretty_mode(server_mode),
+            "miDebuggerPath": modified_recipe.debugger_cross.gdb,
+            "miDebuggerServerAddress": "%s:%d" % (modified_recipe.debugger_cross.host, cross_debug_config.debug_server_port)
         }
 
         # Search for header files in recipe-sysroot.
@@ -360,9 +362,9 @@  class IdeVSCode(IdeBase):
             }
         ]
 
-        if gdb_cross_config.image_recipe.rootfs_dbg:
+        if cross_debug_config.image_recipe.rootfs_dbg:
             launch_config['additionalSOLibSearchPath'] = modified_recipe.solib_search_path_str(
-                gdb_cross_config.image_recipe)
+                cross_debug_config.image_recipe)
             # First: Search for sources of this recipe in the workspace folder
             # If compiled with DEBUG_PREFIX_MAP = "", no reverse map is is needed. The binaries
             # contain the full path to the source files. But by default there is a reverse map.
@@ -383,7 +385,7 @@  class IdeVSCode(IdeBase):
                 )
             else:
                 src_file_map["/usr/src/debug"] = os.path.join(
-                    gdb_cross_config.image_recipe.rootfs_dbg, "usr", "src", "debug")
+                    cross_debug_config.image_recipe.rootfs_dbg, "usr", "src", "debug")
         else:
             logger.warning(
                 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
@@ -406,8 +408,8 @@  class IdeVSCode(IdeBase):
         launch_config['setupCommands'] = setup_commands
 
         # Add postDebugTask for attach mode to clean up gdbserver
-        if gdbserver_mode == GdbServerModes.ATTACH:
-            kill_task_label = "kill_gdbserver_" + gdb_cross_config.id_pretty_mode(gdbserver_mode)
+        if server_mode == DebuggerServerModes.ATTACH:
+            kill_task_label = "kill_gdbserver_" + cross_debug_config.id_pretty_mode(server_mode)
             launch_config["postDebugTask"] = kill_task_label
 
         return launch_config
@@ -423,10 +425,10 @@  class IdeVSCode(IdeBase):
         """
 
         configurations = []
-        for gdb_cross_config in self.gdb_cross_configs:
-            if gdb_cross_config.modified_recipe is modified_recipe:
-                for gdbserver_mode in gdb_cross_config.gdbserver_modes():
-                    configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config, gdbserver_mode))
+        for cross_debug_config in self.cross_debug_configs:
+            if cross_debug_config.modified_recipe is modified_recipe:
+                for server_mode in cross_debug_config.server_modes():
+                    configurations.append(self.vscode_launch_bin_dbg(cross_debug_config, server_mode))
         launch_dict = {
             "version": "0.2.0",
             "configurations": configurations
@@ -450,16 +452,16 @@  class IdeVSCode(IdeBase):
                 }
             ]
         }
-        for gdb_cross_config in self.gdb_cross_configs:
-            if gdb_cross_config.modified_recipe is not modified_recipe:
+        for cross_debug_config in self.cross_debug_configs:
+            if cross_debug_config.modified_recipe is not modified_recipe:
                 continue
-            for gdbserver_mode in gdb_cross_config.gdbserver_modes():
+            for server_mode in cross_debug_config.server_modes():
                 new_task = {
-                    "label": gdb_cross_config.id_pretty_mode(gdbserver_mode),
+                    "label": cross_debug_config.id_pretty_mode(server_mode),
                     "type": "shell",
                     "isBackground": True,
-                    "command": gdb_cross_config.gdb_cross.target_device.ssh_sshexec,
-                    "args": gdb_cross_config.target_ssh_gdbserver_start_args(gdbserver_mode),
+                    "command": cross_debug_config.debugger_cross.target_device.ssh_sshexec,
+                    "args": cross_debug_config.target_ssh_gdbserver_start_args(server_mode),
                     "problemMatcher": [
                         {
                             "pattern": [
@@ -479,7 +481,7 @@  class IdeVSCode(IdeBase):
                     ]
                 }
                 # Deploy the artifacts to the target before starting gdbserver if not already running
-                if gdbserver_mode != GdbServerModes.ATTACH:
+                if server_mode != DebuggerServerModes.ATTACH:
                     new_task['dependsOn'] = [
                         install_task_name
                     ]
@@ -495,13 +497,13 @@  class IdeVSCode(IdeBase):
                 # 4. Press the Stop button which detaches gdbserver from the debugged process
                 # 5. Start debugging again in attach mode
                 # Without this kill task, step 5 would fail because gdbserver is still running
-                if gdbserver_mode == GdbServerModes.ATTACH:
-                    new_task_kill_label = "kill_gdbserver_"+ gdb_cross_config.id_pretty_mode(gdbserver_mode)
+                if server_mode == DebuggerServerModes.ATTACH:
+                    new_task_kill_label = "kill_gdbserver_"+ cross_debug_config.id_pretty_mode(server_mode)
                     new_task_kill = {
                         "label": new_task_kill_label,
                         "type": "shell",
-                        "command": gdb_cross_config.gdb_cross.target_device.ssh_sshexec,
-                        "args": gdb_cross_config.target_ssh_gdbserver_kill_args(),
+                        "command": cross_debug_config.debugger_cross.target_device.ssh_sshexec,
+                        "args": cross_debug_config.target_ssh_gdbserver_kill_args(),
                         "presentation": {
                             "close": True
                         },
@@ -538,7 +540,7 @@  class IdeVSCode(IdeBase):
         launch.json 'reload kernel module' entry uses preLaunchTask: verify,
         providing a single F5 / click action for the complete reload cycle.
         """
-        td = modified_recipe.gdb_cross.target_device
+        td = modified_recipe.debugger_cross.target_device
         ko_name = modified_recipe.bpn + '.ko'
         # rmmod / lsmod use the kernel module name (- replaced by _ per kernel convention)
         mod_name = modified_recipe.bpn.replace('-', '_')
@@ -664,36 +666,49 @@  class IdeVSCode(IdeBase):
                 }
             ]
         }
-        if modified_recipe.gdb_cross:
-            for gdb_cross_config in self.gdb_cross_configs:
-                if gdb_cross_config.modified_recipe is not modified_recipe:
+        if modified_recipe.debugger_cross:
+            for cross_debug_config in self.cross_debug_configs:
+                if cross_debug_config.modified_recipe is not modified_recipe:
                     continue
-                for gdbserver_mode in gdb_cross_config.gdbserver_modes():
-                    new_task = {
-                        "label": gdb_cross_config.id_pretty(gdbserver_mode),
-                        "type": "shell",
-                        "isBackground": True,
-                        "command": gdb_cross_config.gdb_cross.target_device.ssh_sshexec,
-                        "args": gdb_cross_config.target_ssh_gdbserver_start_args(gdbserver_mode),
-                        "problemMatcher": [
-                            {
-                                "pattern": [
-                                    {
-                                        "regexp": ".",
-                                        "file": 1,
-                                        "location": 2,
-                                        "message": 3
+                for server_mode in cross_debug_config.server_modes():
+                    if server_mode == DebuggerServerModes.MULTI:
+                        # MULTI mode: SSH command blocks until port is ready, treat as
+                        # a regular non-background task (same as vscode_tasks_cpp).
+                        new_task = {
+                            "label": cross_debug_config.id_pretty_mode(server_mode),
+                            "type": "shell",
+                            "command": cross_debug_config.debugger_cross.target_device.ssh_sshexec,
+                            "args": cross_debug_config.target_ssh_gdbserver_start_args(server_mode),
+                            "problemMatcher": []
+                        }
+                    else:
+                        # ONCE / ATTACH: server runs for the whole session, needs
+                        # isBackground so VSCode does not wait for the task to exit.
+                        new_task = {
+                            "label": cross_debug_config.id_pretty_mode(server_mode),
+                            "type": "shell",
+                            "isBackground": True,
+                            "command": cross_debug_config.debugger_cross.target_device.ssh_sshexec,
+                            "args": cross_debug_config.target_ssh_gdbserver_start_args(server_mode),
+                            "problemMatcher": [
+                                {
+                                    "pattern": [
+                                        {
+                                            "regexp": ".",
+                                            "file": 1,
+                                            "location": 2,
+                                            "message": 3
+                                        }
+                                    ],
+                                    "background": {
+                                        "activeOnStart": True,
+                                        "beginsPattern": ".",
+                                        "endsPattern": ".",
                                     }
-                                ],
-                                "background": {
-                                    "activeOnStart": True,
-                                    "beginsPattern": ".",
-                                    "endsPattern": ".",
                                 }
-                            }
-                        ]
-                    }
-                    if gdbserver_mode != GdbServerModes.ATTACH:
+                            ]
+                        }
+                    if server_mode != DebuggerServerModes.ATTACH:
                         new_task['dependsOn'] = [
                             dt_build_deploy_label
                         ]
@@ -703,7 +718,7 @@  class IdeVSCode(IdeBase):
             self.dot_code_dir(modified_recipe), tasks_file, tasks_dict)
 
     def vscode_tasks(self, args, modified_recipe):
-        if modified_recipe.build_tool.is_c_ccp:
+        if modified_recipe.build_tool.is_c_cpp:
             self.vscode_tasks_cpp(args, modified_recipe)
         elif modified_recipe.build_tool == BuildTool.KERNEL_MODULE:
             self.vscode_tasks_kernel_module(args, modified_recipe)
@@ -715,7 +730,7 @@  class IdeVSCode(IdeBase):
         self.vscode_extensions(modified_recipe)
         self.vscode_c_cpp_properties(modified_recipe)
         if args.target:
-            self.initialize_gdb_cross_configs(
+            self.initialize_cross_debug_configs(
                 image_recipe, modified_recipe, GdbCrossConfigVSCode)
             self.vscode_launch(args, modified_recipe)
             self.vscode_tasks(args, modified_recipe)
diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py
index ed96afa33c..f390331776 100644
--- a/scripts/lib/devtool/ide_plugins/ide_none.py
+++ b/scripts/lib/devtool/ide_plugins/ide_none.py
@@ -9,38 +9,38 @@  import os
 import logging
 import stat
 from bb.utils import mkdirhier
-from devtool.ide_plugins import IdeBase, GdbCrossConfig, GdbServerModes
+from devtool.ide_plugins import IdeBase, GdbCrossConfig, DebuggerServerModes
 
 logger = logging.getLogger('devtool')
 
 
 class GdbCrossConfigNone(GdbCrossConfig):
     def __init__(self, image_recipe, modified_recipe, binary,
-                 gdbserver_default_mode=GdbServerModes.MULTI):
+                 default_mode=DebuggerServerModes.MULTI):
         super().__init__(image_recipe, modified_recipe, binary,
-                         gdbserver_default_mode)
+                         default_mode)
 
-    def _target_gdbserver_stop_cmd(self, gdbserver_mode):
+    def _target_gdbserver_stop_cmd(self, server_mode):
         """Kill a gdbserver process"""
         # This is the usual behavior: gdbserver is stopped on demand
-        if gdbserver_mode == GdbServerModes.MULTI:
+        if server_mode == DebuggerServerModes.MULTI:
             gdbserver_cmd_stop = "test -f %s && kill \\$(cat %s);" % (
-                self.gdbserver_pid_file(gdbserver_mode), self.gdbserver_pid_file(gdbserver_mode))
-            gdbserver_cmd_stop += " rm -rf %s" % self.gdbserver_tmp_dir(gdbserver_mode)
+                self._gdbserver_pid_file(server_mode), self._gdbserver_pid_file(server_mode))
+            gdbserver_cmd_stop += " rm -rf %s" % self._gdbserver_tmp_dir(server_mode)
         # This is unexpected since gdbserver should terminate after each debug session
         # Just kill all gdbserver instances to keep it simple
         else:
             gdbserver_cmd_stop = "killall gdbserver"
         return "\"/bin/sh -c '" + gdbserver_cmd_stop + "'\""
 
-    def _gen_gdbserver_start_script(self, gdbserver_mode=None):
+    def _gen_gdbserver_start_script(self, server_mode=None):
         """Generate a shell script starting the gdbserver on the remote device via ssh"""
-        if gdbserver_mode is None:
-            gdbserver_mode = self.gdbserver_default_mode
-        gdbserver_cmd_start = self._target_gdbserver_start_cmd(gdbserver_mode)
-        gdbserver_cmd_stop = self._target_gdbserver_stop_cmd(gdbserver_mode)
-        remote_ssh = "%s %s" % (self.gdb_cross.target_device.ssh_sshexec,
-                                " ".join(self._target_ssh_gdbserver_args()))
+        if server_mode is None:
+            server_mode = self.default_mode
+        gdbserver_cmd_start = self._target_start_cmd(server_mode)
+        gdbserver_cmd_stop = self._target_gdbserver_stop_cmd(server_mode)
+        remote_ssh = "%s %s" % (self.debugger_cross.target_device.ssh_sshexec,
+                                " ".join(self._target_ssh_args()))
         gdbserver_cmd = ['#!/bin/sh']
         gdbserver_cmd.append('if [ "$1" = "stop" ]; then')
         gdbserver_cmd.append('  shift')
@@ -48,26 +48,26 @@  class GdbCrossConfigNone(GdbCrossConfig):
         gdbserver_cmd.append('else')
         gdbserver_cmd.append("  %s %s" % (remote_ssh, gdbserver_cmd_start))
         gdbserver_cmd.append('fi')
-        GdbCrossConfigNone.write_file(self.gdbserver_script(gdbserver_mode), gdbserver_cmd, True)
+        GdbCrossConfigNone.write_file(self.server_script(server_mode), gdbserver_cmd, True)
 
-    def _gen_gdbinit_config(self, gdbserver_mode=None):
+    def _gen_gdbinit_config(self, server_mode=None):
         """Generate a gdbinit file for this binary and the corresponding gdbserver configuration"""
-        if gdbserver_mode is None:
-            gdbserver_mode = self.gdbserver_default_mode
+        if server_mode is None:
+            server_mode = self.default_mode
         gdbinit_lines = ['# This file is generated by devtool ide-sdk']
-        if gdbserver_mode == GdbServerModes.MULTI:
-            target_help = '#   gdbserver --multi :%d' % self.gdbserver_port
+        if server_mode == DebuggerServerModes.MULTI:
+            target_help = '#   gdbserver --multi :%d' % self.debug_server_port
             remote_cmd = 'target extended-remote'
         else:
             target_help = '#   gdbserver :%d %s' % (
-                self.gdbserver_port, self.binary)
+                self.debug_server_port, self.binary)
             remote_cmd = 'target remote'
         gdbinit_lines.append('# On the remote target:')
         gdbinit_lines.append(target_help)
         gdbinit_lines.append('# On the build machine:')
         gdbinit_lines.append('#   cd ' + self.modified_recipe.real_srctree)
         gdbinit_lines.append(
-            '#   ' + self.gdb_cross.gdb + ' -ix ' + self.gdbinit)
+            '#   ' + self.debugger_cross.gdb + ' -ix ' + self.gdbinit)
         gdbinit_lines.append('set sysroot ' + self.modified_recipe.d)
 
         if self.image_recipe.rootfs_dbg:
@@ -111,7 +111,7 @@  class GdbCrossConfigNone(GdbCrossConfig):
             gdbinit_lines.append("end" + os.linesep)
 
         gdbinit_lines.append(
-            '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port))
+            '%s %s:%d' % (remote_cmd, self.debugger_cross.host, self.debug_server_port))
         gdbinit_lines.append('set remote exec-file ' + self.binary.binary_path)
         gdbinit_lines.append('run ' + self.binary.binary_path)
 
@@ -121,14 +121,14 @@  class GdbCrossConfigNone(GdbCrossConfig):
         """Generate a script starting GDB with the corresponding gdbinit configuration."""
         cmd_lines = ['#!/bin/sh']
         cmd_lines.append('cd ' + self.modified_recipe.real_srctree)
-        cmd_lines.append(self.gdb_cross.gdb + ' -ix ' +
+        cmd_lines.append(self.debugger_cross.gdb + ' -ix ' +
                          self.gdbinit + ' "$@"')
         GdbCrossConfigNone.write_file(self.gdb_script, cmd_lines, True)
 
     def initialize(self):
         self._gen_gdbserver_start_script()
-        if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH:
-            self._gen_gdbserver_start_script(GdbServerModes.ATTACH)
+        if self.binary.runs_as_service and self.default_mode != DebuggerServerModes.ATTACH:
+            self._gen_gdbserver_start_script(DebuggerServerModes.ATTACH)
         self._gen_gdbinit_config()
         self._gen_gdb_start_script()
 
@@ -177,7 +177,7 @@  class IdeNone(IdeBase):
         script_path = modified_recipe.gen_install_deploy_script(args)
         logger.info("Created: %s" % script_path)
 
-        self.initialize_gdb_cross_configs(
+        self.initialize_cross_debug_configs(
             image_recipe, modified_recipe, GdbCrossConfigNone)
 
         IdeBase.gen_oe_scripts_sym_link(modified_recipe)
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 69b002b635..13fab50f22 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -130,7 +130,7 @@  class RecipeGdbCross(RecipeNative):
         gdb_path = os.path.join(
             self.staging_bindir_native, self.target_sys, gdb_bin)
         self.gdb = gdb_path
-        self.gdbserver_path = self.__find_gdbserver(config, tinfoil)
+        self.debug_server_path = self.__find_gdbserver(config, tinfoil)
 
     @property
     def host(self):
@@ -385,7 +385,7 @@  class RecipeModified:
     def __init__(self, name):
         self.name = name
         self.bootstrap_tasks = [name + ':do_install']
-        self.gdb_cross = None
+        self.debugger_cross = None
         # workspace
         self.real_srctree = None
         self.srctree = None
@@ -1169,21 +1169,25 @@  def ide_setup(args, config, basepath, workspace):
         if args.mode == DevtoolIdeMode.modified:
             logger.info("Setting up workspaces for modified recipe: %s" %
                         str(recipes_modified_names))
-            gdbs_cross = {}
+            debuggers = {}
             for recipe_name in recipes_modified_names:
                 recipe_modified = RecipeModified(recipe_name)
                 recipe_modified.initialize(config, workspace, tinfoil)
                 bootstrap_tasks += recipe_modified.bootstrap_tasks
                 recipes_modified.append(recipe_modified)
 
-                if recipe_modified.target_arch not in gdbs_cross:
+                # Key by (arch, toolchain) so recipes with different toolchains
+                # targeting the same arch each get the right debugger.
+                debugger_key = (recipe_modified.target_arch,
+                                recipe_modified.toolchain or '')
+                if debugger_key not in debuggers:
                     target_device = TargetDevice(args)
-                    gdb_cross = RecipeGdbCross(
+                    debugger = RecipeGdbCross(
                         args, recipe_modified.target_arch, target_device)
-                    gdb_cross.initialize(config, workspace, tinfoil)
-                    bootstrap_tasks += gdb_cross.bootstrap_tasks
-                    gdbs_cross[recipe_modified.target_arch] = gdb_cross
-                recipe_modified.gdb_cross = gdbs_cross[recipe_modified.target_arch]
+                    debugger.initialize(config, workspace, tinfoil)
+                    bootstrap_tasks += debugger.bootstrap_tasks
+                    debuggers[debugger_key] = debugger
+                recipe_modified.debugger_cross = debuggers[debugger_key]
 
     finally:
         tinfoil.shutdown()
@@ -1201,7 +1205,8 @@  def ide_setup(args, config, basepath, workspace):
                 config.init_path, basepath, bb_cmd_late, watch=True)
 
     wants_gdbserver = any(
-        r.wants_gdbserver for r in recipes_modified)
+        r.wants_gdbserver and r.toolchain == 'gcc'
+        for r in recipes_modified)
     for recipe_image in recipes_images:
         if wants_gdbserver and recipe_image.gdbserver_missing:
             logger.warning(