From patchwork Sun Aug 2 19:52:48 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94250 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 50B50C55822 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-64-225.siemens.flowmailer.net (mta-64-225.siemens.flowmailer.net [185.136.64.225]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29326.1785700422013768987 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=SRpF8+6m; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.225, mailfrom: fm-1329275-20260802195338239a389133000207c0-_l408l@rts-flowmailer.siemens.com) Received: by mta-64-225.siemens.flowmailer.net with ESMTPSA id 20260802195338239a389133000207c0 for ; Sun, 02 Aug 2026 21:53:39 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=UHh2HNYQjhFHHJq7QwwouJ4Ed/KBTUt+6sL2o91Y2vs=; b=SRpF8+6msAHjKAWpHHhAjFNEcgHzufQiRyxnb6mpmWvtXnKX0qYfvW7YRGBYlp5YX66mVi AqgHfDLEr5cAtlPQ0SokimquOFKlanVnGx+LYczoaI+CKDqwPMLt1Yi2Pi8ppbO+adQbwPU4 J54xYD/LJgSbESx6HsPbwCUMyHphZ9Ls6OkzdzR05JEvxCtYrOf4omQure5NOZm3MvKmjAQN pFIFvtPj/6Adi2KIisgVGdsM1F3kyUIMT9Wo+14YlDoti/YIU0oEsMGrXU8te19AsohjWZdZ BrHlhTFYO2wCNpp6BKY5IU34dUzCbg4ppgqOBcyEbL8RMz7LFz695mlA==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 01/14] qemuboot: make do_write_qemuboot_conf a standalone sstate task Date: Sun, 2 Aug 2026 21:52:48 +0200 Message-ID: <20260802195324.64533-2-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242576 From: Adrian Freihofer There seems to be still some corner cases where the qemuboot.conf file is not deployed reliably. Previously do_write_qemuboot_conf wrote its output into IMGDEPLOYDIR, which is shared with do_image_complete. The conf file only reached DEPLOY_DIR_IMAGE when do_image_complete ran, so for example running: bitbake -c write_qemuboot_conf --force had no visible effect on the deployed conf. Introduce a dedicated QEMUBOOTCONFDEPLOYDIR and register do_write_qemuboot_conf as a proper SSTATE task with its own sstate-inputdirs/sstate-outputdirs pair pointing at DEPLOY_DIR_IMAGE. A matching do_write_qemuboot_conf_setscene task handles sstate restores. Signed-off-by: Adrian Freihofer --- meta/classes-recipe/qemuboot.bbclass | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/meta/classes-recipe/qemuboot.bbclass b/meta/classes-recipe/qemuboot.bbclass index d1794c7899..1ced9b9b73 100644 --- a/meta/classes-recipe/qemuboot.bbclass +++ b/meta/classes-recipe/qemuboot.bbclass @@ -126,6 +126,11 @@ QB_DRIVE_TYPE ?= "/dev/sd" inherit image-artifact-names +# Dedicated deploy directory for the qemuboot config, separate from IMGDEPLOYDIR +# so that do_write_qemuboot_conf can be its own sstate task and deploy its +# output to DEPLOY_DIR_IMAGE independently of do_image_complete. +QEMUBOOTCONFDEPLOYDIR = "${WORKDIR}/deploy-${PN}-qemubootconf" + # Create qemuboot.conf addtask do_write_qemuboot_conf after do_rootfs before do_image do_write_qemuboot_conf[depends] += "${@ '' if bb.utils.to_boolean(d.getVar('INITRAMFS_IMAGE_BUNDLE')) else (d.getVar('KERNEL_DEPLOY_DEPEND') or '')}" @@ -140,12 +145,22 @@ def qemuboot_vars(d): do_write_qemuboot_conf[vardeps] += "${@' '.join(qemuboot_vars(d))}" do_write_qemuboot_conf[vardepsexclude] += "TOPDIR" +do_write_qemuboot_conf[dirs] = "${QEMUBOOTCONFDEPLOYDIR}" +do_write_qemuboot_conf[cleandirs] = "${QEMUBOOTCONFDEPLOYDIR}" +do_write_qemuboot_conf[stamp-extra-info] = "${MACHINE_ARCH}" +SSTATETASKS += "do_write_qemuboot_conf" +do_write_qemuboot_conf[sstate-inputdirs] = "${QEMUBOOTCONFDEPLOYDIR}" +do_write_qemuboot_conf[sstate-outputdirs] = "${DEPLOY_DIR_IMAGE}" +python do_write_qemuboot_conf_setscene () { + sstate_setscene(d) +} +addtask do_write_qemuboot_conf_setscene python do_write_qemuboot_conf() { import configparser - qemuboot = "%s/%s.qemuboot.conf" % (d.getVar('IMGDEPLOYDIR'), d.getVar('IMAGE_NAME')) + qemuboot = "%s/%s.qemuboot.conf" % (d.getVar('QEMUBOOTCONFDEPLOYDIR'), d.getVar('IMAGE_NAME')) if d.getVar('IMAGE_LINK_NAME'): - qemuboot_link = "%s/%s.qemuboot.conf" % (d.getVar('IMGDEPLOYDIR'), d.getVar('IMAGE_LINK_NAME')) + qemuboot_link = "%s/%s.qemuboot.conf" % (d.getVar('QEMUBOOTCONFDEPLOYDIR'), d.getVar('IMAGE_LINK_NAME')) else: qemuboot_link = "" finalpath = d.getVar("DEPLOY_DIR_IMAGE") From patchwork Sun Aug 2 19:52:49 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94245 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id AC632C55189 for ; Sun, 2 Aug 2026 19:53:54 +0000 (UTC) Received: from mta-64-227.siemens.flowmailer.net (mta-64-227.siemens.flowmailer.net [185.136.64.227]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29485.1785700422013920714 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=DQAE/LkG; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.227, mailfrom: fm-1329275-20260802195339e1210d67b6000207e8-k5c0wy@rts-flowmailer.siemens.com) Received: by mta-64-227.siemens.flowmailer.net with ESMTPSA id 20260802195339e1210d67b6000207e8 for ; Sun, 02 Aug 2026 21:53:39 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=qzTxp9f6vQ4x6cKLkZp7XUvdGJbpQJ6w1Fchk8y8fr0=; b=DQAE/LkGt+1uDKPNMsjzhYOh/Kd6Vu+MYpMr0ooOyCxfQXWU9BE+cQg9WP6YluOOhaoJ9u 5YpMPz0LCcw8VNK7Zz6pdZpI92WDemD/NUK0t5PBC5cjRFcjHm/08qRSSxupuV257MFXHIt/ EIwjfap5+2kOtchASCAns/TJpmHCGRQriuYFhc8fy9RyXzlLuIhSZ3qcy2sYvHKYkVZ/cExJ Of3FycHp0KHFDv93znFrx+rIKsM1oq3zdj60VQQCObOVhnjwGLQdYcJ/OYmJYE3zHwdJCJRO LcLjwwxnwPAMjob7S/dB0TSRwZ4SGf9SiqW6IYH8SuJfJl8nzEkznu3w==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 02/14] oe-selftest: devtool: use stat for reading user/group names in ide-sdk tests Date: Sun, 2 Aug 2026 21:52:49 +0200 Message-ID: <20260802195324.64533-3-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:54 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242582 From: Adrian Freihofer On some systems, ls truncates long user and group names, which causes the ownership check to fail. For example: AssertionError: Regex didn't match: '^-.+ cmake-example cmake-example .+ /etc/cmake\\-example\\.conf$' not found in '-rw-r--r-- 1 cmake-ex cmake-ex 83 Mar 9 2018 /etc/cmake-example.conf' Use "stat -c '%U %G'" instead, which always returns the full user and group names regardless of terminal width or system configuration. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index b138a5ef6f..2e91c2be97 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -3118,11 +3118,14 @@ class DevtoolIdeSdkTests(DevtoolBase): def _verify_conf_file(self, qemu, conf_file, owner, group): """Helper to verify a configuration file is owned by the proper user and group""" - ls_cmd = "ls -l %s" % conf_file - status, output = qemu.run(ls_cmd) - self.assertEqual(status, 0, msg="Failed to ls %s: %s" % (conf_file, output)) - self.assertRegex(output, rf"^-.+ {owner} {group} .+ {re.escape(conf_file)}$", - msg="%s not owned by %s:%s: %s" % (conf_file, owner, group, output)) + stat_cmd = "stat -c '%%U %%G' %s" % conf_file + status, output = qemu.run(stat_cmd) + self.assertEqual(status, 0, msg="Failed to stat %s: %s" % (conf_file, output)) + actual_owner, actual_group = output.strip().split() + self.assertEqual(actual_owner, owner, + msg="%s not owned by user %s: got %s" % (conf_file, owner, actual_owner)) + self.assertEqual(actual_group, group, + msg="%s not owned by group %s: got %s" % (conf_file, group, actual_group)) @OETestTag("runqemu") def test_devtool_ide_sdk_none_qemu(self): From patchwork Sun Aug 2 19:52:50 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94249 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 21116C55190 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-65-227.siemens.flowmailer.net (mta-65-227.siemens.flowmailer.net [185.136.65.227]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29327.1785700422013885819 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=axO7UkYF; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.227, mailfrom: fm-1329275-20260802195339fb59943d4f00020741-ws4qbl@rts-flowmailer.siemens.com) Received: by mta-65-227.siemens.flowmailer.net with ESMTPSA id 20260802195339fb59943d4f00020741 for ; Sun, 02 Aug 2026 21:53:39 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=BsOteJ3HJweHVmGwEU7qEgWEKNRPiDxm3vaPLTAwHoI=; b=axO7UkYFn/9d5Bo9QDI0qkkKsydZ2oWtNHgAQjG6i0xHB2nlRMzlCc8clVw/G2Ffhzabri MVHQvP/VOJbfz8w9sXczppTcO8b7IwMuIE7vtxyGx2BlC9xf1WwMlcSlIG5cV9Lbc0Z+dbQF oYeIAF4QKJKjpv5b46pbut/Reb1Bp7HT/2b3zSIAgkfTODw98nrGCqvvHFi73feiaKc5ISuw Xl52D9w3NebDTsnc3E3WnpDbQOwTxOqHJYFJLDVS5KHhWDqsvV4vywCV5u94/II0TBnvDClL T8QlAO4qbkyWgVtxU2wtY2otl3iTDp3LPEBUppIL+evhWayrRclMS0NA==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 03/14] devtool: ide-sdk: fix duplicate -p flag in _target_ssh_args Date: Sun, 2 Aug 2026 21:52:50 +0200 Message-ID: <20260802195324.64533-4-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242578 From: Adrian Freihofer ssh_port is stored as ['-p', ''] (already includes the flag). _target_ssh_args was wrapping it in another list with a leading "-p", producing invalid ssh args like ["-p", ["-p", "2222"]]. Extend ssh_args directly with ssh_port instead of nesting it. Also pass --port to the install-and-deploy task in the generated VS Code configuration so the task reaches the target on non-default SSH ports. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/__init__.py | 2 +- scripts/lib/devtool/ide_plugins/ide_code.py | 10 ++-------- scripts/lib/devtool/ide_sdk.py | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index eaf88e78cd..94ed7c9e6d 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -142,7 +142,7 @@ class GdbCrossConfig: def _target_ssh_gdbserver_args(self): ssh_args = [] if self.gdb_cross.target_device.ssh_port: - ssh_args += ["-p", 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: diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 603d3cecf3..96f007ca91 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -404,10 +404,7 @@ class IdeVSCode(IdeBase): "label": install_task_name, "type": "shell", "command": run_install_deploy, - "args": [ - "--target", - args.target - ], + "args": ["--target", args.target] + (["--port", args.port] if args.port else []), "problemMatcher": [] } ] @@ -515,10 +512,7 @@ class IdeVSCode(IdeBase): "label": install_task_name, "type": "shell", "command": run_install_deploy, - "args": [ - "--target", - args.target - ], + "args": ["--target", args.target] + (["--port", args.port] if args.port else []), "problemMatcher": [] }, { diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 07f5552758..fea44740f3 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -55,7 +55,7 @@ class TargetDevice: self.ssh_sshexec = 'ssh' if args.ssh_exec: self.ssh_sshexec = args.ssh_exec - self.ssh_port = '' + self.ssh_port = [] if args.port: self.ssh_port = ['-p', args.port] if args.key: From patchwork Sun Aug 2 19:52:51 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94246 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id C4F99C5518E for ; Sun, 2 Aug 2026 19:53:54 +0000 (UTC) Received: from mta-64-228.siemens.flowmailer.net (mta-64-228.siemens.flowmailer.net [185.136.64.228]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29484.1785700422013406576 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=GjzihNuO; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.228, mailfrom: fm-1329275-202608021953399381e2695e00020774-isd5_p@rts-flowmailer.siemens.com) Received: by mta-64-228.siemens.flowmailer.net with ESMTPSA id 202608021953399381e2695e00020774 for ; Sun, 02 Aug 2026 21:53:39 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=B4kve12KlGad3gutgddg+RgxW8oUA+Jh2WhOZlpBjhQ=; b=GjzihNuOK5de2qQuhyBqpRfJETU7VXwfLVDQkvHnid26n+FshlSkjrh7DnxP8H5TfGf3PJ ihWVB3AoqIhda92Z6FtOn7bYUnm2/qdD4Qyb3maO9sg0cHYQCqIeWDc8oaIj9P1W4j3SdOuU eJG8wz9TWwgYS3QAI2qC/uS0rAv+0YbOhcagcP3tFVFEXE4j4amZx/dU1ZNsoB95GRoRcYTM aAoPRyFaPLzYRdS/gAjXGKBy4zh64XYD4BA+vfRFvNCWMiXhsATSr3I+AP4b4zR+08LFwXKi ge1RP9++WI1wnprszoUdz8HEuAuaYazehBsSxzFcnZXj0n4lH8p+Hrpg==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 04/14] devtool: ide-sdk: fix $@ overwritten by set in install_and_deploy script Date: Sun, 2 Aug 2026 21:52:51 +0200 Message-ID: <20260802195324.64533-5-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:54 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242581 From: Adrian Freihofer The generated install_and_deploy shell script uses 'set $BUILDDIR $BITBAKEDIR' to pass arguments to oe-init-build-env. This overwrites $@, so the original command-line arguments (e.g. -t user@host -P 2222) are lost before the embedded deploy script can read them. Save the original arguments in $_args before the 'set' call and pass $_args to the deploy script instead of $@. Extend the argument-parsing loop in the embedded Python deploy script to handle -P/--port in addition to -t/--target, so that non-default SSH ports are forwarded to devtool deploy-target. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_sdk.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index fea44740f3..940bb8211e 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -988,9 +988,16 @@ class RecipeModified: cmd_lines.append(' for key in my_dict:') cmd_lines.append(' setattr(self, key, my_dict[key])') cmd_lines.append('filtered_args = Dict2Class(filtered_args_dict)') - cmd_lines.append('if len(sys.argv) > 2:') - cmd_lines.append(' if sys.argv[1] == "-t" or sys.argv[1] == "--target":') - cmd_lines.append(' setattr(filtered_args, "target", sys.argv[2])') + cmd_lines.append('i = 1') + cmd_lines.append('while i < len(sys.argv) - 1:') + cmd_lines.append(' if sys.argv[i] in ("-t", "--target"):') + cmd_lines.append(' setattr(filtered_args, "target", sys.argv[i + 1])') + cmd_lines.append(' i += 2') + cmd_lines.append(' elif sys.argv[i] in ("-P", "--port"):') + cmd_lines.append(' setattr(filtered_args, "port", sys.argv[i + 1])') + cmd_lines.append(' i += 2') + cmd_lines.append(' else:') + cmd_lines.append(' i += 1') cmd_lines.append( 'setattr(filtered_args, "recipename", "%s")' % self.bpn) cmd_lines.append('deploy_no_d("%s", "%s", "%s", "%s", "%s", "%s", %d, "%s", "%s", filtered_args)' % @@ -1003,6 +1010,8 @@ class RecipeModified: """Generate a script which does install and deploy""" cmd_lines = ['#!/bin/sh'] + # Save the original command-line args before 'set' overwrites $@ + cmd_lines.append('_args="$@"') # . oe-init-build-env $BUILDDIR $BITBAKEDIR # Using 'set' to pass the build directory to oe-init-build-env in sh syntax cmd_lines.append('cd "%s" || { echo "cd %s failed"; exit 1; }' % ( @@ -1015,8 +1024,8 @@ class RecipeModified: cmd_lines.append( 'bitbake %s -c install --force || { echo "bitbake %s -c install --force failed"; exit 1; }' % (self.bpn, self.bpn)) - # Self contained devtool deploy-target - cmd_lines.append(self.gen_deploy_target_script(args) + ' "$@"') + # Self contained devtool deploy-target - use saved args, not $@ (overwritten by 'set') + cmd_lines.append(self.gen_deploy_target_script(args) + ' $_args') return self.write_script(cmd_lines, 'install_and_deploy') From patchwork Sun Aug 2 19:52:52 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94247 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 093B5C5518F for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29328.1785700422013989840 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=jqtx0dNt; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-20260802195339ab7a517a1200020783-etempx@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 20260802195339ab7a517a1200020783 for ; Sun, 02 Aug 2026 21:53:39 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=KIp3fy9ivWo03/5d6hf+YxRNrFAt41ChKTqy4tb7ZQA=; b=jqtx0dNtNZbU2qPsqztCDjNBUhADnGpdvGahWGFCrZLRGplgX+7i31RgxRemV6rTpq+U4R PiS5ufBFNjZiG1UKOG5En4JwTbGHJyB/+Ojw/7jndesfL/RnBNa/NtMwmr0v/jbPdVC697qL EvmDh304gwZ29SZ7hHe3ZgqfpdIXm0iP2VdGkr++CTHKJ3Vh7APZcR10WOFqLnio0LoDvqqO 3oif9/0vXAyqWaCqXzb0ZE4DHdUuS5V9sGJ4JBxjP7wFdqDCwVr7fE5nDRmXKAZbIGa2jL6E qFvA3bFcnm+kHif3EPCvdUDaYzsQN9uR0NurJYzdHL2YD0tFPyCEpz3g==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 05/14] devtool: ide-sdk: fix meson compile_commands.json Date: Sun, 2 Aug 2026 21:52:52 +0200 Message-ID: <20260802195324.64533-6-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242579 From: Adrian Freihofer The generated meson.cross references the toolchain c/c++ binaries by bare name (e.g. "aarch64-poky-linux-clang++"), relying on PATH being set up by the meson wrapper script at build time. Meson stores that command exactly as configured in compile_commands.json and meson-info/intro-compilers.json without resolving it to an absolute path. cpptools (via the mesonbuild extension) resolves compilerPath using its own process PATH, which does not include the toolchain directory, and silently falls back to a host compiler, breaking IntelliSense. Real builds are unaffected since the wrapper script sets up PATH at build time, so leave the recipe's meson.cross untouched. Instead, layer an extra --cross-file on top that only absolutizes the c/cpp [binaries] entries, reusing the exact same flags as CC/CXX to avoid any behavior drift. Meson merges multiple machine files, with later files overriding matching keys from earlier ones. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/ide_code.py | 41 +++++++++++++++++++++ scripts/lib/devtool/ide_sdk.py | 1 + 2 files changed, 42 insertions(+) diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 96f007ca91..38f73e355c 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -128,6 +128,45 @@ class IdeVSCode(IdeBase): def dot_code_dir(self, modified_recipe): return os.path.join(modified_recipe.srctree, '.vscode') + def __gen_meson_absolute_cross_file(self, modified_recipe): + """Generate an extra cross file overriding c/cpp binaries with absolute paths. + + The recipe's generated meson.cross references the toolchain binaries + (e.g. "aarch64-poky-linux-clang++") by bare name, relying on PATH + being set up by the meson wrapper script at build time. Meson stores + that command exactly as configured (it never resolves it to an + absolute path itself), so it ends up unresolved in + compile_commands.json and meson-info/intro-compilers.json. cpptools + (via the mesonbuild extension) resolves compilerPath using its own + process PATH, which does not include the toolchain directory, and + silently falls back to a host compiler for IntelliSense. + + Real builds are unaffected since the wrapper script sets up PATH, so + the recipe's meson.cross is left untouched. Instead, an extra + --cross-file is layered on top with only the c/cpp [binaries] + entries absolutized, reusing the exact same flags as CC/CXX to avoid + any behavior drift. Meson merges multiple machine files, with later + files overriding matching keys from earlier ones. + """ + def absolutize(cmd): + args = cmd.split() + args[0] = os.path.join( + modified_recipe.staging_bindir_toolchain, args[0]) + return repr(args) + + lines = ["[binaries]"] + if modified_recipe.cc: + lines.append("c = %s" % absolutize(modified_recipe.cc)) + if modified_recipe.cxx: + lines.append("cpp = %s" % absolutize(modified_recipe.cxx)) + + os.makedirs(modified_recipe.ide_sdk_dir, exist_ok=True) + cross_file = os.path.join( + modified_recipe.ide_sdk_dir, 'meson-absolute-toolchain.cross') + with open(cross_file, 'w') as f: + f.write(os.linesep.join(lines) + os.linesep) + return ['--cross-file', cross_file] + def __vscode_settings_meson(self, settings_dict, modified_recipe): if modified_recipe.build_tool is not BuildTool.MESON: return @@ -135,6 +174,8 @@ class IdeVSCode(IdeBase): confopts = modified_recipe.mesonopts.split() confopts += modified_recipe.meson_cross_file.split() + if modified_recipe.meson_cross_file: + confopts += self.__gen_meson_absolute_cross_file(modified_recipe) confopts += modified_recipe.extra_oemeson.split() settings_dict["mesonbuild.configureOptions"] = confopts settings_dict["mesonbuild.buildFolder"] = modified_recipe.b diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 940bb8211e..69b002b635 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -478,6 +478,7 @@ class RecipeModified: self.bblayers = recipe_d.getVar('BBLAYERS').split() self.bitbakepath = recipe_d.getVar('BITBAKEPATH') self.bpn = recipe_d.getVar('BPN') + self.cc = recipe_d.getVar('CC') self.cxx = recipe_d.getVar('CXX') self.d = recipe_d.getVar('D') self.debug_build = recipe_d.getVar('DEBUG_BUILD') From patchwork Sun Aug 2 19:52:53 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94253 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 40A81C55196 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29324.1785700422013293272 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=TOTPwDaX; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-202608021953394baeddcd7e00020707-payrnc@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 202608021953394baeddcd7e00020707 for ; Sun, 02 Aug 2026 21:53:39 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=k4UPVtFk3W2QlwLQPTx1kP5ShJN+VdbGJTbwMCsrWQk=; b=TOTPwDaXpdksF1H1oKLvYCmXOyowKdXQBKQKOA/3Iz4SjsIRisrt7bbF8mCafrZnBEK17G htVj0oW0Q+sfxXX1cYPs0B8Ak5fBAkZK4UiZO/6xVsIBbS62TkwdpfB37Hs0T+C1rpGGnip2 xS4/ynVY7mCok6QljzvTd/S0/eTgTQ3DkXPQ7ohfoX8SZSj48DDIkFb4fjuHqQGxu/qoAzlf zSRAngVHU6elJf86rk7p/tOEtdKwVDNkKP+NskKoRtkNHSf9ttzqJCWKH8cQslB7fh95cAQh p8f3CuWDdntcE1oqqBAGhAPs7yWTjC4rzUZDc1d6LXp98k2aCmkPnmuw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 06/14] oe-selftest: devtool ide-sdk: cover breakpoints in exe, header and library Date: Sun, 2 Aug 2026 21:52:53 +0200 Message-ID: <20260802195324.64533-7-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242577 From: Adrian Freihofer _gdb_debug_cpp_example() already set breakpoints in the executable (cpp-example.cpp, by file:line) and the library (CppExample::print_json() in cpp-example-lib.cpp, by symbol), plus a 'list' (not a breakpoint) of the test_string declaration in the shared header cpp-example-lib.hpp. There was no actual breakpoint set on code that lives in the header itself, and no file:line breakpoint set inside the library's own .cpp file. Add CppExample::scale_number(), a trivial inline function defined directly in cpp-example-lib.hpp (not in a .cpp file), and call it from cpp-example.cpp's std::vector traversal loop. Set a breakpoint on it by file:line (cpp-example-lib.hpp, conditioned on n == 1 since the loop calls it once per vector element) and verify it is hit with the expected value. Also set a second, file:line breakpoint directly inside the library's own print_json() body (cpp-example-lib.cpp), in addition to the existing symbol breakpoint on print_json() itself. A symbol breakpoint only needs the function's entry address and resolves the same whether the debugger picks up stale or freshly rebuilt debug info for the library, since it doesn't rely on the library's own line-to-address mapping being fresh; a file:line breakpoint does, making it the only breakpoint in this test that actually exercises solib-search-path debug info resolution for the library. The second _gdb_cross_debugging_multi() call (after the code change/recompile/redeploy cycle) originally reused the exact same file:line breakpoints as the first call. A stale/incorrect line-to-address mapping left over from the previous build could in principle still resolve to a plausible address at the same line number, without that being noticed. To close that gap, parameterize the exe, header and library file:line breakpoints (exe_break_line/exe_list_line/hpp_break_line/lib_break_line) through _gdb_debug_cpp_example(), _gdb_debug_cpp_example_check() and _gdb_cross_debugging_multi(). Insert LINE_SHIFT (3) extra lines right before the targeted statements in cpp-example.cpp, cpp-example-lib.hpp and cpp-example-lib.cpp as part of the existing code-change step, and use the shifted line numbers for the second debug session. Together, this exercises breakpoint resolution for header-only debug info, the executable's own file:line breakpoint, the library's symbol breakpoint and the library's own file:line breakpoint as distinct code paths, and proves all of them are resolved against the freshly rebuilt debug info after a recompile/redeploy, rather than a stale/cached mapping from the previous build. Signed-off-by: Adrian Freihofer --- .../cpp/files/cpp-example-lib.hpp | 10 + .../recipes-test/cpp/files/cpp-example.cpp | 4 + meta/lib/oeqa/selftest/cases/devtool.py | 171 ++++++++++++++---- 3 files changed, 153 insertions(+), 32 deletions(-) diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp index 24dd0defb6..d1c9bca416 100644 --- a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp +++ b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include "config.h" @@ -13,6 +14,15 @@ struct CppExample { inline static const std::string test_string = "cpp-example-lib Magic: 123456789"; + /* Header-only function, to exercise breakpoint resolution against + * header-only debug info. */ + inline static int scale_number(int n) + { + int scaled = n * 7; + std::cout << "scale_number(" << n << ") = " << scaled << std::endl; + return scaled; + } + /* Retrieve a constant string */ const std::string &get_string(); /* Retrieve a constant string from a library */ diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp index 23d7169092..ad1abae257 100644 --- a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp +++ b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp @@ -58,5 +58,9 @@ int main(int argc, char* argv[]) std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl; } + // Example: call a header-only function once, to exercise breakpoint + // resolution against header-only debug info. + CppExample::scale_number(6); + return 0; } diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 2e91c2be97..dbcb05022a 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2868,11 +2868,31 @@ class DevtoolIdeSdkTests(DevtoolBase): self.assertExists(i_and_d_script_path) def _devtool_ide_sdk_qemu(self, tempdir, qemu, recipe_name, example_exe, compile_cmd): - """Verify deployment and execution in Qemu system work for one recipe. + """Verify deployment, execution and remote debugging in Qemu system work for one recipe. - This function checks the entire SDK workflow: changing the code, recompiling - it and deploying it back to Qemu, and checking that the changes have been - incorporated into the provided binaries. It also runs the tests of the recipe. + This function checks the entire SDK workflow twice, before and after a code + change: + - Deploying the example and its ptests to Qemu and running them. + - Remote debugging with gdb-cross, hitting breakpoints in the executable + (by file:line), the library (by symbol and by file:line) and a + header-only function (by file:line), see _gdb_cross_debugging_multi(). + + Between the two passes, it changes the magic string printed by the example + and inserts LINE_SHIFT extra lines right before the statements the file:line + breakpoints target, then recompiles and redeploys. This proves the second + pass's breakpoints resolve against the freshly rebuilt debug info (now at + shifted line numbers), rather than a stale/cached line-to-address mapping + left over from the first pass. + + The library's own cpp-example-lib.cpp is shifted too, and a breakpoint is + set there by file:line (in addition to the existing symbol breakpoint on + print_json()). Only install_deploy_cmd (do_install + deploy-target) runs + between the two passes, not a full image rebuild, so rootfs/rootfs-dbg + keep whatever debug symbols the initial bootstrap image build produced. + A file:line breakpoint at a line that only exists after the shift can + therefore only resolve correctly if solib-search-path prefers the + library's freshly rebuilt debug info in the image folder (D) over a + stale rootfs-dbg copy. """ recipe_id, _ = self._get_recipe_ids(recipe_name) i_and_d_script = "install_and_deploy_" + recipe_id @@ -2905,13 +2925,55 @@ class DevtoolIdeSdkTests(DevtoolBase): self._gdb_cross_debugging_multi( qemu, recipe_name, example_exe, DevtoolIdeSdkTests.MAGIC_STRING_ORIG) - # Replace the Magic String in the code, compile and deploy to Qemu + # Replace the Magic String in the code, compile and deploy to Qemu. + # Also insert LINE_SHIFT extra lines right before the statements the + # gdb file:line breakpoints target in the executable (cpp-example.cpp), + # the library (cpp-example-lib.cpp) and the header (cpp-example-lib.hpp), + # so those breakpoints land on different line numbers after the + # recompile/redeploy below. This proves the breakpoints are resolved + # against the freshly rebuilt debug info, rather than happening to + # still work against a stale, cached line-to-address mapping from the + # previous build. + LINE_SHIFT = 3 + extra_lines = "".join( + " // extra line %d inserted by the test to shift subsequent line numbers\n" % i + for i in range(LINE_SHIFT)) + cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp') with open(cpp_example_lib_hpp, 'r') as file: cpp_code = file.read() cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, MAGIC_STRING_NEW) + cpp_code = cpp_code.replace( + " inline static int scale_number(int n)", + extra_lines + " inline static int scale_number(int n)") with open(cpp_example_lib_hpp, 'w') as file: file.write(cpp_code) + + cpp_example_cpp = os.path.join(tempdir, 'cpp-example.cpp') + with open(cpp_example_cpp, 'r') as file: + cpp_code = file.read() + cpp_code = cpp_code.replace( + " std::vector numbers = {1, 2, 3};", + extra_lines + " std::vector numbers = {1, 2, 3};") + with open(cpp_example_cpp, 'w') as file: + file.write(cpp_code) + + # Shift a line inside the library's own .cpp file (not the header, not + # the executable). This is the only file:line breakpoint target that + # actually resolves through solib-search-path for the rebuilt shared + # library, so it is the one that would fail to resolve (or resolve to + # the wrong/stale line) if solib-search-path preferred a stale + # rootfs-dbg copy of the library's debug info over the freshly + # rebuilt one in the image folder (D). + cpp_example_lib_cpp = os.path.join(tempdir, 'cpp-example-lib.cpp') + with open(cpp_example_lib_cpp, 'r') as file: + cpp_code = file.read() + cpp_code = cpp_code.replace( + " std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;", + extra_lines + " std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;") + with open(cpp_example_lib_cpp, 'w') as file: + file.write(cpp_code) + runCmd(compile_cmd, cwd=tempdir, output_log=self._cmd_logger) runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger) @@ -2927,9 +2989,13 @@ class DevtoolIdeSdkTests(DevtoolBase): self.assertEqual(status, 0, msg="%s failed: %s" % (ptest_cmd, output)) self.assertIn("PASS: cpp-example-lib", output) - # Verify remote debugging works wit the modified magic string + # Verify remote debugging works with the modified magic string, with + # the file:line breakpoints shifted by LINE_SHIFT lines compared to + # the first _gdb_cross_debugging_multi call above. self._gdb_cross_debugging_multi( - qemu, recipe_name, example_exe, MAGIC_STRING_NEW) + qemu, recipe_name, example_exe, MAGIC_STRING_NEW, + exe_break_line=56 + LINE_SHIFT, exe_list_line=55 + LINE_SHIFT, + hpp_break_line=21 + LINE_SHIFT, lib_break_line=31 + LINE_SHIFT) def _gdb_cross(self): """Verify gdb-cross is provided by devtool ide-sdk""" @@ -2944,53 +3010,91 @@ class DevtoolIdeSdkTests(DevtoolBase): self.assertEqual(r.status, 0) self.assertIn("GNU gdb", r.output) - def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run"): + def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run", + exe_break_line=56, exe_list_line=55, hpp_break_line=21, + lib_break_line=31): """Get a series of gdb commands to debug the cpp-example-lib example""" gdb_batch_cmd = " -ex 'break main' -ex '%s'" % gdb_start_cmd gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'" gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string - gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'" + gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:15,15'" + + # Break inside the library's own .cpp file by file:line (not by + # symbol), while still inside the print_json() call reached above. + # Unlike the symbol breakpoint above, resolving a file:line breakpoint + # requires the line-to-address mapping from the library's debug info + # that matches the currently deployed build. lib_break_line shifts + # after the test edits and recompiles cpp-example-lib.cpp, to prove + # this breakpoint resolves via the freshly rebuilt library debug info + # found through solib-search-path, rather than a stale rootfs-dbg + # copy left over from the last full image build. + gdb_batch_cmd += " -ex 'break cpp-example-lib.cpp:%d'" % lib_break_line + gdb_batch_cmd += " -ex 'continue'" + gdb_batch_cmd += " -ex 'list cpp-example-lib.cpp:%d,%d'" % (lib_break_line, lib_break_line) # check if resolving std::vector works with python scripts - gdb_batch_cmd += " -ex 'list cpp-example.cpp:55,55'" - # Break on line 56 (the std::cout after the declaration) so the vector - # constructor on line 55 has already run when GDB stops. - gdb_batch_cmd += " -ex 'break cpp-example.cpp:56'" + gdb_batch_cmd += " -ex 'list cpp-example.cpp:%d,%d'" % (exe_list_line, exe_list_line) + # Break on exe_break_line (the std::cout after the declaration) so the + # vector constructor on exe_list_line has already run when GDB stops. + # These line numbers shift after the test inserts extra lines and + # recompiles, proving the breakpoint resolves via the freshly rebuilt + # debug info rather than a stale, cached line-to-address mapping. + gdb_batch_cmd += " -ex 'break cpp-example.cpp:%d'" % exe_break_line gdb_batch_cmd += " -ex 'continue'" gdb_batch_cmd += " -ex 'print numbers'" + + # Break on scale_number(), an inline function defined directly in the + # header (cpp-example-lib.hpp), to exercise breakpoint resolution for + # header-only debug info, separately from the executable's own + # cpp-example.cpp (file:line breakpoint above) and the library's + # cpp-example-lib.cpp (CppExample::print_json() breakpoint above). + # hpp_break_line shifts for the same reason as exe_break_line above. + gdb_batch_cmd += " -ex 'break cpp-example-lib.hpp:%d'" % hpp_break_line + gdb_batch_cmd += " -ex 'continue'" + gdb_batch_cmd += " -ex 'print n'" gdb_batch_cmd += " -ex 'continue'" return gdb_batch_cmd - def _gdb_debug_cpp_example_check(self, gdb_output, magic_string): + def _gdb_debug_cpp_example_check(self, gdb_output, magic_string, exe_list_line=55, lib_break_line=31): self.assertIn("Breakpoint 1, main", gdb_output) self.assertIn("$1 = 0", gdb_output) # test.string.compare equal self.assertIn("$2 = -3", gdb_output) # test.string.compare longer self.assertIn( 'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, gdb_output) + # check that the file:line breakpoint set directly in the library's + # own .cpp file actually resolved and was hit at the expected + # (possibly shifted) line, i.e. against the freshly rebuilt library + # debug info rather than a stale rootfs-dbg copy + self.assertIn("cpp-example-lib.cpp:%d" % lib_break_line, gdb_output) + self.assertRegex( + gdb_output, r"%d\s+std::cout << json_object_to_json_string_ext\(jobj, flag\) << std::endl;" % lib_break_line) + # check if resolving std::vector works with python scripts - self.assertRegex(gdb_output, r"55\s+std::vector numbers = \{1, 2, 3\};") + self.assertRegex( + gdb_output, r"%d\s+std::vector numbers = \{1, 2, 3\};" % exe_list_line) self.assertIn("$3 = std::vector of length 3, capacity 3 = {1, 2, 3}", gdb_output) + # check that a breakpoint in an inline function defined directly in + # the header (cpp-example-lib.hpp) is resolved and hit + self.assertIn("scale_number", gdb_output) + self.assertIn("$4 = 6", gdb_output) # n == 6, the call argument + self.assertIn("exited normally", gdb_output) - def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string): + def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string, + exe_break_line=56, exe_list_line=55, hpp_break_line=21, + lib_break_line=31): """Verify gdb-cross is working - Test remote debugging: - break main - run - continue - break CppExample::print_json() - continue - print CppExample::test_string.compare("cpp-example-lib Magic: 123456789") - $1 = 0 - print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa") - $2 = -3 - list cpp-example-lib.hpp:14,14 - 13 inline static const std::string test_string = "cpp-example-lib Magic: 123456789"; - continue + Test remote debugging with breakpoints in the executable + (cpp-example.cpp), the library (cpp-example-lib.cpp, by symbol and by + file:line) and a header defined directly in cpp-example-lib.hpp. + exe_break_line, exe_list_line, hpp_break_line and lib_break_line are + parameterized because the caller shifts them to different line + numbers after recompiling, to prove the breakpoints resolve via the + freshly rebuilt debug info. """ sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' gdbserver_script = os.path.join(self._workspace_scripts_dir( @@ -3017,12 +3121,15 @@ class DevtoolIdeSdkTests(DevtoolBase): self.assertIn("1234", r.output) # Test remote debugging works - gdb_batch_cmd = " --batch " + self._gdb_debug_cpp_example(magic_string) + gdb_batch_cmd = " --batch " + self._gdb_debug_cpp_example( + magic_string, exe_break_line=exe_break_line, exe_list_line=exe_list_line, + hpp_break_line=hpp_break_line, lib_break_line=lib_break_line) r = runCmd(gdb_script + gdb_batch_cmd, output_log=self._cmd_logger) self.logger.debug("%s %s returned: %s", gdb_script, gdb_batch_cmd, r.output) self.assertEqual(r.status, 0) - self._gdb_debug_cpp_example_check(r.output, magic_string=magic_string) + self._gdb_debug_cpp_example_check( + r.output, magic_string=magic_string, exe_list_line=exe_list_line, lib_break_line=lib_break_line) # Stop the gdbserver r = runCmd(gdbserver_script + ' stop', output_log=self._cmd_logger) @@ -3314,7 +3421,7 @@ class DevtoolIdeSdkTests(DevtoolBase): -ex 'continue' \ -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789")' \ -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")' \ - -ex 'list cpp-example-lib.hpp:14,14' \ + -ex 'list cpp-example-lib.hpp:15,15' \ -ex 'continue' 3. Verifying debug output and stopping gdbserver """ From patchwork Sun Aug 2 19:52:54 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94243 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 6A2C9C55174 for ; Sun, 2 Aug 2026 19:53:53 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29486.1785700422278848954 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=FThEXcr+; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-202608021953398366a4b9d500020787-gv2ykr@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 202608021953398366a4b9d500020787 for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=QxrzNH6HUoHOfL0LV6BOP3tcS7Y8Psw+E/5mesx8zWg=; b=FThEXcr+dLuWrGbLql8pSPuVv8aUmbGqiJF2A5vqBemoaQqKPSYNuHFIPb1MsHbCHZVCZm Mz5kzVHbk1Z4VEe0VQT0qR05EeazhrUJr6AejbjZswLNLx92keSYR3sn5raC+GQBFIWeQ7ZQ gbu8rZs+qE5YRjHUpJU/h1UeNh3qSCVldajzNwvvuABMMVfnO/ZHfQf3GwccyRAGJPkm6dMF Y3LyXp8dcpUQTn5yx8SwLKgtGl+csHftzaQvze1/FjkIiOghPT9g06ND2Du6/1h0uWr42mtB uEM9OA5yQmxH5sa9/JcTZtfNEFwKnrPEnwkeeAV1B4Uu2dh+zUf7uPOw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 07/14] oe-selftest: devtool ide-sdk: add real debug coverage for meson+code Date: Sun, 2 Aug 2026 21:52:54 +0200 Message-ID: <20260802195324.64533-8-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:53 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242585 From: Adrian Freihofer test_devtool_ide_sdk_code_meson never booted qemu-system (missing @OETestTag("runqemu") and runqemu()), passed a hardcoded, non-existent target address (root@192.168.17.17), and only checked _gdb_cross() (gdb-cross --version). It never validated the generated launch.json nor performed an actual remote debugging session, unlike its cmake sibling test_devtool_ide_sdk_code_cmake. Bring it to parity with test_devtool_ide_sdk_code_cmake: boot qemu-system, use the real qemu.ip as target address, and call _verify_launch_json() plus _verify_launch_json_debugging() to exercise a genuine gdbserver-based debug session through the generated launch.json/tasks.json. This closes the last gap in the 2x2 matrix of build system (cmake/meson) x ide mode (code/none): all four combinations now have real breakpoint-debugging coverage. _verify_launch_json_debugging() matched the 'once' debug configuration by looking for "usr-bin-{recipe_name}_once" in its name. But the config name is derived from the binary's install path, so it is "usr-bin-{example_exe}_once". This happened to work for cmake-example, where the binary name equals the recipe name, but not for meson-example, whose recipe name is 'meson-example' while the installed binary is 'mesonex'. Match on example_exe instead, and drop the now-unused recipe_name parameter. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 83 +++++++++++++++---------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index dbcb05022a..3b24ce3914 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -3395,7 +3395,7 @@ class DevtoolIdeSdkTests(DevtoolBase): self.assertEqual(len(once_configs), 2, f"Should have two once configuration, found: {once_configs}") self.assertEqual(len(attach_configs), 1, f"Should have one attach configuration, found: {attach_configs}") - def _verify_launch_json_debugging(self, tempdir, qemu, recipe_name, example_exe): + def _verify_launch_json_debugging(self, tempdir, qemu, example_exe): """Verify remote debugging and deployment works using launch.json configurations This method tests the VSCode debug configurations by: @@ -3434,15 +3434,19 @@ class DevtoolIdeSdkTests(DevtoolBase): tasks = tasks_d["tasks"] # Test one configuration for remote debugging + # The config name is derived from the binary's install path + # (e.g. "usr-bin-"), which is not necessarily the same + # as the recipe name (e.g. meson-example installs a binary named + # "mesonex"). once_config_count = 0 for config in configurations: - if f"usr-bin-{recipe_name}_once" in config["name"]: + if f"usr-bin-{example_exe}_once" in config["name"]: once_config_count += 1 self._verify_launch_config(tempdir, config, tasks, qemu, example_exe, self._gdb_debug_cpp_example, self._gdb_debug_cpp_example_check) # It works but is not 100% reliable in VSCode # This one: https://github.com/microsoft/vscode-cpptools/issues/4243 ? - # elif f"usr-bin-{recipe_name}_attach" in config["name"] + # elif f"usr-bin-{example_exe}_attach" in config["name"] # self._verify_launch_config(tempdir, config, tasks, qemu, example_exe) else: continue @@ -3569,51 +3573,64 @@ class DevtoolIdeSdkTests(DevtoolBase): self._verify_launch_json(tempdir) # Verify deployment and remote debugging works - self._verify_launch_json_debugging(tempdir, qemu, recipe_name, example_exe) + self._verify_launch_json_debugging(tempdir, qemu, example_exe) + @OETestTag("runqemu") def test_devtool_ide_sdk_code_meson(self): """Verify a meson recipe works with ide=code mode""" recipe_name = "meson-example" + example_exe = "mesonex" build_file = "meson.build" testimage = "oe-selftest-image" self._check_workspace() self._write_bb_config([recipe_name]) - tempdir = self._devtool_ide_sdk_recipe( - recipe_name, build_file, testimage) - bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % ( - recipe_name, testimage) - runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger) - with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j: - settings_d = json.load(settings_j) - meson_exe = settings_d["mesonbuild.mesonPath"] - meson_build_folder = settings_d["mesonbuild.buildFolder"] + # Verify deployment to Qemu (system mode) works + self._check_runqemu_prerequisites() + bitbake(testimage) + with runqemu(testimage, runqemuparams="nographic") as qemu: + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=code' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger) - # Verify the wrapper for meson native is available - self.assertExists(meson_exe) + with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j: + settings_d = json.load(settings_j) + meson_exe = settings_d["mesonbuild.mesonPath"] + meson_build_folder = settings_d["mesonbuild.buildFolder"] - # Verify meson re-uses the o files compiled by bitbake - result = runCmd('%s compile -C %s' % - (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger) - self.assertIn("ninja: no work to do.", result.output) + # Verify the wrapper for meson native is available + self.assertExists(meson_exe) - # Verify the unit tests work (in Qemu) - runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir, - output_log=self._cmd_logger) + # Verify meson re-uses the o files compiled by bitbake + result = runCmd('%s compile -C %s' % + (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger) + self.assertIn("ninja: no work to do.", result.output) - # Verify re-building and testing works again - result = runCmd('%s compile -C %s --clean' % - (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger) - self.assertIn("Cleaning...", result.output) - result = runCmd('%s compile -C %s' % - (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger) - self.assertIn("Linking target", result.output) - runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir, - output_log=self._cmd_logger) + # Verify the unit tests work (in Qemu user mode) + runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir, + output_log=self._cmd_logger) - self._verify_install_script_code(tempdir, recipe_name) - self._gdb_cross() + # Verify re-building and testing works again + result = runCmd('%s compile -C %s --clean' % + (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger) + self.assertIn("Cleaning...", result.output) + result = runCmd('%s compile -C %s' % + (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger) + self.assertIn("Linking target", result.output) + runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir, + output_log=self._cmd_logger) + + self._verify_install_script_code(tempdir, recipe_name) + self._gdb_cross() + + # Verify the launch.json file created is valid + self._verify_launch_json(tempdir) + + # Verify deployment and remote debugging works + self._verify_launch_json_debugging(tempdir, qemu, example_exe) @OETestTag("runqemu") def test_devtool_ide_sdk_code_kernel_module(self): From patchwork Sun Aug 2 19:52:55 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94244 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 98623C55182 for ; Sun, 2 Aug 2026 19:53:53 +0000 (UTC) Received: from mta-65-225.siemens.flowmailer.net (mta-65-225.siemens.flowmailer.net [185.136.65.225]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29487.1785700422332029363 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=bWQYuH84; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.225, mailfrom: fm-1329275-2026080219534049d7ff1f2a0002077e-u1g4ys@rts-flowmailer.siemens.com) Received: by mta-65-225.siemens.flowmailer.net with ESMTPSA id 2026080219534049d7ff1f2a0002077e for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=xPPzwRPdJA5zHeEAV1m7w+T21hQQaSdHa1HExur50Z0=; b=bWQYuH8452ZTejPjsMuaqPtMemT/21+8A5wwEeRtH8KW1wXB09eyv+9bgQbaJMKdhfvKLl EnaNK30R01eCc6a9FJ92IWNyn1LVRj+iJOSsFgxuhrwx3zKn4K2hzVLhUS/1KSvDSI67P7jr pyYcNg4GM4AUkse9pIL9SsKrsQ7Kp1NhlcwQ9Sj1h0ny0LSOGqDUS3IbZUrqt3aI3R6Mvw2N uMpDvWzwTgbz+zYNwPEKh3IcSXBJQFlVINeXF5CYuBZpu5d7vpTlk8zOWmcJGXWgkoDAXGUX xW2U6l2z/FHSRdgKaU96Om6L49Jb8n2IeFjWopEbmoqAJVyMGfTEQVyQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 08/14] devtool: ide-sdk debugger back-end abstraction Date: Sun, 2 Aug 2026 21:52:55 +0200 Message-ID: <20260802195324.64533-9-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:53 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242584 From: Adrian Freihofer 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 --- 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 --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( From patchwork Sun Aug 2 19:52:56 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94254 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id ABBE0C55843 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29329.1785700422086603772 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=K07rTKyt; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-202608021953407cfddd211900020742-9ds_0r@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 202608021953407cfddd211900020742 for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=ozaIefXT8Dn6uxpiDQ7Q7l2kyje+P0hOazpzXE1hiqs=; b=K07rTKyt+HpaGyNxsOr8Wc0vF8ZvIAAvuCcfEtGdgBk9JLZxdDXDRmPjl4S5xxWaobXpXl nZsheJDGsyIRQQ0iwz/rtCavLi4/LmsbiPvD7EeWjSKiHydgUJJkSE77lSfrxAr2B5AalpEE ZCaClrsoYY3euFUtEGsQt4kcX/92FpNh/KkqH4aTC25UJU+pTK+vy75TIg61AWeVRoNSP2Hf kOBNAxyC6Vw0BFThP24UzcDzVyhiTDhb6xQduXdzWPqgMtUMiUowSMbiRKAHHuZiCaOUwVfB ppCF1fNgIebUi/dldPfu+fU5M3iBF9mLzLMm3vU0v9dXsidcScVJubsw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 09/14] devtool: ide-sdk: wait for gdbserver port before returning Date: Sun, 2 Aug 2026 21:52:56 +0200 Message-ID: <20260802195324.64533-10-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242572 From: Adrian Freihofer In MULTI mode, gdbserver is started as a background process and the SSH command returned immediately, leaving a race between the caller connecting to gdbserver and gdbserver finishing its bind()/listen() sequence. The race condition was observed with lldb-server not with gdbserver, but it is likely to affect both. It might be a fix for gdbserver as well, but at least it is a preparatory step for adding LLDB support, which is the next planned item. There are two possible synchronisation points: - The pid file: written by the shell immediately after fork(), before gdbserver has called bind() or listen() — not useful as a readiness signal. - /proc/net/tcp: the port entry appears after remote_prepare() completes socket()+bind()+listen(), which is the earliest point at which gdbserver will accept a connection. Replace the pid-file idempotency check with a /proc/net/tcp port check so that: - the SSH command doubles as a readiness probe (exits only when gdbserver is actually listening, or after a 10 s timeout with exit 1) - re-running the start command while the server is already up is still a no-op The VSCode task for MULTI mode is changed accordingly: since the SSH command now exits as soon as the server is ready, VSCode no longer needs isBackground + a pattern matcher — a plain task with an empty problemMatcher suffices. The pid file is still written so that the stop script can kill the server by PID. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/__init__.py | 7 ++- scripts/lib/devtool/ide_plugins/ide_code.py | 59 +++++++++++++-------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index 4a1686a034..cfb067548d 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -170,11 +170,14 @@ class GdbCrossConfig(DebuggerCrossConfig): else: raise DevtoolError("Cannot use gdbserver attach mode for binary %s. No PID found." % self.binary.binary_path) elif server_mode == DebuggerServerModes.MULTI: - gdbserver_cmd_start = "test -f %s && exit 0; " % self._gdbserver_pid_file(server_mode) + hex_port = "%04X" % self.debug_server_port + gdbserver_cmd_start = "grep -q :%s /proc/net/tcp /proc/net/tcp6 2>/dev/null && exit 0; " % hex_port gdbserver_cmd_start += "mkdir -p %s; " % self._gdbserver_tmp_dir(server_mode) gdbserver_cmd_start += "%s --multi :%s > %s 2>&1 & " % ( 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) + gdbserver_cmd_start += "echo \\$! > %s; " % self._gdbserver_pid_file(server_mode) + gdbserver_cmd_start += "_w=0; while ! grep -q :%s /proc/net/tcp /proc/net/tcp6 2>/dev/null; " % hex_port + gdbserver_cmd_start += "do _w=\\$((_w+1)); [ \\$_w -lt 100 ] || exit 1; sleep 0.1; done;" else: raise DevtoolError("Unsupported gdbserver mode: %s" % server_mode) return "\"/bin/sh -c '" + gdbserver_cmd_start + "'\"" diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index dfaba3cff6..d237ab8f66 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -456,30 +456,45 @@ class IdeVSCode(IdeBase): if cross_debug_config.modified_recipe is not modified_recipe: continue for server_mode in cross_debug_config.server_modes(): - 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 + if server_mode == DebuggerServerModes.MULTI: + # MULTI mode: the SSH command blocks until the port is ready + # (wait loop in _target_start_cmd), so VSCode treats this as + # a regular non-background task. + 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: gdbserver runs in the foreground for the + # whole session, so VSCode needs isBackground + a pattern + # matcher to avoid waiting 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": ".", } - } - ] - } + ] + } # Deploy the artifacts to the target before starting gdbserver if not already running if server_mode != DebuggerServerModes.ATTACH: new_task['dependsOn'] = [ From patchwork Sun Aug 2 19:52:57 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94256 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 7C75DC55838 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-64-225.siemens.flowmailer.net (mta-64-225.siemens.flowmailer.net [185.136.64.225]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29330.1785700422188069689 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=PoA+9mig; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.225, mailfrom: fm-1329275-20260802195340a8006b87df00020771-_bijlc@rts-flowmailer.siemens.com) Received: by mta-64-225.siemens.flowmailer.net with ESMTPSA id 20260802195340a8006b87df00020771 for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=1VnbJhghmtfwZ9dwg3c5bmGFQFtVQb8br+AvoDpG4mw=; b=PoA+9migbGVC9uDgjBeC1wgWle3ysoDvHGdb/nLkdzE4LmuCPlZsTtTHkWWHqFb90oZdWf E7c6wVPpliK0jTFmchAf/1waafjw4IIGTlsG5HmIwXpXdJJeK7CIEx66xmffWj8q1K2NUbzt xomojGUPEF/4g8aC4KXx5nL0hBMbGShXvzTDiZO+FQmckLGpn/+uDE4LfIyvaJj2BkKpAqoM 898jmrsBU2Fu2siZ+4Y/4OlZsqu2b3ReJs1JPHdgt3aa/hFAKH6XAB8Ts95XNjrN0hkRIcVL UujLOkWzKWDPNngYEXxMuueBYYveNrGSzaobI7RkS26DTt4bBaF8+JUQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 10/14] devtool: ide-sdk add LLDB support for clang toolchain Date: Sun, 2 Aug 2026 21:52:57 +0200 Message-ID: <20260802195324.64533-11-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242574 From: Adrian Freihofer Add support for LLDB (CodeLLDB) remote debugging in VSCode when using the clang toolchain. This includes: - New LldbServerConfig class for configuring lldb-server on the target - LldbServerConfigVSCode for VSCode-specific LLDB configuration - RecipeLldbNative to handle lldb-native (architecture-agnostic) on the host - CodeLLDB VSCode extension recommendation for clang toolchain - Launch configuration generator for LLDB debugging - Proper handling of source maps and debug symbol paths for LLDB Meson/ninja invoke the compiler with source paths relative to the build directory B, rather than absolute paths. -fdebug-prefix-map and -ffile-prefix-map only rewrite paths that literally start with the mapped host prefix, so this relative DW_AT_name is never rewritten; only the absolute DW_AT_comp_dir is. When resolving the compile unit path, debuggers join comp_dir with the relative name, popping one component per leading "..". In devtool workspaces the source directory S is relocated far away from the build directory B (outside WORKDIR), so DW_AT_name typically contains more ".." components than comp_dir has path components. Once the join reaches "/", extra ".." are no-ops, so the resolved path becomes "/" plus the leftover suffix of DW_AT_name - a suffix of the real absolute source directory, not the "/usr/src/debug//" prefix that DEBUG_PREFIX_MAP and the generated sourceMap assume. Compute this "broken" resolved suffix for the recipe's own source directory and use it instead of the original comp_dir-based mapping. Keeping both mappings would point two different debug-info paths at the same host path, which is ambiguous when CodeLLDB needs to reverse the mapping (translating a locally opened file back into a debug-info path to resolve a breakpoint): it picks the first-registered ("normal") mapping, which never matches any real compile unit here, leaving breakpoints stuck pending with 0 locations. Also set launch.json's relativePathBase to the build directory B, so CodeLLDB resolves any source path that is still relative (i.e. not covered by sourceMap/target.source-map) against B - the compiler's working directory - instead of the default ${workspaceFolder}, which does not necessarily match B in devtool workspaces. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/__init__.py | 66 ++++++++ scripts/lib/devtool/ide_plugins/ide_code.py | 150 +++++++++++++++++- scripts/lib/devtool/ide_sdk.py | 161 +++++++++++++++++++- 3 files changed, 368 insertions(+), 9 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index cfb067548d..25575f18f4 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -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 '*:' so lldb-server binds on all interfaces (0.0.0.0), not + # just loopback. The bare ':' 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""" diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index d237ab8f66..9faba3f2d1 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -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 ": + # --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) diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 13fab50f22..b56a0925cb 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -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- 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//" 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) From patchwork Sun Aug 2 19:52:58 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94255 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 935A4C55A79 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29331.1785700422311774899 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=Cdh7NCw6; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-2026080219534050918e345000020793-yxgowa@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 2026080219534050918e345000020793 for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=TUhh4S/xuQOOaG8108ECbDcAgmCZ9vMjvAZpHdb6q64=; b=Cdh7NCw6z8xUGCBHWS4DZ2+B/TDIhenAjsSQPlCoKE/VOQxuC2ihnQU0x7nbrZjk2Sxxnk jr9Sawga+kFL2kpgVpLiA2aUvYTHSB5Pgk+yckSbCSEntM60nAV2qK+YSZBIU3gW7pKgfvSX xj0NiHceYgzql+NeIusKFsWACEk6nN66RY7AeUdxCxP/Ea26M8MR840ufg3nCamcJ3kDD2Qn 2b921fXnh9LWugcv8JLHLNLxNJ3XoFoST2W8zujeD6CiP+sQ2WWsXhg3FgZHsJUlfF04TdpC reA7ICa9cNT7seUcm2dcPZzfpvR2VhKhzy0qHVhk+axTkUPq6Fh/nbQA==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 11/14] devtool: ide-sdk: add LLDB support for ide=none (clang toolchain) Date: Sun, 2 Aug 2026 21:52:58 +0200 Message-ID: <20260802195324.64533-12-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242573 From: Adrian Freihofer Bring ide=none to feature parity with ide=code for clang recipes by adding LldbServerConfigNone and dispatching on the toolchain in IdeNone.setup_modified_recipe. Three files are generated per binary when toolchain == 'clang': lldb_server___ Shell script with start/stop logic (same pattern as the existing gdbserver_* scripts). Passes the lldb-server SSH command produced by LldbServerConfig._target_start_cmd, which already includes the /proc/net/tcp readiness poll for MULTI mode. lldbinit/lldbinit__ Init file sourced by lldb via -s. Sets up the remote platform connection, source maps, debug-file-search-paths and exec-search-paths, then creates the target with "target create --remote-file ". lldb__ Wrapper script: cd && lldb -s "$@" Supporting changes: LldbServerConfig (ide_plugins/__init__.py): add server_script_file / server_script, which were left as NotImplementedError in the base class. Required for LldbServerConfigNone to write its start scripts to the correct paths. RecipeLldbNative (ide_sdk.py): rename _lldb -> lldb (plain attribute), matching the RecipeGdbCross.gdb convention so that ide_none.py can access the host lldb binary path without going through a property. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/__init__.py | 6 + scripts/lib/devtool/ide_plugins/ide_none.py | 138 +++++++++++++++++++- scripts/lib/devtool/ide_sdk.py | 4 +- 3 files changed, 143 insertions(+), 5 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index 25575f18f4..d50ba7bc65 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -250,6 +250,12 @@ class LldbServerConfig(DebuggerCrossConfig): % {'pf': pid_file, 'td': tmp_dir}) return "\"/bin/sh -c '" + cmd + "'\"" + def server_script_file(self, mode): + return 'lldb_server_' + self.id_pretty_mode(mode) + + def server_script(self, mode): + return os.path.join(self.script_dir, self.server_script_file(mode)) + def server_modes(self): """ATTACH mode is not applicable for lldb-server platform.""" return [self.default_mode] diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py index f390331776..a8ddc3f39f 100644 --- a/scripts/lib/devtool/ide_plugins/ide_none.py +++ b/scripts/lib/devtool/ide_plugins/ide_none.py @@ -9,7 +9,7 @@ import os import logging import stat from bb.utils import mkdirhier -from devtool.ide_plugins import IdeBase, GdbCrossConfig, DebuggerServerModes +from devtool.ide_plugins import IdeBase, GdbCrossConfig, LldbServerConfig, DebuggerServerModes logger = logging.getLogger('devtool') @@ -145,6 +145,134 @@ class GdbCrossConfigNone(GdbCrossConfig): logger.info("Created: %s" % script_file) +class LldbServerConfigNone(LldbServerConfig): + """Generate lldb helper scripts for ide=none when using the clang toolchain.""" + + def __init__(self, image_recipe, modified_recipe, binary, + default_mode=DebuggerServerModes.MULTI): + super().__init__(image_recipe, modified_recipe, binary, default_mode) + + @property + def lldbinit_dir(self): + return os.path.join(self.script_dir, 'lldbinit') + + @property + def lldbinit(self): + return os.path.join(self.lldbinit_dir, 'lldbinit_' + self.id_pretty) + + @property + def lldb_script(self): + return os.path.join(self.script_dir, 'lldb_' + self.id_pretty) + + def _target_lldb_server_stop_cmd(self, server_mode): + """SSH command to stop lldb-server on the target.""" + if server_mode == DebuggerServerModes.MULTI: + pid_file = self._lldb_server_pid_file(server_mode) + tmp_dir = self._lldb_server_tmp_dir(server_mode) + cmd = ("test -f %(pf)s && kill \\$(cat %(pf)s) 2>/dev/null; rm -rf %(td)s" + % {'pf': pid_file, 'td': tmp_dir}) + else: + cmd = "killall lldb-server 2>/dev/null || true" + return "\"/bin/sh -c '" + cmd + "'\"" + + def _gen_lldb_server_start_script(self, server_mode=None): + """Generate a shell script starting lldb-server on the remote device via ssh.""" + if server_mode is None: + server_mode = self.default_mode + server_cmd_start = self._target_start_cmd(server_mode) + server_cmd_stop = self._target_lldb_server_stop_cmd(server_mode) + remote_ssh = "%s %s" % (self.debugger_cross.target_device.ssh_sshexec, + " ".join(self._target_ssh_args())) + script_lines = ['#!/bin/sh'] + script_lines.append('if [ "$1" = "stop" ]; then') + script_lines.append(' shift') + script_lines.append(" %s %s" % (remote_ssh, server_cmd_stop)) + script_lines.append('else') + script_lines.append(" %s %s" % (remote_ssh, server_cmd_start)) + script_lines.append('fi') + LldbServerConfigNone.write_file(self.server_script(server_mode), script_lines, True) + + def _gen_lldbinit_config(self, server_mode=None): + """Generate an lldbinit file for connecting to lldb-server on the target.""" + if server_mode is None: + server_mode = self.default_mode + 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) + else: + lines.append('# On the remote target:') + lines.append('# lldb-server platform --one-shot --server --listen *:%d' % self.debug_server_port) + lines.append('# On the build machine:') + lines.append('# cd ' + self.modified_recipe.real_srctree) + lines.append('# ' + self.debugger_cross.lldb + ' -s ' + self.lldbinit) + lines.append('platform select remote-linux') + lines.append('platform connect connect://%s:%d' % ( + self.debugger_cross.host, self.debug_server_port)) + lines.append('settings set target.process.thread.step-avoid-regexp ""') + + if self.image_recipe.rootfs_dbg: + src_file_map = dict(self.modified_recipe.reverse_debug_prefix_map) + if '/usr/src/debug' in src_file_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: + src_file_map['/usr/src/debug'] = os.path.join( + self.image_recipe.rootfs_dbg, 'usr', 'src', 'debug') + if src_file_map: + # Pass all pairs as a single settings set call + map_args = [] + for target_path, host_path in src_file_map.items(): + map_args += ['"' + target_path + '"', '"' + host_path + '"'] + lines.append('settings set target.source-map %s' % ' '.join(map_args)) + + debug_search_paths = ' '.join( + self.modified_recipe.solib_search_path(self.image_recipe)) + lines.append('settings set target.debug-file-search-paths %s' % debug_search_paths) + + exec_paths = list(dict.fromkeys([ + os.path.join(self.modified_recipe.d, self.modified_recipe.libdir.lstrip('/')), + os.path.join(self.modified_recipe.d, self.modified_recipe.base_libdir.lstrip('/')), + os.path.join(self.modified_recipe.d, self.modified_recipe.bindir.lstrip('/')), + ])) + lines.append('settings set target.exec-search-paths %s' % ' '.join(exec_paths)) + else: + logger.warning( + 'Cannot setup debug symbols configuration for LLDB. ' + 'IMAGE_GEN_DEBUGFS is not enabled.') + + lines.append('target create --remote-file %s %s' % ( + self.binary.binary_path, self.binary.binary_host_path)) + + LldbServerConfigNone.write_file(self.lldbinit, lines) + + def _gen_lldb_start_script(self): + """Generate a script starting lldb with the lldbinit configuration.""" + cmd_lines = ['#!/bin/sh'] + cmd_lines.append('cd ' + self.modified_recipe.real_srctree) + cmd_lines.append(self.debugger_cross.lldb + ' -s ' + self.lldbinit + ' "$@"') + LldbServerConfigNone.write_file(self.lldb_script, cmd_lines, True) + + def initialize(self): + self._gen_lldb_server_start_script() + self._gen_lldbinit_config() + self._gen_lldb_start_script() + + @staticmethod + def write_file(script_file, cmd_lines, executable=False): + script_dir = os.path.dirname(script_file) + mkdirhier(script_dir) + with open(script_file, 'w') as script_f: + script_f.write(os.linesep.join(cmd_lines)) + script_f.write(os.linesep) + if executable: + st = os.stat(script_file) + os.chmod(script_file, st.st_mode | stat.S_IEXEC) + logger.info("Created: %s" % script_file) + + class IdeNone(IdeBase): """Generate some generic helpers for other IDEs @@ -177,8 +305,12 @@ class IdeNone(IdeBase): script_path = modified_recipe.gen_install_deploy_script(args) logger.info("Created: %s" % script_path) - self.initialize_cross_debug_configs( - image_recipe, modified_recipe, GdbCrossConfigNone) + if modified_recipe.toolchain == 'clang': + self.initialize_cross_debug_configs( + image_recipe, modified_recipe, LldbServerConfigNone) + else: + 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 b56a0925cb..42b6e381c5 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -151,7 +151,7 @@ class RecipeLldbNative(RecipeNative): def __init__(self, args, target_device): super().__init__('lldb-native') self.target_device = target_device - self._lldb = None + self.lldb = None self._lldb_server_path = None def __find_lldb_server(self, config, tinfoil): @@ -164,7 +164,7 @@ class RecipeLldbNative(RecipeNative): def initialize(self, config, workspace, tinfoil): super()._initialize(config, workspace, tinfoil) - self._lldb = os.path.join(self.staging_bindir_native, 'lldb') + self.lldb = os.path.join(self.staging_bindir_native, 'lldb') self._lldb_server_path = self.__find_lldb_server(config, tinfoil) @property From patchwork Sun Aug 2 19:52:59 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94248 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id B1C84C55162 for ; Sun, 2 Aug 2026 19:53:53 +0000 (UTC) Received: from mta-65-227.siemens.flowmailer.net (mta-65-227.siemens.flowmailer.net [185.136.65.227]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29488.1785700422439725523 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=b3/l1BPR; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.227, mailfrom: fm-1329275-20260802195340cbe41a34bf0002078c-6lya53@rts-flowmailer.siemens.com) Received: by mta-65-227.siemens.flowmailer.net with ESMTPSA id 20260802195340cbe41a34bf0002078c for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=dHH2o3rNee1kXpinTtAx3bne73rKOROIx9uy3qMZlpk=; b=b3/l1BPRI8CmMfHxfBU1ZCNM9+R3cjdrO0+l8QgMEx77cJqFsd2MPVHksm4amfhv7uh7mB G+f9T2KZNef7CaLoMdpld54mzmIV5fJMPUvPx+wcJ4zobmG9WVjsqpjd42r+KtwJYZUTKd9i 3HUnkO2lJGpUGSBGnrpSJ499kmnfwkzZM0dX2nUJnlkpPuWk+ZnD6Kx4aWdQ+EhV3TlpQMJQ NmhS8ZRzisk2eeRXKIr+XbuthPR3K9VJcbBdnNT319oSyWYC3OXlXVShmd9Pc4/JrW8mcqOV rgibKjKJYAQZGoY6L/6q01ezD6CdPIEW1ewpK6X59XsOWpXSSp5FPbbw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 12/14] meta-selftest: refactor cpp examples into .inc files and add clang variants Date: Sun, 2 Aug 2026 21:52:59 +0200 Message-ID: <20260802195324.64533-13-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:53 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242583 From: Adrian Freihofer Refactor cmake-example.bb and meson-example.bb to extract common build logic into separate .inc files. Add clang variants of both examples to enable testing with alternative toolchains. Signed-off-by: Adrian Freihofer --- .../recipes-test/cpp/cmake-example-clang.bb | 13 +++++++ .../recipes-test/cpp/cmake-example.bb | 20 ++--------- .../recipes-test/cpp/cmake-example.inc | 29 +++++++++++++++ .../recipes-test/cpp/cmake-example/run-ptest | 10 ------ .../recipes-test/cpp/cpp-example.inc | 20 +++++++---- .../recipes-test/cpp/files/CMakeLists.txt | 35 +++++++++--------- .../recipes-test/cpp/files/meson.build | 17 +++++---- .../recipes-test/cpp/files/meson.options | 6 ++++ .../cpp/{meson-example => files}/run-ptest | 4 +-- .../recipes-test/cpp/meson-example-clang.bb | 13 +++++++ .../recipes-test/cpp/meson-example.bb | 24 ++----------- .../recipes-test/cpp/meson-example.inc | 36 +++++++++++++++++++ 12 files changed, 147 insertions(+), 80 deletions(-) create mode 100644 meta-selftest/recipes-test/cpp/cmake-example-clang.bb create mode 100644 meta-selftest/recipes-test/cpp/cmake-example.inc delete mode 100644 meta-selftest/recipes-test/cpp/cmake-example/run-ptest rename meta-selftest/recipes-test/cpp/{meson-example => files}/run-ptest (51%) create mode 100644 meta-selftest/recipes-test/cpp/meson-example-clang.bb create mode 100644 meta-selftest/recipes-test/cpp/meson-example.inc diff --git a/meta-selftest/recipes-test/cpp/cmake-example-clang.bb b/meta-selftest/recipes-test/cpp/cmake-example-clang.bb new file mode 100644 index 0000000000..bf20b94e14 --- /dev/null +++ b/meta-selftest/recipes-test/cpp/cmake-example-clang.bb @@ -0,0 +1,13 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +SUMMARY = "A C++ example compiled with cmake and clang." + +require cmake-example.inc + +TOOLCHAIN = "clang" +EX_BINARY_NAME = "${BPN}" +EX_SERVICE_USER = "cmake-example" diff --git a/meta-selftest/recipes-test/cpp/cmake-example.bb b/meta-selftest/recipes-test/cpp/cmake-example.bb index aecfcf780a..19d056fdd8 100644 --- a/meta-selftest/recipes-test/cpp/cmake-example.bb +++ b/meta-selftest/recipes-test/cpp/cmake-example.bb @@ -4,22 +4,8 @@ # SPDX-License-Identifier: MIT # -SUMMARY = "A C++ example compiled with cmake." +SUMMARY = "A C++ example compiled with cmake and GCC." -require cpp-example.inc +require cmake-example.inc -SRC_URI += "file://CMakeLists.txt" - -inherit cmake-qemu - -PACKAGECONFIG[failing_test] = "-DFAILING_TEST=ON" - -FILES:${PN}-ptest += "${bindir}/test-cmake-example" - -do_run_tests () { - bbnote ${DESTDIR:+DESTDIR=${DESTDIR} }${CMAKE_VERBOSE} cmake --build '${B}' --target test -- ${EXTRA_OECMAKE_BUILD} - eval ${DESTDIR:+DESTDIR=${DESTDIR} }${CMAKE_VERBOSE} cmake --build '${B}' --target test -- ${EXTRA_OECMAKE_BUILD} -} -do_run_tests[doc] = "Run cmake --target=test using qemu-user" - -addtask do_run_tests after do_compile +TOOLCHAIN = "gcc" diff --git a/meta-selftest/recipes-test/cpp/cmake-example.inc b/meta-selftest/recipes-test/cpp/cmake-example.inc new file mode 100644 index 0000000000..eb023d389a --- /dev/null +++ b/meta-selftest/recipes-test/cpp/cmake-example.inc @@ -0,0 +1,29 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +require cpp-example.inc + +SRC_URI += "file://CMakeLists.txt" + +inherit cmake-qemu + +PACKAGECONFIG[failing_test] = "-DFAILING_TEST=ON" + +# Support installing all recipe variants in parallel +EXTRA_OECMAKE += "\ + -DBINARY_NAME=${EX_BINARY_NAME} \ + -DTEST_BINARY_NAME=${EX_TEST_BINARY_NAME} \ +" + +FILES:${PN}-ptest += "${bindir}/${EX_TEST_BINARY_NAME}" + +do_run_tests () { + bbnote ${DESTDIR:+DESTDIR=${DESTDIR} }${CMAKE_VERBOSE} cmake --build '${B}' --target test -- ${EXTRA_OECMAKE_BUILD} + eval ${DESTDIR:+DESTDIR=${DESTDIR} }${CMAKE_VERBOSE} cmake --build '${B}' --target test -- ${EXTRA_OECMAKE_BUILD} +} +do_run_tests[doc] = "Run cmake --target=test using qemu-user" + +addtask do_run_tests after do_compile diff --git a/meta-selftest/recipes-test/cpp/cmake-example/run-ptest b/meta-selftest/recipes-test/cpp/cmake-example/run-ptest deleted file mode 100644 index 94b620a198..0000000000 --- a/meta-selftest/recipes-test/cpp/cmake-example/run-ptest +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -# -# Copyright OpenEmbedded Contributors -# -# SPDX-License-Identifier: MIT -# - -test-cmake-example - -# Note: run-ptests exits with exit value from test-cmake-example diff --git a/meta-selftest/recipes-test/cpp/cpp-example.inc b/meta-selftest/recipes-test/cpp/cpp-example.inc index 0671824d1c..0070d17201 100644 --- a/meta-selftest/recipes-test/cpp/cpp-example.inc +++ b/meta-selftest/recipes-test/cpp/cpp-example.inc @@ -35,10 +35,16 @@ INITSCRIPT_PARAMS = "defaults 99" # Create cpp-example user and group USERADD_PACKAGES = "${PN}" -GROUPADD_PARAM:${PN} = "--system ${BPN}" -USERADD_PARAM:${PN} = "--system --home /var/lib/${BPN} --no-create-home --shell /bin/false --gid ${BPN} ${BPN}" +GROUPADD_PARAM:${PN} = "--system ${EX_SERVICE_USER}" +USERADD_PARAM:${PN} = "--system --home /var/lib/${EX_SERVICE_USER} --no-create-home --shell /bin/false --gid ${EX_SERVICE_USER} ${EX_SERVICE_USER}" +EX_SERVICE_USER ?= "${BPN}" EX_BINARY_NAME ?= "${BPN}" +EX_TEST_BINARY_NAME ?= "test-${EX_BINARY_NAME}" + +do_install_ptest() { + sed -i -e 's|@TEST_BINARY_NAME@|${EX_TEST_BINARY_NAME}|g' ${D}${PTEST_PATH}/run-ptest +} do_install:append() { # Install configuration file owned by the recipe's unprivileged user. @@ -47,7 +53,7 @@ do_install:append() { # in the final image. devtool deploy-target is a raw file copy and does not run # pkg_postinst, so the UID in ${D} must already be correct. install -d ${D}${sysconfdir} - install -m 0644 -g ${BPN} -o ${BPN} ${S}/cpp-example.conf ${D}${sysconfdir}/${BPN}.conf + install -m 0644 -g ${EX_SERVICE_USER} -o ${EX_SERVICE_USER} ${S}/cpp-example.conf ${D}${sysconfdir}/${BPN}.conf sed -i -e 's|@BINARY_NAME@|${BPN}|g' ${D}${sysconfdir}/${BPN}.conf # Install service files or init scripts and substitute placeholders in service files @@ -57,8 +63,8 @@ do_install:append() { sed -i \ -e 's|@BINDIR@|${bindir}|g' \ -e 's|@BINARY_NAME@|${EX_BINARY_NAME}|g' \ - -e 's|@USER@|${BPN}|g' \ - -e 's|@GROUP@|${BPN}|g' \ + -e 's|@USER@|${EX_SERVICE_USER}|g' \ + -e 's|@GROUP@|${EX_SERVICE_USER}|g' \ ${D}${systemd_system_unitdir}/${BPN}.service else install -d ${D}${sysconfdir}/init.d @@ -66,8 +72,8 @@ do_install:append() { sed -i \ -e 's|@BINDIR@|${bindir}|g' \ -e 's|@BINARY_NAME@|${EX_BINARY_NAME}|g' \ - -e 's|@USER@|${BPN}|g' \ - -e 's|@GROUP@|${BPN}|g' \ + -e 's|@USER@|${EX_SERVICE_USER}|g' \ + -e 's|@GROUP@|${EX_SERVICE_USER}|g' \ ${D}${sysconfdir}/init.d/${BPN} fi } diff --git a/meta-selftest/recipes-test/cpp/files/CMakeLists.txt b/meta-selftest/recipes-test/cpp/files/CMakeLists.txt index e363f31af2..8802839702 100644 --- a/meta-selftest/recipes-test/cpp/files/CMakeLists.txt +++ b/meta-selftest/recipes-test/cpp/files/CMakeLists.txt @@ -14,6 +14,9 @@ project(cmake-example option(BUILD_SHARED_LIBS "Build using shared libraries" ON) option(FAILING_TEST "Compile a failing unit test to test the test infrastructure" OFF) +set(BINARY_NAME "cmake-example" CACHE STRING "Name of the installed executable and library prefix") +set(TEST_BINARY_NAME "test-cmake-example" CACHE STRING "Name of the installed test executable") + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED On) set(CMAKE_CXX_EXTENSIONS Off) @@ -21,7 +24,7 @@ set(CMAKE_CXX_EXTENSIONS Off) include(GNUInstallDirs) # Define the config file path as a constant -set(CPP_EXAMPLE_CONFIG_PATH "${CMAKE_INSTALL_FULL_SYSCONFDIR}/cmake-example.conf") +set(CPP_EXAMPLE_CONFIG_PATH "${CMAKE_INSTALL_FULL_SYSCONFDIR}/${BINARY_NAME}.conf") # Generate config.h from config.h.in configure_file(config.h.in config.h @ONLY) @@ -30,44 +33,44 @@ configure_file(config.h.in config.h @ONLY) find_package(json-c) # A simple library linking json-c library found by pkgconfig -add_library(cmake-example-lib cpp-example-lib.cpp cpp-example-lib.hpp) -set_target_properties(cmake-example-lib PROPERTIES +add_library(${BINARY_NAME}-lib cpp-example-lib.cpp cpp-example-lib.hpp) +set_target_properties(${BINARY_NAME}-lib PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) # Add the build directory to include path for config.h -target_include_directories(cmake-example-lib PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(${BINARY_NAME}-lib PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(cmake-example-lib PRIVATE json-c::json-c) +target_link_libraries(${BINARY_NAME}-lib PRIVATE json-c::json-c) -install(TARGETS cmake-example-lib +install(TARGETS ${BINARY_NAME}-lib INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) # A simple executable linking the library -add_executable(cmake-example cpp-example.cpp) -target_include_directories(cmake-example PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(cmake-example PRIVATE cmake-example-lib) +add_executable(${BINARY_NAME} cpp-example.cpp) +target_include_directories(${BINARY_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(${BINARY_NAME} PRIVATE ${BINARY_NAME}-lib) -install(TARGETS cmake-example +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) # A simple test executable for testing the library -add_executable(test-cmake-example test-cpp-example.cpp) -target_include_directories(test-cmake-example PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(test-cmake-example PRIVATE cmake-example-lib) +add_executable(${TEST_BINARY_NAME} test-cpp-example.cpp) +target_include_directories(${TEST_BINARY_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(${TEST_BINARY_NAME} PRIVATE ${BINARY_NAME}-lib) if (FAILING_TEST) - target_compile_definitions(test-cmake-example PRIVATE FAIL_COMPARISON_STR="foo") + target_compile_definitions(${TEST_BINARY_NAME} PRIVATE FAIL_COMPARISON_STR="foo") endif(FAILING_TEST) -install(TARGETS test-cmake-example +install(TARGETS ${TEST_BINARY_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) include(CTest) -add_test(NAME test-cmake-example COMMAND test-cmake-example) +add_test(NAME ${TEST_BINARY_NAME} COMMAND ${TEST_BINARY_NAME}) diff --git a/meta-selftest/recipes-test/cpp/files/meson.build b/meta-selftest/recipes-test/cpp/files/meson.build index 53248c4380..3cb4669dfa 100644 --- a/meta-selftest/recipes-test/cpp/files/meson.build +++ b/meta-selftest/recipes-test/cpp/files/meson.build @@ -16,8 +16,11 @@ if get_option('FAILING_TEST').enabled() add_project_arguments('-DFAIL_COMPARISON_STR=foo', language: 'cpp') endif +binary_name = get_option('BINARY_NAME') +test_binary_name = get_option('TEST_BINARY_NAME') + # Generate config.h from config.h.in -config_path = get_option('sysconfdir') / 'meson-example.conf' +config_path = get_option('sysconfdir') / get_option('CONFIG_FILE_NAME') conf_data = configuration_data() conf_data.set('CPP_EXAMPLE_CONFIG_PATH', config_path) configure_file(input : 'config.h.in', @@ -27,7 +30,7 @@ configure_file(input : 'config.h.in', # Include the build directory for config.h inc_dir = include_directories('.') -mesonexlib = shared_library('mesonexlib', +exlib = shared_library(binary_name + 'lib', 'cpp-example-lib.cpp', 'cpp-example-lib.hpp', version: meson.project_version(), soversion: meson.project_version().split('.')[0], @@ -36,18 +39,18 @@ mesonexlib = shared_library('mesonexlib', install : true ) -executable('mesonex', +executable(binary_name, 'cpp-example.cpp', - link_with : mesonexlib, + link_with : exlib, include_directories : inc_dir, install : true ) -test_mesonex = executable('test-mesonex', +test_exe = executable(test_binary_name, 'test-cpp-example.cpp', - link_with : mesonexlib, + link_with : exlib, include_directories : inc_dir, install : true ) -test('meson example test', test_mesonex) +test('meson example test', test_exe) diff --git a/meta-selftest/recipes-test/cpp/files/meson.options b/meta-selftest/recipes-test/cpp/files/meson.options index 58a0bf9e61..374e346197 100644 --- a/meta-selftest/recipes-test/cpp/files/meson.options +++ b/meta-selftest/recipes-test/cpp/files/meson.options @@ -1,3 +1,9 @@ option('FAILING_TEST', type : 'feature', value : 'disabled', description : 'Compile a failing unit test to test the test infrastructure') +option('BINARY_NAME', type : 'string', value : 'mesonex', + description : 'Name of the installed executable') +option('TEST_BINARY_NAME', type : 'string', value : 'test-mesonex', + description : 'Name of the installed test executable') +option('CONFIG_FILE_NAME', type : 'string', value : 'meson-example.conf', + description : 'Configuration file name in sysconfdir') diff --git a/meta-selftest/recipes-test/cpp/meson-example/run-ptest b/meta-selftest/recipes-test/cpp/files/run-ptest similarity index 51% rename from meta-selftest/recipes-test/cpp/meson-example/run-ptest rename to meta-selftest/recipes-test/cpp/files/run-ptest index b1804f0096..62c24db04f 100644 --- a/meta-selftest/recipes-test/cpp/meson-example/run-ptest +++ b/meta-selftest/recipes-test/cpp/files/run-ptest @@ -5,6 +5,6 @@ # SPDX-License-Identifier: MIT # -test-mesonex +@TEST_BINARY_NAME@ -# Note: run-ptests exits with exit value from test-mesonex +# Note: run-ptest exits with exit value from @TEST_BINARY_NAME@ diff --git a/meta-selftest/recipes-test/cpp/meson-example-clang.bb b/meta-selftest/recipes-test/cpp/meson-example-clang.bb new file mode 100644 index 0000000000..341ade21f9 --- /dev/null +++ b/meta-selftest/recipes-test/cpp/meson-example-clang.bb @@ -0,0 +1,13 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +SUMMARY = "A C++ example compiled with meson and clang." + +require meson-example.inc + +TOOLCHAIN = "clang" +EX_BINARY_NAME = "mesonex-clang" +EX_SERVICE_USER = "meson-example" diff --git a/meta-selftest/recipes-test/cpp/meson-example.bb b/meta-selftest/recipes-test/cpp/meson-example.bb index da0ea18376..b2335f5c26 100644 --- a/meta-selftest/recipes-test/cpp/meson-example.bb +++ b/meta-selftest/recipes-test/cpp/meson-example.bb @@ -4,26 +4,8 @@ # SPDX-License-Identifier: MIT # -SUMMARY = "A C++ example compiled with meson." +SUMMARY = "A C++ example compiled with meson and GCC." -require cpp-example.inc +require meson-example.inc -SRC_URI += "\ - file://meson.build \ - file://meson.options \ -" - -inherit pkgconfig meson - -PACKAGECONFIG[failing_test] = "-DFAILING_TEST=enabled" - -FILES:${PN}-ptest += "${bindir}/test-mesonex" - -do_run_tests () { - meson test -C "${B}" --no-rebuild -} -do_run_tests[doc] = "Run meson test using qemu-user" - -addtask do_run_tests after do_compile - -EX_BINARY_NAME = "mesonex" +TOOLCHAIN = "gcc" diff --git a/meta-selftest/recipes-test/cpp/meson-example.inc b/meta-selftest/recipes-test/cpp/meson-example.inc new file mode 100644 index 0000000000..2937be27f8 --- /dev/null +++ b/meta-selftest/recipes-test/cpp/meson-example.inc @@ -0,0 +1,36 @@ +# +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +# + +SUMMARY = "A C++ example compiled with meson." + +require cpp-example.inc + +SRC_URI += "\ + file://meson.build \ + file://meson.options \ +" + +inherit pkgconfig meson + +PACKAGECONFIG[failing_test] = "-DFAILING_TEST=enabled" + +# Support installing all recipes variants in parallel +EXTRA_OEMESON += "\ + -DBINARY_NAME=${EX_BINARY_NAME} \ + -DTEST_BINARY_NAME=${EX_TEST_BINARY_NAME} \ + -DCONFIG_FILE_NAME=${BPN}.conf \ +" + +FILES:${PN}-ptest += "${bindir}/${EX_TEST_BINARY_NAME}" + +do_run_tests () { + meson test -C "${B}" --no-rebuild +} +do_run_tests[doc] = "Run meson test using qemu-user" + +addtask do_run_tests after do_compile + +EX_BINARY_NAME = "mesonex" From patchwork Sun Aug 2 19:53:00 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94251 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id E4273C5518C for ; Sun, 2 Aug 2026 19:53:54 +0000 (UTC) Received: from mta-65-228.siemens.flowmailer.net (mta-65-228.siemens.flowmailer.net [185.136.65.228]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29489.1785700422682375736 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=Ie/Ro0l6; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.228, mailfrom: fm-1329275-202608021953403a80c893ab00020734-hfojp5@rts-flowmailer.siemens.com) Received: by mta-65-228.siemens.flowmailer.net with ESMTPSA id 202608021953403a80c893ab00020734 for ; Sun, 02 Aug 2026 21:53:40 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=4kA6xC9OKiwdqmfBKWt2oZsXbkU20yO9Wm8R4yvHkEY=; b=Ie/Ro0l6OK70i7cUyYq77cjMKwNJ20popj2IlwM8Q6YqOAqZqG9cWHZxRTsOWynWI9QTP0 5VoRdwHGM8lnFPimeFBxOYg1gJaRdzfkQA2YE+rry9YSx4yag+z2OZfHzZ7fVnUW5HQO7vWD qZMKadQVCgd+YncO7HgKmQeLTDIg/iqvy8TH/3CZK8z/5Ym12qVUqp91REopAbWw9GaeNTO5 EYcizSuBdzNRzQryFIk5qyX3u47kQC70EZUIDwanjn/vcTSJtIpOMRKTyGLzlu3Q2kVPqL6W KAAcuLACD1SSSlOM1QKTVFfZZmENadik4vHu3ootiPV9vCp5XsAKje4A==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 13/14] oe-selftest: devtool ide-sdk: add clang/LLDB test Date: Sun, 2 Aug 2026 21:53:00 +0200 Message-ID: <20260802195324.64533-14-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:54 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242580 From: Adrian Freihofer Add test_devtool_ide_sdk_code_cmake_clang to verify the full devtool ide-sdk workflow for a cmake recipe built with clang. Unlike the gcc variant the clang recipe uses lldb-server for remote debugging and CodeLLDB (vadimcn.vscode-lldb) as the VS Code debug adapter. The test covers: - devtool modify + devtool ide-sdk with ide=code - cmake preset compilation and CTest execution (same as the gcc test) - extensions.json recommends vadimcn.vscode-lldb - launch.json uses "type": "lldb" (CodeLLDB) instead of "type": "cppdbg" - End-to-end lldb --batch remote debugging session via lldb-server platform mode running on qemu Supporting changes: - _write_bb_config: accept optional extra_packages parameter so the clang test can add lldb-server to IMAGE_INSTALL - _verify_launch_json_lldb: new helper that validates the CodeLLDB launch.json structure (type, initCommands, program, cwd, preLaunchTask) - _lldb_server_debugging_once: new helper that reads the preLaunchTask SSH command from tasks.json, starts lldb-server on the target, and runs lldb --batch to verify a breakpoint at main is hit - _verify_service_running: use pgrep with exact regex (^name$) for exact process name matching; without that, pgrep would also match cmake-example-clang (truncated to 'cmake-example-c' in /proc/pid/comm) when checking for cmake-example, returning two PIDs and failing the isdigit() assertion Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 478 +++++++++++++++++++++++- 1 file changed, 474 insertions(+), 4 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 3b24ce3914..e9b8f38449 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2762,13 +2762,15 @@ class DevtoolIdeSdkTests(DevtoolBase): if self.logger.isEnabledFor(logging.DEBUG): self._cmd_logger = self.logger - def _write_bb_config(self, recipe_names): + def _write_bb_config(self, recipe_names, extra_packages=None): """Helper to write the bitbake local.conf file""" + image_install = 'gdbserver ' + ' '.join([r + '-ptest' for r in recipe_names]) + if extra_packages: + image_install += ' ' + ' '.join(extra_packages) conf_lines = [ 'IMAGE_CLASSES += "image-combined-dbg"', 'IMAGE_GEN_DEBUGFS = "1"', - 'IMAGE_INSTALL:append = " gdbserver %s"' % ' '.join( - [r + '-ptest' for r in recipe_names]), + 'IMAGE_INSTALL:append = " %s"' % image_install, 'DISTRO_FEATURES:append = " ptest"', # Static UIDs/GIDs are required so that files installed via # "install -o ${BPN}" in do_install embed the same UID that gets @@ -3217,7 +3219,9 @@ class DevtoolIdeSdkTests(DevtoolBase): def _verify_service_running(self, qemu, service_name): """Helper to verify a service is running in Qemu""" - status, output = qemu.run("pgrep %s" % service_name) + # Use anchored regex (^name$) instead of pgrep -x because the target + # may have busybox pgrep which does not support the -x flag. + status, output = qemu.run("pgrep '^%s$'" % service_name) self.assertEqual(status, 0, msg="%s service not running: %s" % (service_name, output)) self.assertTrue(output.strip().isdigit(), @@ -3908,6 +3912,472 @@ class DevtoolIdeSdkTests(DevtoolBase): runCmdEnv('meson setup %s' % tempdir_meson, cwd=cpp_example_src, output_log=self._cmd_logger) runCmdEnv('meson compile', cwd=tempdir_meson, output_log=self._cmd_logger) + def _verify_launch_json_lldb(self, tempdir): + """Verify the launch.json file contains valid CodeLLDB (type: lldb) configurations.""" + launch_json_path = os.path.join(tempdir, '.vscode', 'launch.json') + self.assertTrue(os.path.exists(launch_json_path), "launch.json file should exist") + + with open(launch_json_path) as launch_j: + launch_d = json.load(launch_j) + + self.assertIn("configurations", launch_d) + configurations = launch_d["configurations"] + self.assertGreater(len(configurations), 0, + "Should have at least one debug configuration") + + for config in configurations: + config_name = config.get("name", "Unknown") + # CodeLLDB configs use "type": "lldb", not "type": "cppdbg" + self.assertEqual(config["type"], "lldb", + f"Configuration '{config_name}' should use lldb type (CodeLLDB)") + self.assertNotIn("MIMode", config, + f"Configuration '{config_name}' should not have MIMode (CodeLLDB)") + self.assertNotIn("miDebuggerPath", config, + f"Configuration '{config_name}' should not have miDebuggerPath") + self.assertEqual(config["request"], "launch", + f"Configuration '{config_name}' should be launch type") + self.assertEqual(config["cwd"], "/tmp", + f"Configuration '{config_name}' cwd should be /tmp (writable on target)") + + # Verify initCommands contain the platform connect sequence + init_commands = config.get("initCommands", []) + self.assertTrue(any("platform select remote-linux" in cmd + for cmd in init_commands), + f"Configuration '{config_name}' should select remote-linux platform") + self.assertTrue(any("platform connect" in cmd for cmd in init_commands), + f"Configuration '{config_name}' should connect to remote platform") + + # Verify targetCreateCommands creates the target with --remote-file so + # LLDB uses the host debug binary for symbols but executes the pre-deployed + # binary on the target (avoiding the module-cache upload path). + target_create_commands = config.get("targetCreateCommands", []) + self.assertTrue(len(target_create_commands) > 0, + f"Configuration '{config_name}' should have targetCreateCommands") + create_cmd = target_create_commands[0] + self.assertIn("--remote-file", create_cmd, + f"Configuration '{config_name}' targetCreateCommands should use " + "--remote-file to specify the remote binary path") + self.assertIn("/image/", create_cmd, + f"Configuration '{config_name}' targetCreateCommands should reference " + "the host debug binary in the image directory") + + # Verify preLaunchTask referencing the lldb-server start task + task = config.get("preLaunchTask", "") + self.assertTrue(task, + f"Configuration '{config_name}' preLaunchTask should not be empty") + + def _find_source_break_line(self, tempdir, source_file, marker): + """Find the 1-based line number of `marker` in tempdir/source_file + + Used to set a source-level (file:line) breakpoint at a stable, + self-documenting location instead of hard-coding a line number that + would silently go stale whenever the example source changes. + """ + source_path = os.path.join(tempdir, source_file) + with open(source_path) as f: + for lineno, line in enumerate(f, start=1): + if marker in line: + return lineno + self.fail("Could not find marker %r in %s" % (marker, source_path)) + + def _lldb_debug_cpp_example_batch_commands(self, tempdir): + """Get a list of lldb --batch '-o' commands to debug the cpp-example-lib example + + Mirrors _gdb_debug_cpp_example: sets source-level (file:line) + breakpoints in the executable (cpp-example.cpp), the library's own + .cpp file (cpp-example-lib.cpp) and a header-only inline function + (cpp-example-lib.hpp), then inspects a variable at each stop. This + exercises breakpoint resolution against all three distinct debug-info + sources (executable, library, header-only), instead of only the + single call-site breakpoint in the executable used previously. + + The breakpoints are deliberately set by source file:line (rather than + by function/symbol name, e.g. "b main") because that is what exercises + CodeLLDB/LLDB's sourceMap reverse-lookup: translating a source file + path back into debug-info space to resolve the breakpoint location. + A symbol breakpoint resolves directly from the symbol table and would + not catch a broken/ambiguous sourceMap (see the "fix source map for + lldb" change), so it is not a sufficient regression test on its own. + + Line numbers are (re-)discovered via markers rather than hard-coded, + so the same commands keep working after a caller shifts lines with + _shift_cpp_example_lines_and_recompile() and recompiles. + """ + exe_break_line = self._find_source_break_line( + tempdir, 'cpp-example.cpp', 'cpp_example.print_json();') + lib_break_line = self._find_source_break_line( + tempdir, 'cpp-example-lib.cpp', + 'std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;') + hpp_break_line = self._find_source_break_line( + tempdir, 'cpp-example-lib.hpp', 'int scaled = n * 7;') + + return [ + "-o", "breakpoint set --file cpp-example.cpp --line %d" % exe_break_line, + "-o", "breakpoint set --file cpp-example-lib.cpp --line %d" % lib_break_line, + "-o", "breakpoint set --file cpp-example-lib.hpp --line %d" % hpp_break_line, + # Report the resolved location count right away: sourceMap + # ambiguity/underflow (the meson/ninja relative-path bug) leaves a + # breakpoint at "locations = 0 (pending)" even though the + # breakpoint gutter/status can otherwise look "verified". + "-o", "breakpoint list", + "-o", "run", + # Stop 1: the executable's own breakpoint, right before the call + # into the library. + "-o", "p cpp_example.get_string()", + "-o", "continue", + # Stop 2: inside the library's own .cpp file, by file:line. This + # is the breakpoint that would fail to resolve (or resolve to a + # stale line) if the freshly rebuilt library debug info was not + # preferred over a stale rootfs-dbg copy. Inspect a plain local + # variable (rather than the CppExample::test_string static class + # member, as the GDB test does) because LLDB cannot reliably + # evaluate an expression that needs to trigger the lazy + # initialization guard of a C++17 inline static std::string over + # a remote gdb-remote connection ("Couldn't look up symbols"). + "-o", "p flag", + "-o", "continue", + # Stop 3: inside an inline function defined directly in the + # header (cpp-example-lib.hpp). + "-o", "p n", + "-o", "continue", + "-o", "exit", + ] + + def _lldb_debug_cpp_example_check(self, output, magic_string): + """Check the output of an lldb --batch session run with the commands + from _lldb_debug_cpp_example_batch_commands()""" + self.assertNotIn("(pending)", output, + "breakpoints should resolve to a location instead of staying " + "pending (sourceMap/prefix-map path mismatch): %s" % output) + # LLDB emits this when the MD5 checksum embedded in the DWARF line + # table (recorded by the compiler at compile time) doesn't match the + # file currently on disk. This should never happen for a freshly + # (re)compiled and (re)deployed example: it would mean the debugger + # is displaying/attributing source lines that don't actually + # correspond to the binary being debugged. Treat it as a hard + # failure instead of silently tolerating it, so a regression here + # (e.g. a sourceMap entry getting clobbered, causing LLDB to resolve + # a source file against the wrong, stale location) doesn't go + # unnoticed. + self.assertNotIn("source file checksum mismatch", output, + "debug info should match the current source files exactly " + "(sourceMap resolving to a stale copy of the file?): %s" % output) + # exe (by file:line), library (by file:line) and header (by + # file:line) breakpoints should all have been hit: 1 + 1 + 1 = 3 + self.assertEqual(output.count("stop reason = breakpoint"), 3, + "expected 3 breakpoint hits (executable once, library " + "once, header inline function once): %s" % output) + # a local variable should be inspectable at the library breakpoint, + # proving the library's own debug info is usable (JSON_C_TO_STRING_SPACED + # | JSON_C_TO_STRING_PRETTY == 3). LLDB doesn't always print a "$N =" + # value slot (e.g. "(const int) 3" for a compile-time constant vs. + # "(int) $0 = 3"/"(int) 3" for an ordinary variable), so anchor the + # check on the "p flag" command itself rather than assuming a "=" + # appears in its output. + self.assertRegex(output, r"\(lldb\) p flag\r?\n[^\n]*\b3\b", + "should be able to inspect flag at the library breakpoint: %s" % output) + # the magic string should be visible in the program's own output, + # proving the freshly (re)compiled example actually ran + self.assertIn(magic_string, output, + "should be able to see the magic string printed by the example") + # the header-only inline function was hit once, called with n == 6 + self.assertRegex(output, r"\(lldb\) p n\r?\n[^\n]*\b6\b", + "should be able to inspect n == 6 at the header breakpoint: %s" % output) + self.assertIn("exited with status = 0", output, + "the example should run to completion and exit normally: %s" % output) + + def _shift_cpp_example_lines_and_recompile(self, tempdir, compile_cmd, install_deploy_cmd, + magic_string_new, line_shift=3): + """Change the magic string and insert line_shift extra lines before the + statements the LLDB file:line breakpoints target, then recompile and redeploy. + + Mirrors the code-change step of _devtool_ide_sdk_qemu (used by the GDB + tests): shifts the executable's, the library's and the header's source + lines so that a subsequent debugging pass' file:line breakpoints only + resolve correctly if they are based on the freshly rebuilt debug info, + rather than a stale/cached line-to-address mapping (or, for the + library, a stale rootfs-dbg copy instead of the freshly redeployed + image folder). + """ + extra_lines = "".join( + " // extra line %d inserted by the test to shift subsequent line numbers\n" % i + for i in range(line_shift)) + + cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp') + with open(cpp_example_lib_hpp, 'r') as file: + cpp_code = file.read() + cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, magic_string_new) + cpp_code = cpp_code.replace( + " inline static int scale_number(int n)", + extra_lines + " inline static int scale_number(int n)") + with open(cpp_example_lib_hpp, 'w') as file: + file.write(cpp_code) + + cpp_example_cpp = os.path.join(tempdir, 'cpp-example.cpp') + with open(cpp_example_cpp, 'r') as file: + cpp_code = file.read() + cpp_code = cpp_code.replace( + " cpp_example.print_json();", + extra_lines + " cpp_example.print_json();") + with open(cpp_example_cpp, 'w') as file: + file.write(cpp_code) + + cpp_example_lib_cpp = os.path.join(tempdir, 'cpp-example-lib.cpp') + with open(cpp_example_lib_cpp, 'r') as file: + cpp_code = file.read() + cpp_code = cpp_code.replace( + " std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;", + extra_lines + " std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;") + with open(cpp_example_lib_cpp, 'w') as file: + file.write(cpp_code) + + runCmd(compile_cmd, cwd=tempdir, output_log=self._cmd_logger) + runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger) + + def _lldb_cross_debugging_multi(self, tempdir, recipe_name, compile_cmd, lldb_session_func): + """Verify LLDB remote debugging in Qemu, before and after a code change. + + Mirrors _devtool_ide_sdk_qemu (the GDB equivalent): debugs the example + once, then edits the source (magic string + line shift on the + executable, library and header), recompiles, redeploys, and debugs a + second time to prove the breakpoints resolve against the freshly + rebuilt debug info rather than a stale/cached line-to-address mapping, + closing the coverage gap between the GDB and LLDB test paths. + + lldb_session_func is called once per pass with the magic string + expected for that pass; it is expected to run an lldb --batch session + and check its output (e.g. a closure around _lldb_server_debugging_once + or _lldb_none_debugging_multi). + """ + recipe_id, _ = self._get_recipe_ids(recipe_name) + install_deploy_cmd = os.path.join( + self._workspace_scripts_dir(recipe_name), 'install_and_deploy_' + recipe_id) + self.assertExists(install_deploy_cmd, '%s script not found' % install_deploy_cmd) + runCmd(install_deploy_cmd, output_log=self._cmd_logger) + + # First pass: debug the freshly deployed, unmodified example + lldb_session_func(DevtoolIdeSdkTests.MAGIC_STRING_ORIG) + + # Change the magic string, shift breakpoint lines, recompile and redeploy + magic_string_new = "Magic: 987654321" + self._shift_cpp_example_lines_and_recompile( + tempdir, compile_cmd, install_deploy_cmd, magic_string_new) + + # Second pass: debug again, breakpoints now resolved at shifted line numbers + lldb_session_func(magic_string_new) + + def _lldb_server_debugging_once(self, tempdir, qemu, magic_string): + """Verify lldb-server (platform mode) + lldb batch debugging works end-to-end. + + Reads the preLaunchTask SSH command from tasks.json to start lldb-server + on the target, then runs lldb --batch to perform a debugging session + covering the executable, the library and a header-only inline function + (see _lldb_debug_cpp_example_batch_commands), and checks that the + expected magic string and variable values are visible. + """ + sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' + + with open(os.path.join(tempdir, '.vscode', 'launch.json')) as f: + launch_d = json.load(f) + with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as f: + tasks_d = json.load(f) + + # Find the first *_once or *_multi config + lldb_config = next( + (c for c in launch_d["configurations"] + if "_once" in c["name"] or "_multi" in c["name"]), None) + self.assertIsNotNone(lldb_config, "Should have at least one lldb debug configuration") + + prelaunch_task_name = lldb_config["preLaunchTask"] + prelaunch_task = next( + (t for t in tasks_d["tasks"] if t["label"] == prelaunch_task_name), None) + self.assertIsNotNone(prelaunch_task, + "preLaunchTask '%s' not found in tasks.json" % prelaunch_task_name) + + # Extract the SSH command and start lldb-server on the target + task_command = prelaunch_task["command"] + task_args = prelaunch_task["args"] + self.assertEqual(task_command, "ssh", + "preLaunchTask should use ssh to start lldb-server") + ssh_cmd = [task_command] + task_args + if ssh_cmd[-1].startswith('"') and ssh_cmd[-1].endswith('"'): + # The tasks.json arg is formatted for a shell double-quoted context + # (e.g. bash running "ssh ... \"...\$((...))...\""). Strip the + # surrounding quotes and undo the \$ → $ escaping that would + # normally be done by bash, so the arg works when passed directly + # to SSH via subprocess without an intermediate shell. + ssh_cmd[-1] = ssh_cmd[-1][1:-1].replace('\\$', '$') + + # Extract connection details from initCommands + init_commands = lldb_config["initCommands"] + connect_cmd = next((c for c in init_commands if "platform connect" in c), None) + self.assertIsNotNone(connect_cmd, "initCommands should contain a platform connect command") + + # Find lldb binary from lldb-native sysroot + lldb_native_sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'lldb-native') + lldb_binary = os.path.join(lldb_native_sysroot, 'usr', 'bin', 'lldb') + self.assertExists(lldb_binary, "lldb binary should exist in lldb-native sysroot") + + with RunCmdBackground(ssh_cmd, output_log=self._cmd_logger): + time.sleep(1) + + # Verify lldb-server is running on the target + r = runCmd('ssh %s root@%s ps' % (sshargs, qemu.ip), + output_log=self._cmd_logger) + self.assertIn("lldb-server", r.output, + "lldb-server should be running on target") + + # Run lldb --batch: connect to platform, create target with remote-file, + # set a source-level breakpoint, and run. + # targetCreateCommands replaces the "program" field; each entry is + # passed as a separate -o command in batch mode. + target_create_commands = lldb_config.get("targetCreateCommands", []) + source_map = lldb_config.get("sourceMap", {}) + + lldb_batch = [lldb_binary, "--batch"] + for cmd in init_commands: + lldb_batch += ["-o", cmd] + for cmd in target_create_commands: + lldb_batch += ["-o", cmd] + if source_map: + # "settings set target.source-map" replaces the *entire* + # mapping list rather than appending to it. Issuing one + # "-o settings set target.source-map ..." per entry (as done + # previously) silently discards all but the last mapping, so + # LLDB ends up resolving source files (and verifying their + # DWARF MD5 checksum) against the wrong location, e.g. a + # stale rootfs-dbg copy of a devtool-modified recipe's own + # sources instead of the freshly edited workspace srctree. + # All pairs must therefore be set together in a single + # command, exactly like CodeLLDB itself does. + source_map_args = [] + for k, v in source_map.items(): + v_resolved = v.replace("${workspaceFolder}", tempdir) + source_map_args += [k, v_resolved] + lldb_batch += ["-o", "settings set target.source-map %s" % " ".join(source_map_args)] + lldb_batch += self._lldb_debug_cpp_example_batch_commands(tempdir) + r = runCmd(lldb_batch, output_log=self._cmd_logger) + self.assertEqual(r.status, 0, "lldb batch session failed: %s" % r.output) + self._lldb_debug_cpp_example_check(r.output, magic_string) + + @OETestTag("runqemu") + def test_devtool_ide_sdk_code_cmake_clang(self): + """Verify a cmake recipe built with clang works with ide=code (CodeLLDB debugging). + + This test uses the cmake-example-clang recipe which is a cmake-example variant + built with clang. It installs a separate binary (cmake-example-clang) so all four + recipe variants (cmake/meson x gcc/clang) can be installed in the same image + without conflicts. It is configured to use lldb-server for debugging instead of + gdbserver. The test flow is similar to test_devtool_ide_sdk_code_cmake but with + additional checks related to lldb: + - devtool ide-sdk selects lldb-native / lldb-server instead of gdb-cross + - launch.json uses "type": "lldb" (CodeLLDB) instead of "type": "cppdbg" + - extensions.json recommends vadimcn.vscode-lldb + - A basic lldb --batch remote debugging session succeeds against the + lldb-server platform running on the Qemu target + """ + recipe_name = "cmake-example-clang" + build_file = "CMakeLists.txt" + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config([recipe_name], extra_packages=['lldb-server']) + + self._check_runqemu_prerequisites() + bitbake(testimage) + with runqemu(testimage, runqemuparams="nographic") as qemu: + tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=code' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger) + + # Verify the cmake preset still works (build system unchanged) + compile_cmd = self._verify_cmake_preset(tempdir) + + # Verify the install && deploy-target task script exists + self._verify_install_script_code(tempdir, recipe_name) + + # Verify extensions.json recommends CodeLLDB instead of / alongside cpptools + with open(os.path.join(tempdir, '.vscode', 'extensions.json')) as ext_j: + ext_d = json.load(ext_j) + recommendations = ext_d.get('recommendations', []) + self.assertIn('vadimcn.vscode-lldb', recommendations, + 'vadimcn.vscode-lldb should be recommended for clang recipes') + + # Verify launch.json uses CodeLLDB format + self._verify_launch_json_lldb(tempdir) + + # Verify deployment and lldb batch remote debugging work end-to-end, + # before and after a code change/recompile/redeploy cycle (see + # _lldb_cross_debugging_multi) + self._lldb_cross_debugging_multi( + tempdir, recipe_name, compile_cmd, + lambda magic_string: self._lldb_server_debugging_once( + tempdir, qemu, magic_string)) + + @OETestTag("runqemu") + def test_devtool_ide_sdk_code_meson_clang(self): + """Verify a meson recipe built with clang works with ide=code (CodeLLDB debugging). + + This is the meson/ninja counterpart of test_devtool_ide_sdk_code_cmake_clang. + It matters as its own test (rather than being covered by the cmake/clang + test alone) because meson/ninja invoke the compiler with source paths + relative to the build directory, unlike cmake (with the Ninja or + Makefiles generators used here), which normally passes absolute source + paths. That relative-path compilation is what previously caused + -fdebug-prefix-map/-ffile-prefix-map underflow (DW_AT_name climbing + above DW_AT_comp_dir with excess dot-dot components) for devtool + workspaces, breaking source-level breakpoint resolution in CodeLLDB. + The cmake/clang test alone would not catch that regression. + + This test uses the meson-example-clang recipe which is a meson-example + variant built with clang. It installs a separate binary + (mesonex-clang) so all four recipe variants (cmake/meson x gcc/clang) + can be installed in the same image without conflicts. + """ + recipe_name = "meson-example-clang" + build_file = "meson.build" + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config([recipe_name], extra_packages=['lldb-server']) + + # Build image with debug settings (lldb-server for clang) before starting QEMU + self._check_runqemu_prerequisites() + tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage) + runCmd('devtool ide-sdk %s %s -c --ide=code' % (recipe_name, testimage), + output_log=self._cmd_logger) + + with runqemu(testimage, runqemuparams="nographic") as qemu: + # Re-run with actual QEMU IP; image is already built + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=code' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger) + + # Verify the meson build system still works (unchanged by clang/lldb support) + compile_cmd = self._verify_meson_build(tempdir, recipe_name) + + # Verify the install && deploy-target task script exists + self._verify_install_script_code(tempdir, recipe_name) + + # Verify extensions.json recommends CodeLLDB instead of / alongside cpptools + with open(os.path.join(tempdir, '.vscode', 'extensions.json')) as ext_j: + ext_d = json.load(ext_j) + recommendations = ext_d.get('recommendations', []) + self.assertIn('vadimcn.vscode-lldb', recommendations, + 'vadimcn.vscode-lldb should be recommended for clang recipes') + + # Verify launch.json uses CodeLLDB format + self._verify_launch_json_lldb(tempdir) + + # Verify deployment and lldb batch remote debugging work end-to-end, + # before and after a code change/recompile/redeploy cycle (see + # _lldb_cross_debugging_multi) + self._lldb_cross_debugging_multi( + tempdir, recipe_name, compile_cmd, + lambda magic_string: self._lldb_server_debugging_once( + tempdir, qemu, magic_string)) + def test_devtool_ide_sdk_plugins(self): """Test that devtool ide-sdk can use plugins from other layers.""" From patchwork Sun Aug 2 19:53:01 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 94252 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 65CC9C55197 for ; Sun, 2 Aug 2026 19:53:55 +0000 (UTC) Received: from mta-64-228.siemens.flowmailer.net (mta-64-228.siemens.flowmailer.net [185.136.64.228]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.29332.1785700422645016214 for ; Sun, 02 Aug 2026 12:53:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=S+/QlOnx; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.228, mailfrom: fm-1329275-202608021953403fb33a7e180002079c-p7svo6@rts-flowmailer.siemens.com) Received: by mta-64-228.siemens.flowmailer.net with ESMTPSA id 202608021953403fb33a7e180002079c for ; Sun, 02 Aug 2026 21:53:41 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=VlBbab+b47jjuu6VBG+Za41cQO3xmLB1NKGzvLZaJt8=; b=S+/QlOnxUyq27QESFlVu4JKCj4EspA8acTo7jwtM7CD1F9Z7mW5Xgr/zjG1hJe8Er2rB1y y/EOVG4+hTWgSoEGzc8GLI31XF3dM24ygBc4q+7JKow2Oo14NpR9pfk6xqD8DNE7Loe/xnth 5L8JhK4gWZB14A9jObCU6PbkH2qKqAH2iP/Le+GMM4BeNzI8jrzhESvIXxFPc5xJlwywL1wU kcTnfatJx4nAdLN6Es98T6mW2rnd/Dm+EjgHalDAqdNKOpLfKH9bzO6NKuRCfz9+M/paOscJ Od8vsCkrrZGQs5ZJIoiUEa8MgJSWSfajw2R4xSlp/i28ee3VeipC53Ag==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 14/14] oe-selftest: devtool ide-sdk: add test for ide=none LLDB/clang support Date: Sun, 2 Aug 2026 21:53:01 +0200 Message-ID: <20260802195324.64533-15-adrian.freihofer@siemens.com> In-Reply-To: <20260802195324.64533-1-adrian.freihofer@siemens.com> References: <20260802195324.64533-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Sun, 02 Aug 2026 19:53:55 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/242575 From: Adrian Freihofer Add test_devtool_ide_sdk_none_cmake_clang to DevtoolIdeSdkTests to exercise the new LldbServerConfigNone code path in ide_none.py. The test uses the cmake-example-clang recipe (TOOLCHAIN = "clang") and runs devtool ide-sdk with --ide=none. It verifies: - lldb_server___multi script is generated - lldbinit/lldbinit__ init file is generated - lldb__ wrapper script is generated - No gdbserver_* script is generated for a clang recipe - The install_and_deploy script exists - The oe-scripts symlink inside the source tree is valid - lldb-server can be started on the target via the generated script - The pid file written by the start script references the live process - An lldb --batch session using the generated wrapper reaches main and can evaluate CppExample::test_string at the breakpoint - lldb-server stops cleanly via the generated stop script A new helper _lldb_none_debugging_multi is added alongside the existing _gdb_cross_debugging_multi, following the same structure. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 192 ++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index e9b8f38449..7145755fed 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -4378,6 +4378,198 @@ class DevtoolIdeSdkTests(DevtoolBase): lambda magic_string: self._lldb_server_debugging_once( tempdir, qemu, magic_string)) + def _lldb_none_debugging_multi(self, tempdir, qemu, recipe_name, example_exe, magic_string): + """Verify lldb-server scripts generated by ide=none work end-to-end. + + Mirrors _gdb_cross_debugging_multi but for the LLDB/clang path: + - uses the generated lldb_server_* start/stop script + - uses the generated lldb_* wrapper script with lldb --batch + + Covers the executable, the library and a header-only inline function + (see _lldb_debug_cpp_example_batch_commands), since a single call-site + breakpoint would not exercise (and thus not catch a regression in) the + lldbinit source map / debug-file-search-paths setup for the library's + own debug info specifically. + """ + sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' + scripts_dir = self._workspace_scripts_dir(recipe_name) + binary_pretty = 'usr-bin-' + example_exe + lldb_server_script = os.path.join( + scripts_dir, 'lldb_server_1234_%s_multi' % binary_pretty) + lldb_script = os.path.join(scripts_dir, 'lldb_1234_%s' % binary_pretty) + + self.assertExists(lldb_server_script) + self.assertExists(lldb_script) + + # Start lldb-server (script exits once the port is ready) + r = runCmd(lldb_server_script, output_log=self._cmd_logger) + self.assertEqual(r.status, 0) + + # Verify lldb-server is running on the target + r = runCmd('ssh %s root@%s ps' % (sshargs, qemu.ip), + output_log=self._cmd_logger) + self.assertEqual(r.status, 0) + self.assertIn('lldb-server', r.output) + + # Verify the pid file points at the running lldb-server process + pid_file = '/tmp/lldb_server_1234_%s_multi/lldb_server.pid' % binary_pretty + test_cmd = "'cat /proc/$(cat %s)/cmdline'" % pid_file + r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd), + output_log=self._cmd_logger) + self.assertEqual(r.status, 0) + self.assertIn('lldb-server', r.output) + + # Run an lldb batch session covering the executable, library and + # header breakpoints, then continue to completion + lldb_batch_args = self._lldb_debug_cpp_example_batch_commands(tempdir) + r = runCmd([lldb_script, '--batch'] + lldb_batch_args, + output_log=self._cmd_logger) + self.logger.debug("lldb batch output: %s", r.output) + self.assertEqual(r.status, 0) + self._lldb_debug_cpp_example_check(r.output, magic_string) + + # Stop lldb-server + r = runCmd(lldb_server_script + ' stop', output_log=self._cmd_logger) + self.assertEqual(r.status, 0) + + # Verify lldb-server is no longer running + r = runCmd('ssh %s root@%s ps' % (sshargs, qemu.ip), + output_log=self._cmd_logger) + self.assertEqual(r.status, 0) + self.assertNotIn('lldb-server', r.output) + + @OETestTag("runqemu") + def test_devtool_ide_sdk_none_cmake_clang(self): + """Verify ide=none generates correct LLDB scripts for a clang cmake recipe. + + Uses cmake-example-clang (TOOLCHAIN = "clang") which is built with the + clang toolchain. devtool ide-sdk with --ide=none should produce: + - lldb_server___multi (start/stop script) + - lldbinit/lldbinit__ (platform connect + source maps) + - lldb__ (lldb wrapper) + + The test verifies that lldb-server can be started via the generated + script, and that a basic lldb --batch debugging session reaches main. + """ + recipe_name = 'cmake-example-clang' + example_exe = 'cmake-example-clang' + build_file = 'CMakeLists.txt' + testimage = 'oe-selftest-image' + + self._check_workspace() + self._write_bb_config([recipe_name], extra_packages=['lldb-server']) + self._check_runqemu_prerequisites() + bitbake(testimage) + with runqemu(testimage, runqemuparams='nographic') as qemu: + tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage) + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger) + + # Verify cmake preset still works (build system unchanged) + compile_cmd = self._verify_cmake_preset(tempdir) + + # Verify install && deploy script exists + # (_verify_install_script_code opens .vscode/tasks.json which is not + # generated for ide=none; check the script path directly instead) + recipe_id, _ = self._get_recipe_ids(recipe_name) + scripts_dir = self._workspace_scripts_dir(recipe_name) + self.assertExists(os.path.join( + scripts_dir, 'install_and_deploy_' + recipe_id)) + + # Verify LLDB scripts were generated (not GDB scripts) + binary_pretty = 'usr-bin-' + example_exe + self.assertExists(os.path.join( + scripts_dir, 'lldb_server_1234_%s_multi' % binary_pretty)) + self.assertExists(os.path.join( + scripts_dir, 'lldbinit', 'lldbinit_1234_%s' % binary_pretty)) + self.assertExists(os.path.join( + scripts_dir, 'lldb_1234_%s' % binary_pretty)) + # No GDB scripts should have been generated for a clang recipe + self.assertFalse(os.path.exists(os.path.join( + scripts_dir, 'gdbserver_1234_%s_multi' % binary_pretty)), + 'gdbserver script should not be generated for clang recipe') + + # Verify the oe-scripts sym-link is valid + self.assertEqual(self._workspace_scripts_dir(recipe_name), + self._sources_scripts_dir(tempdir)) + + # Verify end-to-end lldb debugging, before and after a code + # change/recompile/redeploy cycle (see _lldb_cross_debugging_multi) + self._lldb_cross_debugging_multi( + tempdir, recipe_name, compile_cmd, + lambda magic_string: self._lldb_none_debugging_multi( + tempdir, qemu, recipe_name, example_exe, magic_string)) + + @OETestTag("runqemu") + def test_devtool_ide_sdk_none_meson_clang(self): + """Verify ide=none generates correct LLDB scripts for a clang meson recipe. + + This is the meson/ninja counterpart of test_devtool_ide_sdk_none_cmake_clang. + It is required in addition to the cmake/clang test because meson/ninja + (unlike cmake here) invoke the compiler with source paths relative to + the build directory, which is what previously caused source-level + breakpoints to be left unresolved (pending) for devtool workspaces due + to -fdebug-prefix-map/-ffile-prefix-map underflow; see + _lldb_none_debugging_multi. + + Uses meson-example-clang (TOOLCHAIN = "clang"). + """ + recipe_name = 'meson-example-clang' + example_exe = 'mesonex-clang' + build_file = 'meson.build' + testimage = 'oe-selftest-image' + + self._check_workspace() + self._write_bb_config([recipe_name], extra_packages=['lldb-server']) + self._check_runqemu_prerequisites() + + # Build image with debug settings (lldb-server for clang) before starting QEMU + tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage) + runCmd('devtool ide-sdk %s %s -c --ide=none' % (recipe_name, testimage), + output_log=self._cmd_logger) + + with runqemu(testimage, runqemuparams='nographic') as qemu: + # Re-run with actual QEMU IP; image is already built + bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=none' % ( + recipe_name, testimage, qemu.ip) + runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger) + + # Verify the meson build system still works (unchanged by clang/lldb support) + compile_cmd = self._verify_meson_build(tempdir, recipe_name) + + # Verify install && deploy script exists + # (_verify_install_script_code opens .vscode/tasks.json which is not + # generated for ide=none; check the script path directly instead) + recipe_id, _ = self._get_recipe_ids(recipe_name) + scripts_dir = self._workspace_scripts_dir(recipe_name) + self.assertExists(os.path.join( + scripts_dir, 'install_and_deploy_' + recipe_id)) + + # Verify LLDB scripts were generated (not GDB scripts) + binary_pretty = 'usr-bin-' + example_exe + self.assertExists(os.path.join( + scripts_dir, 'lldb_server_1234_%s_multi' % binary_pretty)) + self.assertExists(os.path.join( + scripts_dir, 'lldbinit', 'lldbinit_1234_%s' % binary_pretty)) + self.assertExists(os.path.join( + scripts_dir, 'lldb_1234_%s' % binary_pretty)) + # No GDB scripts should have been generated for a clang recipe + self.assertFalse(os.path.exists(os.path.join( + scripts_dir, 'gdbserver_1234_%s_multi' % binary_pretty)), + 'gdbserver script should not be generated for clang recipe') + + # Verify the oe-scripts sym-link is valid + self.assertEqual(self._workspace_scripts_dir(recipe_name), + self._sources_scripts_dir(tempdir)) + + # Verify end-to-end lldb debugging, before and after a code + # change/recompile/redeploy cycle (see _lldb_cross_debugging_multi) + self._lldb_cross_debugging_multi( + tempdir, recipe_name, compile_cmd, + lambda magic_string: self._lldb_none_debugging_multi( + tempdir, qemu, recipe_name, example_exe, magic_string)) + def test_devtool_ide_sdk_plugins(self): """Test that devtool ide-sdk can use plugins from other layers."""