@@ -187,6 +187,72 @@ class GdbCrossConfig(DebuggerCrossConfig):
return "\"kill \\$(pgrep -o -f 'gdbserver --attach :%s') 2>/dev/null || true\"" % self.debug_server_port
+class LldbServerConfig(DebuggerCrossConfig):
+ """Configure lldb-server (platform mode) on the target for CodeLLDB remote debugging.
+
+ Unlike gdbserver, lldb-server platform mode is architecture-agnostic on the host
+ side: a single lldb-native binary handles all target architectures via the
+ LLDB platform protocol that CodeLLDB speaks natively.
+
+ The ATTACH mode is not supported because lldb-server platform does not take a
+ PID argument; attaching is done client-side via 'process attach'.
+ """
+
+ def __init__(self, image_recipe, modified_recipe, binary,
+ default_mode=DebuggerServerModes.MULTI):
+ super().__init__(image_recipe, modified_recipe, binary,
+ default_mode)
+
+ def _lldb_server_tmp_dir(self, mode):
+ return os.path.join('/tmp', 'lldb_server_%s' % self.id_pretty_mode(mode))
+
+ def _lldb_server_pid_file(self, mode):
+ return os.path.join(self._lldb_server_tmp_dir(mode), 'lldb_server.pid')
+
+ def _lldb_server_log_file(self, mode):
+ return os.path.join(self._lldb_server_tmp_dir(mode), 'lldb_server.log')
+
+ def _target_start_cmd(self, mode):
+ """SSH command to start lldb-server in platform mode on the target."""
+ lldb_server = self.debugger_cross.debug_server_path
+ # Use '*:<port>' so lldb-server binds on all interfaces (0.0.0.0), not
+ # just loopback. The bare ':<port>' form only binds to 127.0.0.1 in
+ # lldb-server 21.x and the remote lldb client connects from the host.
+ # Start from /tmp because lldb-server creates temp files in its cwd and
+ # the SSH default cwd (/home/root) may not exist on a minimal image.
+ if mode == DebuggerServerModes.ONCE:
+ cmd = "cd /tmp && %s platform --one-shot --server --listen *:%s" % (
+ lldb_server, self.debug_server_port)
+ elif mode == DebuggerServerModes.MULTI:
+ hex_port = "%04X" % self.debug_server_port
+ pid_file = self._lldb_server_pid_file(mode)
+ tmp_dir = self._lldb_server_tmp_dir(mode)
+ log_file = self._lldb_server_log_file(mode)
+ cmd = "grep -q :%s /proc/net/tcp /proc/net/tcp6 2>/dev/null && exit 0; " % hex_port
+ cmd += "mkdir -p %s; " % tmp_dir
+ cmd += "cd %s; " % tmp_dir
+ cmd += "%s platform --server --listen *:%s > %s 2>&1 & " % (
+ lldb_server, self.debug_server_port, log_file)
+ cmd += "echo \\$! > %s; " % pid_file
+ cmd += "_w=0; while ! grep -q :%s /proc/net/tcp /proc/net/tcp6 2>/dev/null; " % hex_port
+ cmd += "do _w=\\$((_w+1)); [ \\$_w -lt 100 ] || { echo lldb-server did not start on port %s >&2; exit 1; }; sleep 0.1; done;" % self.debug_server_port
+ else:
+ raise DevtoolError(
+ "lldb-server does not support mode %s "
+ "(ATTACH is handled client-side with 'process attach')" % mode)
+ return "\"/bin/sh -c '" + cmd + "'\""
+
+ def _target_kill_cmd(self):
+ """SSH command to stop a MULTI-mode lldb-server on the target."""
+ pid_file = self._lldb_server_pid_file(DebuggerServerModes.MULTI)
+ tmp_dir = self._lldb_server_tmp_dir(DebuggerServerModes.MULTI)
+ cmd = ("test -f %(pf)s && kill \\$(cat %(pf)s) 2>/dev/null; rm -rf %(td)s"
+ % {'pf': pid_file, 'td': tmp_dir})
+ return "\"/bin/sh -c '" + cmd + "'\""
+
+ def server_modes(self):
+ """ATTACH mode is not applicable for lldb-server platform."""
+ return [self.default_mode]
class IdeBase:
"""Base class defining the interface for IDE plugins"""
@@ -9,7 +9,7 @@ import json
import logging
import os
import shutil
-from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, DebuggerServerModes, get_devtool_deploy_opts
+from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, DebuggerServerModes, LldbServerConfig, get_devtool_deploy_opts
logger = logging.getLogger('devtool')
@@ -43,6 +43,28 @@ class GdbCrossConfigVSCode(GdbCrossConfig):
]
+class LldbServerConfigVSCode(LldbServerConfig):
+ """VSCode-specific lldb-server configuration for CodeLLDB remote debugging."""
+
+ def __init__(self, image_recipe, modified_recipe, binary,
+ default_mode=DebuggerServerModes.MULTI):
+ super().__init__(image_recipe, modified_recipe, binary,
+ default_mode)
+
+ def target_ssh_gdbserver_start_args(self, mode=None):
+ """SSH argument list to start lldb-server on the target"""
+ 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):
+ """SSH argument list to stop a running MULTI-mode lldb-server"""
+ return self._target_ssh_args() + [
+ self._target_kill_cmd()
+ ]
+
class IdeVSCode(IdeBase):
"""Manage IDE configurations for VSCode
@@ -284,6 +306,10 @@ class IdeVSCode(IdeBase):
"ms-vscode.cpptools-extension-pack",
"ms-vscode.cpptools-themes"
]
+ # For clang toolchain, CodeLLDB provides native LLDB debugging in VSCode
+ if (modified_recipe.toolchain == 'clang'
+ and modified_recipe.build_tool.is_c_cpp):
+ recommendations.append("vadimcn.vscode-lldb")
if modified_recipe.build_tool is BuildTool.CMAKE:
recommendations.append("ms-vscode.cmake-tools")
if modified_recipe.build_tool is BuildTool.MESON:
@@ -327,7 +353,9 @@ class IdeVSCode(IdeBase):
self.dot_code_dir(modified_recipe), prop_file, properties_dicts)
def vscode_launch_bin_dbg(self, cross_debug_config, server_mode):
- """Dispatch to the GDB launch config generator."""
+ """Dispatch to the GDB or LLDB launch config generator."""
+ if isinstance(cross_debug_config, LldbServerConfig):
+ return self._vscode_launch_bin_dbg_lldb(cross_debug_config, server_mode)
return self._vscode_launch_bin_dbg_gdb(cross_debug_config, server_mode)
def _vscode_launch_bin_dbg_gdb(self, cross_debug_config, server_mode):
@@ -414,6 +442,116 @@ class IdeVSCode(IdeBase):
return launch_config
+ def _vscode_launch_bin_dbg_lldb(self, lldb_config, server_mode):
+ """Generate a CodeLLDB (type: lldb) launch configuration entry for launch.json.
+
+ CodeLLDB connects to lldb-server via the LLDB platform protocol. The
+ initCommands select the remote platform and open the connection before
+ the process is launched, so CodeLLDB can inspect and control it.
+
+ Using targetCreateCommands instead of "program" so we can pass both the
+ local host binary (for debug symbols) and the remote target path (where
+ devtool deploy-target has already installed the binary) to
+ "target create --remote-file". This prevents LLDB from uploading the
+ binary from its module cache to a temporary directory and ensures the
+ process starts from its installed location where the dynamic linker can
+ find shared libraries via the standard search paths.
+ """
+ modified_recipe = lldb_config.modified_recipe
+ debugger_cross = modified_recipe.debugger_cross
+
+ init_commands = [
+ "platform select remote-linux",
+ "platform connect connect://%s:%d" % (debugger_cross.host, lldb_config.debug_server_port),
+ # Clear the default step-avoid-regexp so std:: and other library
+ # namespaces are not silently skipped on step-in. (default is "std::" in LLDB 15+)
+ "settings set target.process.thread.step-avoid-regexp \"\"",
+ ]
+ # Search for header files in recipe-sysroot (same as GDB sourceFileMap).
+ source_map = {
+ "/usr/include": os.path.join(modified_recipe.recipe_sysroot, "usr", "include")
+ }
+ if lldb_config.image_recipe.rootfs_dbg:
+ # Map build-time paths back to the workspace source tree.
+ for target_path, host_path in modified_recipe.reverse_debug_prefix_map.items():
+ if host_path.startswith(modified_recipe.real_srctree):
+ source_map[target_path] = (
+ "${workspaceFolder}"
+ + host_path[len(modified_recipe.real_srctree):])
+ else:
+ source_map[target_path] = host_path
+ if "/usr/src/debug" in source_map:
+ logger.error(
+ 'Key "/usr/src/debug" already exists in source_map. '
+ 'Something with DEBUG_PREFIX_MAP looks unexpected and finding '
+ 'sources in the rootfs-dbg will not work as expected.')
+ else:
+ source_map["/usr/src/debug"] = os.path.join(
+ lldb_config.image_recipe.rootfs_dbg, "usr", "src", "debug")
+
+ # Point LLDB at the .debug directories in rootfs-dbg.
+ debug_search_paths = " ".join(
+ modified_recipe.solib_search_path(lldb_config.image_recipe))
+ init_commands.append(
+ "settings set target.debug-file-search-paths %s" % debug_search_paths)
+
+ # Point LLDB at the unstripped binaries and shared libraries in ${D}
+ # so it can load debug symbols for the recipe's own shared libraries.
+ # These are the files deployed by devtool deploy-target.
+ exec_search_paths = " ".join([
+ os.path.join(modified_recipe.d, modified_recipe.libdir.lstrip('/')),
+ os.path.join(modified_recipe.d, modified_recipe.base_libdir.lstrip('/')),
+ os.path.join(modified_recipe.d, modified_recipe.bindir.lstrip('/')),
+ ])
+ # Deduplicate in case base_libdir == libdir or paths coincide
+ exec_search_paths = " ".join(dict.fromkeys(exec_search_paths.split()))
+ init_commands.append(
+ "settings set target.exec-search-paths %s" % exec_search_paths)
+ else:
+ logger.warning(
+ "Cannot setup debug symbols configuration for LLDB. "
+ "IMAGE_GEN_DEBUGFS is not enabled.")
+
+ # "target create --remote-file <target_path> <host_debug_binary>":
+ # --remote-file tells LLDB which path to execute on the target.
+ # The positional argument is the local host binary, loaded for symbols.
+ # This keeps devtool deploy-target as the sole deployment mechanism and
+ # avoids LLDB uploading the binary to a temporary directory via its
+ # module cache. Running from the installed path ensures the dynamic
+ # linker on the target can find shared libraries at their standard
+ # locations.
+ target_create_cmd = "target create --remote-file %s %s" % (
+ lldb_config.binary.binary_path,
+ lldb_config.binary.binary_host_path)
+
+ launch_config = {
+ "name": lldb_config.id_pretty_mode(server_mode),
+ "type": "lldb",
+ "request": "launch",
+ # Use targetCreateCommands instead of "program" to control both
+ # the local binary (for debug symbols) and the remote path.
+ "targetCreateCommands": [target_create_cmd],
+ "stopOnEntry": False,
+ "cwd": "/tmp",
+ "preLaunchTask": lldb_config.id_pretty_mode(server_mode),
+ "initCommands": init_commands,
+ }
+ if source_map:
+ launch_config["sourceMap"] = source_map
+ if modified_recipe.b:
+ # CodeLLDB resolves any source path that is still relative (as
+ # opposed to being rewritten to an absolute path by sourceMap /
+ # target.source-map) against "relativePathBase", defaulting to
+ # ${workspaceFolder}. Compilers are invoked with the build
+ # directory B as their working directory, so relative DW_AT_name
+ # entries (e.g. from meson/ninja) are relative to B. Pointing
+ # relativePathBase at B lets CodeLLDB resolve these directly,
+ # which matters in particular for devtool workspaces where S
+ # (and thus ${workspaceFolder}) is relocated outside of WORKDIR.
+ launch_config["relativePathBase"] = modified_recipe.b
+
+ return launch_config
+
def vscode_launch(self, args, modified_recipe):
"""GDB launch configurations for user-space binaries.
@@ -745,8 +883,12 @@ class IdeVSCode(IdeBase):
self.vscode_extensions(modified_recipe)
self.vscode_c_cpp_properties(modified_recipe)
if args.target:
- self.initialize_cross_debug_configs(
- image_recipe, modified_recipe, GdbCrossConfigVSCode)
+ if modified_recipe.toolchain == 'clang':
+ self.initialize_cross_debug_configs(
+ image_recipe, modified_recipe, LldbServerConfigVSCode)
+ else:
+ self.initialize_cross_debug_configs(
+ image_recipe, modified_recipe, GdbCrossConfigVSCode)
self.vscode_launch(args, modified_recipe)
self.vscode_tasks(args, modified_recipe)
@@ -137,6 +137,45 @@ class RecipeGdbCross(RecipeNative):
return self.target_device.host
+class RecipeLldbNative(RecipeNative):
+ """Handle lldb on the host and lldb-server on the target device.
+
+ Unlike GDB which requires a per-architecture gdb-cross-<arch> binary, LLDB
+ is architecture-agnostic: a single lldb-native installation can debug any
+ target architecture via the LLDB platform protocol.
+
+ On the target side, lldb-server (the ${PN}-server sub-package from the lldb
+ recipe) provides the platform server that CodeLLDB connects to.
+ """
+
+ def __init__(self, args, target_device):
+ super().__init__('lldb-native')
+ self.target_device = target_device
+ self._lldb = None
+ self._lldb_server_path = None
+
+ def __find_lldb_server(self, config, tinfoil):
+ """Absolute path of lldb-server on the target (from the lldb recipe)."""
+ recipe_d_lldb = parse_recipe(
+ config, tinfoil, 'lldb', appends=True, filter_workspace=False)
+ if not recipe_d_lldb:
+ raise DevtoolError("Parsing lldb recipe failed")
+ return os.path.join(recipe_d_lldb.getVar('bindir'), 'lldb-server')
+
+ def initialize(self, config, workspace, tinfoil):
+ super()._initialize(config, workspace, tinfoil)
+ self._lldb = os.path.join(self.staging_bindir_native, 'lldb')
+ self._lldb_server_path = self.__find_lldb_server(config, tinfoil)
+
+ @property
+ def debug_server_path(self):
+ return self._lldb_server_path
+
+ @property
+ def host(self):
+ return self.target_device.host
+
+
class RecipeImage:
"""Handle some image recipe related properties
@@ -169,8 +208,9 @@ class RecipeImage:
if image_d.getVar('IMAGE_GEN_DEBUGFS') == "1":
self.__rootfs_dbg = os.path.join(workdir, 'rootfs-dbg')
- self.gdbserver_missing = 'gdbserver' not in image_d.getVar(
- 'IMAGE_INSTALL') and 'tools-debug' not in image_d.getVar('IMAGE_FEATURES')
+ package_install = image_d.getVar('PACKAGE_INSTALL').split()
+ self.gdbserver_missing = 'gdbserver' not in package_install
+ self.lldb_server_missing = 'lldb-server' not in package_install
@property
def debug_support(self):
@@ -396,6 +436,7 @@ class RecipeModified:
self.b = None
self.base_libdir = None
self.bblayers = None
+ self.bindir = None
self.bitbakepath = None
self.bpn = None
self.d = None
@@ -476,6 +517,7 @@ class RecipeModified:
self.b = recipe_d.getVar('B')
self.base_libdir = recipe_d.getVar('base_libdir')
self.bblayers = recipe_d.getVar('BBLAYERS').split()
+ self.bindir = recipe_d.getVar('bindir')
self.bitbakepath = recipe_d.getVar('BITBAKEPATH')
self.bpn = recipe_d.getVar('BPN')
self.cc = recipe_d.getVar('CC')
@@ -669,8 +711,106 @@ class RecipeModified:
if unused_host_paths:
logger.info("Some source directories mapped by -fdebug-prefix-map are not included in the debugger search paths. Ignored host paths: %s", unused_host_paths)
+ self._add_broken_srctree_prefix_map(mappings)
+
return mappings
+ def _add_broken_srctree_prefix_map(self, mappings):
+ """Work around a -f*-prefix-map / DWARF path resolution issue affecting
+ out-of-tree devtool workspaces (e.g. meson recipes built via 'devtool modify'
+ with the clang toolchain).
+
+ meson/ninja may invoke the compiler with a *relative* source file path
+ when the build directory B (under WORKDIR) and the source directory S
+ (relocated outside WORKDIR by 'devtool modify') only share a distant
+ common ancestor. -fdebug-prefix-map/-ffile-prefix-map only rewrite
+ paths that literally start with the mapped host prefix, so a relative
+ path argument is never rewritten: only DW_AT_comp_dir (which is
+ absolute) gets rewritten, DW_AT_name stays relative and unrewritten.
+
+ This has only been observed to actually happen with the clang
+ toolchain: clang's meson/ninja invocation embeds a relative DW_AT_name
+ for out-of-tree sources, while gcc, even via meson/ninja, embeds an
+ absolute (and correctly -fdebug-prefix-map-rewritten) DW_AT_name, so
+ no underflow can happen there - confirmed empirically:
+ oe-selftest's test_devtool_ide_sdk_none_qemu (gcc toolchain, covering
+ both cmake-example and meson-example) fails when this workaround is
+ applied unconditionally to meson, while the dedicated clang tests
+ (test_devtool_ide_sdk_{code,none}_meson_clang) require it. cmake
+ (with the Ninja or Makefiles generators used here) always passes
+ absolute source paths to the compiler regardless of toolchain, so it
+ never needs this workaround either. Applying this workaround outside
+ of the meson+clang combination would incorrectly discard the correct
+ (and, for gcc/cmake, already working) comp_dir-based mapping - see the
+ 'del mappings[target_path]' below - falling back to the generic
+ '/usr/src/debug' mapping to the image's (stale, whole-image-build-time)
+ rootfs-dbg instead of the live source tree.
+
+ Debuggers resolve the compile unit path by joining DW_AT_comp_dir with
+ the relative DW_AT_name, popping one path component per leading "..".
+ If DW_AT_name contains more ".." components than DW_AT_comp_dir has
+ path components, the extra ".." are no-ops once the root is reached
+ (they can't go above "/"), so the final resolved path becomes "/"
+ followed by the leftover (non-"..") components of DW_AT_name - i.e. a
+ suffix of the real, absolute source directory rather than the
+ "/usr/src/debug/<pn>/<pv>" prefix that DEBUG_PREFIX_MAP and the
+ generated sourceMap/sourceFileMap assume.
+
+ This computes that resolved suffix for the recipe's own source
+ directory (S) and replaces the (now dead, since every file under S is
+ affected the same way) comp_dir-based mapping with it, so debuggers
+ relying on prefix matching (e.g. CodeLLDB, GDB) can still locate the
+ sources.
+
+ Note: the original comp_dir-based target_path is removed rather than
+ kept alongside the new one. Keeping both would mean two different
+ target paths map to the same host path (S), which is ambiguous when a
+ debugger needs to go the other way round: translating a local file
+ (opened from the host/workspace) back into a debug-info path in order
+ to resolve a source breakpoint. CodeLLDB in particular appears to
+ pick the first-registered ("normal", comp_dir-based) mapping in that
+ case, which never matches any real compile unit here, leaving the
+ breakpoint pending with 0 locations.
+ """
+ if self.build_tool is not BuildTool.MESON or self.toolchain != "clang":
+ return
+ if not self.real_srctree or not self.b:
+ return
+
+ b_real = os.path.realpath(self.b)
+ srctree_real = os.path.realpath(self.real_srctree)
+ common = os.path.commonpath([b_real, srctree_real])
+ if common in (b_real, srctree_real):
+ # B is srctree (or a parent of it), or B is nested inside srctree:
+ # either way the compiler is never invoked with a source path that
+ # climbs above the common ancestor, so no underflow can happen.
+ return
+
+ # Number of ".." path components needed to get from the compiler's
+ # working directory (the build directory B) up to the common ancestor
+ # with the source tree. This is how many leading ".." components
+ # DW_AT_name would contain for sources directly under S.
+ overshoot_components = len(os.path.relpath(b_real, common).split(os.sep))
+
+ for target_path, host_path in list(mappings.items()):
+ if host_path != srctree_real:
+ # Only the recipe's own source directory (S) is relocated by
+ # devtool modify, other mapped directories are unaffected.
+ continue
+ comp_dir_components = len([c for c in target_path.split('/') if c])
+ if overshoot_components <= comp_dir_components:
+ # The rewritten DW_AT_comp_dir has enough components to
+ # absorb all the ".." in DW_AT_name, no underflow happens.
+ continue
+ broken_target = '/' + os.path.relpath(srctree_real, common)
+ if broken_target not in mappings:
+ mappings[broken_target] = host_path
+ # The comp_dir-based target_path never actually occurs in the
+ # debug info for files under S (all of them hit the same
+ # overshoot), so keeping it around only creates an ambiguous
+ # reverse mapping (see docstring above). Drop it.
+ del mappings[target_path]
+
@property
def gdb_pretty_print_scripts(self):
if self._gdb_pretty_print_scripts is None:
@@ -1182,8 +1322,11 @@ def ide_setup(args, config, basepath, workspace):
recipe_modified.toolchain or '')
if debugger_key not in debuggers:
target_device = TargetDevice(args)
- debugger = RecipeGdbCross(
- args, recipe_modified.target_arch, target_device)
+ if recipe_modified.toolchain == 'clang':
+ debugger = RecipeLldbNative(args, target_device)
+ else:
+ debugger = RecipeGdbCross(
+ args, recipe_modified.target_arch, target_device)
debugger.initialize(config, workspace, tinfoil)
bootstrap_tasks += debugger.bootstrap_tasks
debuggers[debugger_key] = debugger
@@ -1207,12 +1350,20 @@ def ide_setup(args, config, basepath, workspace):
wants_gdbserver = any(
r.wants_gdbserver and r.toolchain == 'gcc'
for r in recipes_modified)
+ wants_lldb_server = any(
+ r.wants_gdbserver and r.toolchain == 'clang'
+ for r in recipes_modified)
for recipe_image in recipes_images:
if wants_gdbserver and recipe_image.gdbserver_missing:
logger.warning(
"gdbserver not installed in image %s. Remote debugging will not be available" % recipe_image)
+ if wants_lldb_server and recipe_image.lldb_server_missing:
+ logger.warning(
+ "lldb-server not installed in image %s. "
+ "Remote debugging with LLDB (CodeLLDB) will not be available. "
+ "Add 'lldb-server' to IMAGE_INSTALL." % recipe_image)
- if wants_gdbserver and recipe_image.combine_dbg_image is False:
+ if (wants_gdbserver or wants_lldb_server) and recipe_image.combine_dbg_image is False:
logger.warning(
'IMAGE_CLASSES += "image-combined-dbg" is missing for image %s. Remote debugging will not find debug symbols from rootfs-dbg.' % recipe_image)