@@ -65,6 +65,10 @@ class DebuggerCrossConfig:
DebuggerCrossConfig._port_next += 1
self.debug_server_port = self.debug_server_ports[self.default_mode]
self.id_pretty = "%d_%s" % (self.debug_server_port, self.binary_pretty)
+ # Hook for subclasses needing additional fixed ports forwarded through
+ # slirp beyond the one-per-mode debug_server_ports (e.g. lldb-server's
+ # spawned gdbserver instances).
+ self.extra_ports = []
if self.id_pretty in DebuggerCrossConfig._configs:
raise DevtoolError(
@@ -269,6 +273,13 @@ class LldbServerConfig(DebuggerCrossConfig):
default_mode=DebuggerServerModes.MULTI):
super().__init__(image_recipe, modified_recipe, binary,
default_mode)
+ # lldb-server platform spawns a separate gdb-remote-protocol
+ # "gdbserver" instance per debug session; without --gdbserver-port it
+ # picks a random port, which cannot be forwarded through slirp NAT.
+ # Pin it to a fixed, dedicated port that gets slirp-forwarded too.
+ self.gdbserver_port = DebuggerCrossConfig._port_next
+ DebuggerCrossConfig._port_next += 1
+ self.extra_ports.append(self.gdbserver_port)
def _lldb_server_tmp_dir(self, mode):
return os.path.join('/tmp', 'lldb_server_%s' % self.id_pretty_mode(mode))
@@ -294,8 +305,8 @@ class LldbServerConfig(DebuggerCrossConfig):
cmd = self._target_tcp_port_check_cmd() + " && exit 0; "
cmd += "mkdir -p %s; " % tmp_dir
cmd += "cd %s; " % tmp_dir
- cmd += "%s platform --server --listen *:%s > %s 2>&1 & _lldb_server_pid=\\$!; " % (
- lldb_server, self.debug_server_port, log_file)
+ cmd += "%s platform --server --listen *:%s --gdbserver-port %s > %s 2>&1 & _lldb_server_pid=\\$!; " % (
+ lldb_server, self.debug_server_port, self.gdbserver_port, log_file)
cmd += "echo \\$_lldb_server_pid > %s; " % pid_file
cmd += self._target_wait_for_tcp_port_cmd(
"lldb_server_pid", log_file)
@@ -207,10 +207,12 @@ class LldbServerConfigNone(LldbServerConfig):
lines = ['# This file is generated by devtool ide-sdk']
if server_mode == DebuggerServerModes.MULTI:
lines.append('# On the remote target:')
- lines.append('# lldb-server platform --server --listen *:%d' % self.debug_server_port)
+ lines.append('# lldb-server platform --server --listen *:%d --gdbserver-port %d' % (
+ self.debug_server_port, self.gdbserver_port))
else:
lines.append('# On the remote target:')
- lines.append('# lldb-server platform --one-shot --server --listen *:%d' % self.debug_server_port)
+ lines.append('# lldb-server platform --one-shot --server --listen *:%d --gdbserver-port %d' % (
+ self.debug_server_port, self.gdbserver_port))
lines.append('# On the build machine:')
lines.append('# cd ' + self.modified_recipe.real_srctree)
lines.append('# ' + self.debugger_cross.lldb + ' -s ' + self.lldbinit)
@@ -23,7 +23,7 @@ import scriptutils
import bb
from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError, parse_recipe
from devtool.standard import get_real_srctree
-from devtool.ide_plugins import BuildTool
+from devtool.ide_plugins import BuildTool, DebuggerCrossConfig
from oe.kernel_module import kernel_module_os_env
@@ -186,15 +186,17 @@ class RecipeImage:
"""
MARKER = '# devtool ide-sdk: image debug settings'
+ QB_SLIRP_MARKER = '# devtool ide-sdk: QB_SLIRP_OPT'
def __init__(self, name, orig_bbappend_content=None):
self.name = name
self.rootfs = None
self.__rootfs_dbg = None
+ self.qb_slirp_opt = ''
self.bootstrap_tasks = [self.name + ':do_build']
# Debug settings already provided by the base configuration (e.g.
# local.conf, MACHINE, DISTRO, the recipe itself) plus any bbappend
- # content other than devtool ide-sdk's own section (see
+ # content other than devtool ide-sdk's own sections (see
# strip_bbappend_sections()). Populated by initialize().
self.base_image_gen_debugfs = False
self.base_image_fstypes_debugfs = ''
@@ -204,11 +206,11 @@ class RecipeImage:
# Content of the bbappend before strip_bbappend_sections() ran.
self._orig_bbappend_content = orig_bbappend_content
- @classmethod
- def _strip_marker_section(cls, content):
- """Remove devtool ide-sdk's own image debug settings section, if any"""
+ @staticmethod
+ def _strip_marker_section(content, marker):
+ """Remove one devtool ide-sdk marker section, if present"""
return re.sub(
- r'^' + re.escape(cls.MARKER) + r'\n(?:[^\n]+\n)*',
+ r'^' + re.escape(marker) + r'\n(?:[^\n]+\n)*',
'', content, flags=re.MULTILINE)
@classmethod
@@ -227,7 +229,8 @@ class RecipeImage:
with open(bbappend, 'r') as f:
content = f.read()
originals[name] = content
- stripped = cls._strip_marker_section(content)
+ stripped = cls._strip_marker_section(content, cls.MARKER)
+ stripped = cls._strip_marker_section(stripped, cls.QB_SLIRP_MARKER)
if stripped != content:
with open(bbappend, 'w') as f:
f.write(stripped)
@@ -259,6 +262,8 @@ class RecipeImage:
self.rootfs = os.path.join(workdir, 'rootfs')
self.__rootfs_dbg = os.path.join(workdir, 'rootfs-dbg')
+ self.qb_slirp_opt = image_d.getVar('QB_SLIRP_OPT') or ''
+
@property
def debug_support(self):
return bool(self.rootfs_dbg)
@@ -279,9 +284,10 @@ class RecipeImage:
initialize() already stripped this section from the bbappend on
disk before parsing, so it only needs to be added back here, if
- still needed. Returns True if the resulting bbappend content
- actually differs from what was on disk when initialize() ran, False
- if it is left exactly as it was.
+ still needed. Also updates QB_SLIRP_OPT with the debugger server
+ port forwards (see update_qb_slirp_opt()). Returns True if the
+ resulting bbappend content actually differs from what was on disk
+ when initialize() ran, False if it is left exactly as it was.
"""
wants_gdbserver = any(
r.wants_gdbserver and r.toolchain != 'clang'
@@ -319,24 +325,90 @@ class RecipeImage:
parsed_content = ''
if not lines:
- # The base configuration already provides everything needed.
- if parsed_content != original_content:
+ if self.MARKER in original_content:
logger.info(
"Removed image debug settings from %s: already provided by the base configuration", self._bbappend)
+ image_changed = False
+ else:
+ new_section = self.MARKER + '\n' + '\n'.join(lines) + '\n'
+ new_content = parsed_content
+ if new_content and not new_content.endswith('\n'):
+ new_content += '\n'
+ new_content += new_section
+
+ appends_dir = os.path.dirname(self._bbappend)
+ os.makedirs(appends_dir, exist_ok=True)
+ with open(self._bbappend, 'w') as f:
+ f.write(new_content)
+ logger.info("Updated image bbappend %s", self._bbappend)
+ image_changed = True
+
+ slirp_changed = self.update_qb_slirp_opt()
+ return image_changed or slirp_changed
+
+ def update_qb_slirp_opt(self):
+ """Update QB_SLIRP_OPT in the image bbappend
+
+ Support connecting to a debugger server running on the target device via
+ runqemu's slirp network:
+ - If the base value is non-empty (recipe/machine sets QB_SLIRP_OPT):
+ only missing port forwards are appended via QB_SLIRP_OPT:append.
+ - If the base value is empty (runqemu would use its own built-in default
+ of SSH 2222, telnet 2323, tftp): a full QB_SLIRP_OPT assignment is
+ written that mirrors that default plus the debugger ports, so that
+ runqemu reads the complete set from the .qemuboot.conf.
+
+ Returns True if the bbappend content actually changed, False otherwise.
+ """
+ ports = sorted({port for cfg in DebuggerCrossConfig._configs.values()
+ for port in list(cfg.debug_server_ports.values()) + cfg.extra_ports})
+ if not ports:
return False
- new_section = self.MARKER + '\n' + '\n'.join(lines) + '\n'
- new_content = parsed_content
+ # Determine which ports are already in the base value
+ already = {int(m.group(1))
+ for m in re.finditer(r':(\d+)-:\d+', self.qb_slirp_opt)}
+ missing_ports = [p for p in ports if p not in already]
+ if not missing_ports:
+ logger.info("QB_SLIRP_OPT already contains all needed port forwards")
+ return False
+
+ if self.qb_slirp_opt:
+ # Base value exists: :append only the missing port forwards
+ extra = ''.join(
+ ',hostfwd=tcp:127.0.0.1:%d-:%d' % (p, p) for p in missing_ports)
+ new_line = 'QB_SLIRP_OPT:append = "%s"' % extra
+ else:
+ # No base value: mirror runqemu's built-in default (SSH 2222, telnet
+ # 2323, tftp) and add the debugger ports.
+ all_hostfwds = (
+ 'hostfwd=tcp:127.0.0.1:2222-:22,'
+ 'hostfwd=tcp:127.0.0.1:2323-:23'
+ )
+ all_hostfwds += ''.join(
+ ',hostfwd=tcp:127.0.0.1:%d-:%d' % (p, p) for p in missing_ports)
+ new_line = 'QB_SLIRP_OPT = "-netdev user,id=net0,%s,tftp=${DEPLOY_DIR_IMAGE}"' % all_hostfwds
+
+ if os.path.exists(self._bbappend):
+ with open(self._bbappend, 'r') as f:
+ content = f.read()
+ else:
+ content = ''
+ stripped_content = self._strip_marker_section(content, self.QB_SLIRP_MARKER)
+ new_content = stripped_content
if new_content and not new_content.endswith('\n'):
new_content += '\n'
- new_content += new_section
+ new_content += self.QB_SLIRP_MARKER + '\n' + new_line + '\n'
+
+ if new_content == content:
+ logger.debug("QB_SLIRP_OPT in %s is already up to date", self._bbappend)
+ return False
appends_dir = os.path.dirname(self._bbappend)
os.makedirs(appends_dir, exist_ok=True)
with open(self._bbappend, 'w') as f:
f.write(new_content)
-
- logger.info("Updated image bbappend %s", self._bbappend)
+ logger.info("Updated QB_SLIRP_OPT in %s: %s", self._bbappend, new_line)
return True
@@ -1359,9 +1431,9 @@ def ide_setup(args, config, basepath, workspace):
# Collect information about tasks which need to be bitbaked.
# In modified mode the image build is held back until after
# setup_modified_recipe() has assigned the debugger port numbers and
- # update_image_bbappend() has written the complete bbappend. That way
- # the image is built with a single, stable recipe hash so that no
- # basehash-changed warnings are emitted.
+ # update_image_bbappend() has written the complete bbappend (including
+ # QB_SLIRP_OPT). That way the image is built with a single, stable
+ # recipe hash so that no basehash-changed warnings are emitted.
bootstrap_tasks = []
bootstrap_tasks_late = []
image_bootstrap_tasks = []
@@ -1442,8 +1514,8 @@ def ide_setup(args, config, basepath, workspace):
recipe_image.initialize(config, tinfoil)
if args.mode == DevtoolIdeMode.modified:
# Keep the image build separate so that the complete bbappend
- # can be written in one step before the image is built,
- # avoiding sstate hash mismatches.
+ # (IMAGE_ vars + QB_SLIRP_OPT) can be written in one step
+ # before the image is built, avoiding sstate hash mismatches.
image_bootstrap_tasks += recipe_image.bootstrap_tasks
else:
bootstrap_tasks += recipe_image.bootstrap_tasks
@@ -1536,12 +1608,13 @@ def ide_setup(args, config, basepath, workspace):
'Note that devtool modify --debug-build can do this automatically.',
recipe_modified.name, recipe_modified.bbappend)
- # Ports are now assigned. Write the complete image bbappend in a
- # single step so that the image is built with exactly one recipe
- # hash. This avoids the sstate basehash-changed warnings that
- # arise when the bbappend is modified after the image has
- # already been built. This also runs with --skip-bitbake, otherwise
- # the section removed by strip_bbappend_sections() would be lost.
+ # Ports are now assigned. Write the complete image bbappend --
+ # IMAGE_ debug settings and QB_SLIRP_OPT -- in a single step so
+ # that the image is built with exactly one recipe hash. This
+ # avoids the sstate basehash-changed warnings that arise when
+ # the bbappend is modified after the image has already been
+ # built. This also runs with --skip-bitbake, otherwise the sections
+ # removed by strip_bbappend_sections() would be lost.
bbappend_changed = False
for ri in recipes_images:
if ri.update_image_bbappend(recipes_modified):
@@ -1564,7 +1637,9 @@ def ide_setup(args, config, basepath, workspace):
finally:
reparse_tinfoil.shutdown()
- # Phase 2: build the image
+ # Phase 2: build the image. do_image -> do_write_qemuboot_conf
+ # picks up QB_SLIRP_OPT from the bbappend written above, so no
+ # separate write_qemuboot_conf step is needed.
exec_build_env_command(
config.init_path, basepath,
bb_cmd + ' '.join(image_bootstrap_tasks), watch=True)