From patchwork Wed Mar 18 22:36:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83783 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 208C31088E56 for ; Wed, 18 Mar 2026 22:38:34 +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.27344.1773873511005352542 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=QPIi0otw; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.228, mailfrom: fm-1329275-20260318223827e0a4c1b0700002071c-nqyt3q@rts-flowmailer.siemens.com) Received: by mta-64-228.siemens.flowmailer.net with ESMTPSA id 20260318223827e0a4c1b0700002071c for ; Wed, 18 Mar 2026 23:38:28 +0100 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=PA/gKoxstrdCkK4pkvM2pRAulHSwohCk8obtB87bgZ4=; b=QPIi0otwXoF99MzcUCC2GZMh1P93AKvw/wboyNv6o7h2Wxqt3s/ws3gzlMxZNtDEr3FSo6 JYzYsaxbeQIIgTEyll/rFANhp/n1Nbl/YFaKIeBVTnKXP4ycdWtCubBFi4jcPODYd5DuNtp8 M0l61YayWUpZ3MIH+GxiMtxXd/HO1QXGoJqGYuYRF2b/69hUXHPRbBR1W00uxvdnqODivhg4 iqI+csiRZl0FqEPa9KvWWA4sDkwTqxiXK5cAhUo4Y9/xnLGx3yE79AO/CFvjdoEmkqiw8VHE RpsPcHzq3p7Wkj5FnSWAGmAKc103xMjGzGXzMJbDdDrbmSeoKodVv9VQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 1/9] oe-selftest: devtool: use stat for reading user/group names in ide-sdk tests Date: Wed, 18 Mar 2026 23:36:09 +0100 Message-ID: <20260318223736.3414885-2-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:34 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233440 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 # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Mon Mar 2 23:32:13 2026 +0100 # # interactive rebase in progress; onto c1fb515f2a # Last commands done (3 commands done): # pick 08cf9a6fc0 # Revert "devtool: ide-sdk deploy-target without bitbake" # reword 2ab466cf23 # oe-selftest: devtool: use stat for reading user names in ide-sdk tests # Next commands to do (3 remaining commands): # reword 81562bd21a # oe-selftest/cpp-example: fix conf file ownership with static UIDs/GIDs # reword 1f6fd56a04 # oe-selftest: devtool: GDB breakpoint after std::vector is constructed # You are currently editing a commit while rebasing branch 'adrianf/ide-sdk-improvements' on 'c1fb515f2a'. # # Changes to be committed: # modified: meta/lib/oeqa/selftest/cases/devtool.py # # ------------------------ >8 ------------------------ # Do not modify or remove the line above. # Everything below it will be ignored. diff --git c/meta/lib/oeqa/selftest/cases/devtool.py i/meta/lib/oeqa/selftest/cases/devtool.py index 5f25c4803b..9d8ffcc786 100644 --- c/meta/lib/oeqa/selftest/cases/devtool.py +++ i/meta/lib/oeqa/selftest/cases/devtool.py @@ -2934,11 +2934,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): 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 5f25c4803b..9d8ffcc786 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2934,11 +2934,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 Wed Mar 18 22:36:10 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83781 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 406311088E49 for ; Wed, 18 Mar 2026 22:38:33 +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.27347.1773873511005508456 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=DQfuxRMv; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-202603182238277a62e978e3000207f2-5x4upi@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 202603182238277a62e978e3000207f2 for ; Wed, 18 Mar 2026 23:38:28 +0100 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=IL6pWSrSkB7eUOwiByHTaIEidz0YIwg8zyFUkkzrP/o=; b=DQfuxRMvk0GyypDvN8c++P7kCJtsl6+M7GefDO4ZHIvjvC09MDayHqJp9zZ/+ZVncQ90zp tlgbQQVTDPhRP0iHHEE/99jDli+jXmUts8R0uhdiOgslLU3hl8wnkfjG19neK7aQUZoM9jFT ARO1v2pcq5alx91GhYXKU6dVVviOYiRLwTtNWD7/M+6W74+u++N5eF4J12hFVPHoyg7rKzpR W4pxtJLApnbJiEIC6087O/bTwhrrTzlssvqwIZ/T5l9dJin3U2EFC81yAjyJUbX9GfdvLiva +6AVsTT1m/3biNLGHVHhZvScy2hpWaIT6i4c2QJyJvsSHXbB5EUZWOWg==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 2/9] oe-selftest: devtool: GDB breakpoint after std::vector is constructed Date: Wed, 18 Mar 2026 23:36:10 +0100 Message-ID: <20260318223736.3414885-3-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:33 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233443 From: Adrian Freihofer Change the GDB breakpoint from line 55 to 56 in cpp-example.cpp so that the std::vector constructor has already executed when GDB stops. This ensures that inspecting the vector with GDB works as intended also with older GDB versions (e.g. on scarthgap). Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 9d8ffcc786..297dda7457 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2772,7 +2772,9 @@ class DevtoolIdeSdkTests(DevtoolBase): # check if resolving std::vector works with python scripts gdb_batch_cmd += " -ex 'list cpp-example.cpp:55,55'" - gdb_batch_cmd += " -ex 'break cpp-example.cpp: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 'continue'" gdb_batch_cmd += " -ex 'print numbers'" gdb_batch_cmd += " -ex 'continue'" From patchwork Wed Mar 18 22:36:11 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83786 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 08C151088E54 for ; Wed, 18 Mar 2026 22:38:34 +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.27345.1773873511005398453 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=HYppx01b; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.225, mailfrom: fm-1329275-20260318223827eb65a9a1370002075a-l17cs_@rts-flowmailer.siemens.com) Received: by mta-64-225.siemens.flowmailer.net with ESMTPSA id 20260318223827eb65a9a1370002075a for ; Wed, 18 Mar 2026 23:38:28 +0100 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=/K626ca1deFDCnmAmQkaFpTob7xtoG62ZwsM+fBUSRE=; b=HYppx01bAkRIDOvtjNLXkQIeQSjaJE57yiuawuJq2UZ7gsS1vfFxW3Y6mt5OtYiYOBdRHM 1WHQHtuSWmV5cRYfqCETsjkY3WRIOF6xv56V5SemTEpIURTQp13s5tjkGSRBF2BjNOf1Dmx4 DFITH7dknlfbWnT+QS60EXvvpmqVD+e2dSrFGbnSiQQamCvfJSph7hvYW+jlrrhitys2Q5uC URgMHaWiPP5BdtXfa4erWDqEniagB08pP0Vx0A39nvObP1MtuTq90Ay4TBKSvra+o6U8TDR9 lfm7FncVrzdccSicIBLn/02K2xmF8236yVuUD4KFFNRvcjFQjvtVo8Ew==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 3/9] oe-selftest: devtool: use assertRegex to match test output for meson Date: Wed, 18 Mar 2026 23:36:11 +0100 Message-ID: <20260318223736.3414885-4-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:34 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233438 From: Adrian Freihofer Replace strict string matching with assertRegex to allow for flexible whitespace in the "Fail: 0" output from meson tests. This improves test robustness against formatting changes. This issue was discovered with scarthgap. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 297dda7457..dc83d406fd 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2911,7 +2911,7 @@ class DevtoolIdeSdkTests(DevtoolBase): result = runCmd('%s test -C %s' % (meson_exe, build_dir), cwd=tempdir, output_log=self._cmd_logger) self.assertEqual(result.status, 0) - self.assertIn("Fail: 0", result.output) + self.assertRegex(result.output, r"Fail:\s+0") # Verify re-building and testing works again result = runCmd('%s compile -C %s --clean' % (meson_exe, build_dir), @@ -2922,7 +2922,7 @@ class DevtoolIdeSdkTests(DevtoolBase): result = runCmd('%s test -C %s' % (meson_exe, build_dir), cwd=tempdir, output_log=self._cmd_logger) self.assertEqual(result.status, 0) - self.assertIn("Fail: 0", result.output) + self.assertRegex(result.output, r"Fail:\s+0") return compile_cmd From patchwork Wed Mar 18 22:36:12 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83789 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 9F7CF1088E5C for ; Wed, 18 Mar 2026 22:38:34 +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.27165.1773873511001581849 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=VvlPuH3g; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.227, mailfrom: fm-1329275-20260318223828883b89edbc00020760-q9tpe_@rts-flowmailer.siemens.com) Received: by mta-65-227.siemens.flowmailer.net with ESMTPSA id 20260318223828883b89edbc00020760 for ; Wed, 18 Mar 2026 23:38:28 +0100 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=hhs5T89OYgG8aDf6D1ZGK6SZCRkm2A/2BtvBCbp7djU=; b=VvlPuH3g8Nqtp+gR9ZKFFhwvIFGvu3JXi70c0NMkEGHIMB8hEeD+DTnoutL2TU9xf3QfA2 sR5YUcoPZC70f7tAweLMQNv5hwhiofCCuG2L7m6XyH/7FnAnKYdnRovTunGlq2/TvFdb0ANS WpWhd9mnWDislCZv+z6hoRme5iU0E6olVg7Hh+2Osq1lxxb68V69+lthjYPz82if3lIHTHGn MKOvzsrNWzjpKwZzE1Bb4aDEL15QPK+H5turjGCpdR+p5L3uikWlMgbd6T8ZuF1R568rLdzD mVXigb2lgRnZjj/0BZN4A/FXRNj4WIEkFGGIpSvTCG+72uP7TUlmmGwA==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 4/9] oe-selftest/cpp-example: fix conf file ownership with static UIDs/GIDs Date: Wed, 18 Mar 2026 23:36:12 +0100 Message-ID: <20260318223736.3414885-5-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:34 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233442 From: Adrian Freihofer test_devtool_ide_sdk_none_qemu builds an image containing both cmake-example and meson-example, starts a QEMU instance, then uses devtool ide-sdk + devtool deploy-target to rebuild and redeploy each recipe in turn. The test verifies that /etc/.conf is owned by the matching user both before and after each deploy cycle. The test was failing with: /etc/meson-example.conf not owned by user meson-example: got cmake-example Root cause: both recipes call install -m 0644 -o ${BPN} -g ${BPN} ... ${D}${sysconfdir}/${BPN}.conf During do_install, pseudo resolves ${BPN} to a UID by looking up /etc/passwd in the recipe's own isolated RECIPE_SYSROOT. Since the sysroots are independent, both cmake-example and meson-example each see themselves as the first --system user and get the same UID (e.g. 100). Both ${D} trees therefore contain files with UID 100. In the final rootfs cmake-example is allocated UID 100 and meson-example UID 101. Files packaged for meson-example still carry UID 100, so stat reports them as owned by cmake-example. A pkg_postinst chown would fix the rootfs, but devtool deploy-target is a plain tar pipe over SSH with no package-manager involvement - it never runs pkg_postinst. Whatever UID is embedded in ${D} is what lands on the target. Not sure how this could be fixed with dynamic UIDs. A clean solution is to make every recipe sysroot and the final image agree on the same UIDs from the start, i.e. static IDs. Fix: - Enable USERADDEXTENSION = "useradd-staticids" in _write_bb_config so the test builds with static IDs for the duration of the test. - Add cmake-example (UID/GID 533) and meson-example (UID/GID 534) to meta-selftest/files/static-passwd and static-group. - Expand the comment in cpp-example.inc's do_install to document the static-ID requirement so future readers understand why the -o/-g flags work correctly only under useradd-staticids. - Fix a copy-paste error in the in-test comment (said "meson-example.conf ... cmake-example user" for the cmake block). Signed-off-by: Adrian Freihofer --- meta-selftest/files/static-group | 2 ++ meta-selftest/files/static-passwd | 2 ++ meta-selftest/recipes-test/cpp/cpp-example.inc | 6 +++++- meta/lib/oeqa/selftest/cases/devtool.py | 12 +++++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/meta-selftest/files/static-group b/meta-selftest/files/static-group index 3fca4aa5c9..adf436f310 100644 --- a/meta-selftest/files/static-group +++ b/meta-selftest/files/static-group @@ -28,4 +28,6 @@ ptest:x:529: xuser:x:530: seat:x:531: audio:x:532: +cmake-example:x:533: +meson-example:x:534: nogroup:x:65534: diff --git a/meta-selftest/files/static-passwd b/meta-selftest/files/static-passwd index cc6c5acd5c..8d9f149b9f 100644 --- a/meta-selftest/files/static-passwd +++ b/meta-selftest/files/static-passwd @@ -19,3 +19,5 @@ _apt:x:523:523::/:/bin/nologin weston:x:525:525::/:/bin/nologin ptest:x:529:529::/:/bin/nologin xuser:x:530:530::/:/bin/nologin +cmake-example:x:533:533::/var/lib/cmake-example:/bin/false +meson-example:x:534:534::/var/lib/meson-example:/bin/false diff --git a/meta-selftest/recipes-test/cpp/cpp-example.inc b/meta-selftest/recipes-test/cpp/cpp-example.inc index 2653f45e90..0671824d1c 100644 --- a/meta-selftest/recipes-test/cpp/cpp-example.inc +++ b/meta-selftest/recipes-test/cpp/cpp-example.inc @@ -41,7 +41,11 @@ USERADD_PARAM:${PN} = "--system --home /var/lib/${BPN} --no-create-home --shell EX_BINARY_NAME ?= "${BPN}" do_install:append() { - # Install configuration file owned by unprivileged user + # Install configuration file owned by the recipe's unprivileged user. + # Note: this requires static UIDs/GIDs (USERADDEXTENSION = "useradd-staticids") + # so that the UID embedded by pseudo during do_install matches the UID assigned + # 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 sed -i -e 's|@BINARY_NAME@|${BPN}|g' ${D}${sysconfdir}/${BPN}.conf diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index dc83d406fd..6c6f22a667 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2598,6 +2598,16 @@ class DevtoolIdeSdkTests(DevtoolBase): 'IMAGE_INSTALL:append = " gdbserver %s"' % ' '.join( [r + '-ptest' for r in recipe_names]), '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 + # assigned in the final image. Without this, each recipe's isolated + # sysroot allocates UIDs independently (both start at the first free + # system UID), so files end up with colliding UIDs in the image. + # devtool deploy-target is a raw file copy and does not run + # pkg_postinst, so ownership must be correct already in ${D}. + 'USERADDEXTENSION = "useradd-staticids"', + 'USERADD_UID_TABLES += "files/static-passwd"', + 'USERADD_GID_TABLES += "files/static-group"', ] self.write_config("\n".join(conf_lines)) @@ -2985,7 +2995,7 @@ class DevtoolIdeSdkTests(DevtoolBase): self.assertEqual(self._workspace_scripts_dir( recipe_name), self._sources_scripts_dir(tempdir)) - # Verify /etc/meson-example.conf is still owned by the cmake-example user + # Verify /etc/cmake-example.conf is still owned by the cmake-example user # after the install and deploy scripts updated the file self._verify_conf_file(qemu, conf_file, example_exe, example_exe) From patchwork Wed Mar 18 22:36:13 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83785 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 41A0A1088E58 for ; Wed, 18 Mar 2026 22:38:34 +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.27168.1773873511002726908 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=fo9N32Kf; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.228, mailfrom: fm-1329275-2026031822382843071c8bce00020784-mey6hz@rts-flowmailer.siemens.com) Received: by mta-65-228.siemens.flowmailer.net with ESMTPSA id 2026031822382843071c8bce00020784 for ; Wed, 18 Mar 2026 23:38:28 +0100 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=MEqyzXeEN/RVE6RVpBJ+VJEtqbkv6Iaw6xGtaYq4BeY=; b=fo9N32Kf5rsMiA5fjF9LV5nuqvOnfXKNI+wcQkP2BgSWqKS/aQClXWKlrvSDHjDpufCKD2 0w/TFh3T0R3FJVndX/FAvFwOtMBonMv8qv1KCKcOhfhn36jCjJgQcZPr2Zl8DbHxvfoO621v uwVHVar7bh3xiid7wG3jraG4m79e6aTJce4lkvnHKUYJ6UuGTI5RyroF81qT3B2V9KUKJ+U1 OoCKiexofHGw0/xhSFftNBDL7n+sMI/E0yabvxtzbANEEOsiqyNiZlteK0OyGb2bm+WPHxRB VISYZ2saMkiGksMfhxkF2PckUbb3AGRAAwBibjuCr+il7Zyy5aaWzlEg==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 5/9] devtool: ide-sdk: use TOOLCHAIN not TCOVERRIDE Date: Wed, 18 Mar 2026 23:36:13 +0100 Message-ID: <20260318223736.3414885-6-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:34 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233439 From: Adrian Freihofer Looks like TOOLCHAIN is the correct variable to determine the toolchain used by a recipe, not TCOVERRIDE. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_sdk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 9bccd76f0c..07f5552758 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -416,7 +416,7 @@ class RecipeModified: self.staging_incdir = None self.strip_cmd = None self.target_arch = None - self.tcoverride = None + self.toolchain = None self.topdir = None self.workdir = None # Service management @@ -502,7 +502,7 @@ class RecipeModified: recipe_d.getVar('STAGING_INCDIR')) self.strip_cmd = recipe_d.getVar('STRIP') self.target_arch = recipe_d.getVar('TARGET_ARCH') - self.tcoverride = recipe_d.getVar('TCOVERRIDE') + self.toolchain = recipe_d.getVar('TOOLCHAIN') self.topdir = recipe_d.getVar('TOPDIR') self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR')) @@ -673,7 +673,7 @@ class RecipeModified: @property def gdb_pretty_print_scripts(self): if self._gdb_pretty_print_scripts is None: - if self.tcoverride == "toolchain-gcc": + if self.toolchain == "gcc": gcc_python_helpers_pattern = os.path.join(self.recipe_sysroot, "usr", "share", "gcc-*", "python") gcc_python_helpers_dirs = glob.glob(gcc_python_helpers_pattern) if gcc_python_helpers_dirs: From patchwork Wed Mar 18 22:36:14 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83787 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 676A61088E4E for ; Wed, 18 Mar 2026 22:38:33 +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.27166.1773873511002067643 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=HSqQYWOt; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.225, mailfrom: fm-1329275-20260318223828daf13e07ff00020751-4qgyg5@rts-flowmailer.siemens.com) Received: by mta-65-225.siemens.flowmailer.net with ESMTPSA id 20260318223828daf13e07ff00020751 for ; Wed, 18 Mar 2026 23:38:28 +0100 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=cJUIl+9uKq8LAXHgVcuDzz4lfApu4SNO4YBk0RRSK1c=; b=HSqQYWOth0LpCKWQm6cCEv2UgsmDdKja3UhkpOTZz54M5bc2d+F/RIP5ZwNlg3fE3p0sgv AKnOLcZdEOeT9GJ8vQPGg5VfumVt12YKEcZO36nUjJqrzy4OmF4KKj/5M0EDoLzVsbaf7rqm YQJP1BZ5Gh6Bf60tI6XYXRLYMTn1UukiO4XwOpOQH8VOAdo41V1XRJNEldHWwadz7pAsKt5Z v7TjFRH2maDPCaZiFo08nPfYCWzqw6e40u2e09JwneSbhctP4zfFHZqh5CDLY7Q30+dUa4QJ wGuypdz8j1n7DsxpiwcWS/6Ahzge0am1x0GU/l6qfgbIslb8BySZnP9g==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 6/9] devtool: ide-sdk debugger back-end abstraction Date: Wed, 18 Mar 2026 23:36:14 +0100 Message-ID: <20260318223736.3414885-7-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:33 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233445 From: Adrian Freihofer This is a refactoring of 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. 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 | 195 +++++++++++--------- scripts/lib/devtool/ide_plugins/ide_code.py | 38 ++-- scripts/lib/devtool/ide_plugins/ide_none.py | 26 +-- scripts/lib/devtool/ide_sdk.py | 21 ++- 4 files changed, 154 insertions(+), 126 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index eaf88e78cd..8c41afc640 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,7 +31,7 @@ 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 @@ -42,104 +42,48 @@ class GdbServerModes(Enum): 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.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.port = DebuggerCrossConfig._port_next + DebuggerCrossConfig._port_next += 1 + self.id_pretty = "%d_%s" % (self.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 - @property - def gdbinit_dir(self): - return os.path.join(self.script_dir, 'gdbinit') + def server_script_file(self, mode): + return 'gdbserver_' + self.id_pretty_mode(mode) - def gdbserver_script_file(self, gdbserver_mode): - return 'gdbserver_' + self.id_pretty_mode(gdbserver_mode) + def server_script(self, mode): + return os.path.join(self.script_dir, self.server_script_file(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) - - @property - def gdb_script(self): - return os.path.join( - self.script_dir, 'gdb_' + self.id_pretty) - - # 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_pid_file(self, gdbserver_mode): - return os.path.join(self.gdbserver_tmp_dir(gdbserver_mode), 'gdbserver.pid') - - def gdbserver_log_file(self, gdbserver_mode): - return os.path.join(self.gdbserver_tmp_dir(gdbserver_mode), 'gdbserver.log') - - def _target_gdbserver_start_cmd(self, gdbserver_mode): - """Get the ssh command to start gdbserver on the target device - - 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: - gdbserver_cmd_start = "%s --once :%s %s" % ( - self.gdb_cross.gdbserver_path, self.gdbserver_port, self.binary.binary_path) - elif gdbserver_mode == GdbServerModes.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, - 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) - 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) - else: - raise DevtoolError("Unsupported gdbserver mode: %s" % gdbserver_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 argument helpers + def _target_ssh_args(self): ssh_args = [] if self.gdb_cross.target_device.ssh_port: ssh_args += ["-p", self.gdb_cross.target_device.ssh_port] @@ -149,17 +93,94 @@ class GdbCrossConfig: 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: + 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 != GdbServerModes.ATTACH: modes.append(GdbServerModes.ATTACH) return modes def initialize(self): - """Interface function to initialize the gdb config generation""" + """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=GdbServerModes.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') + + @property + def gdbinit(self): + 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) + + # gdbserver files on the target + def _gdbserver_tmp_dir(self, mode): + return os.path.join('/tmp', 'gdbserver_%s' % self.id_pretty_mode(mode)) + + def _gdbserver_pid_file(self, mode): + return os.path.join(self._gdbserver_tmp_dir(mode), 'gdbserver.pid') + + def _gdbserver_log_file(self, mode): + return os.path.join(self._gdbserver_tmp_dir(mode), 'gdbserver.log') + + def _target_start_cmd(self, gdbserver_mode): + """SSH command to start gdbserver on the target device. + + Returns something like: + "\"/bin/sh -c '/usr/bin/gdbserver --once :1234 /usr/bin/cmake-example'\"" + """ + if gdbserver_mode == GdbServerModes.ONCE: + gdbserver_cmd_start = "%s --once :%s %s" % ( + self.gdb_cross.gdbserver_path, self.port, self.binary.binary_path) + elif gdbserver_mode == GdbServerModes.ATTACH: + pid_command = self.binary.pid_command + if pid_command: + gdbserver_cmd_start = "%s --attach :%s \\$(%s)" % ( + self.gdb_cross.gdbserver_path, + self.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) + gdbserver_cmd_start += "%s --multi :%s > %s 2>&1 & " % ( + self.gdb_cross.gdbserver_path, self.port, self._gdbserver_log_file(gdbserver_mode)) + gdbserver_cmd_start += "echo \\$! > %s;" % self._gdbserver_pid_file(gdbserver_mode) + else: + raise DevtoolError("Unsupported gdbserver mode: %s" % gdbserver_mode) + return "\"/bin/sh -c '" + gdbserver_cmd_start + "'\"" + + 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.port + + class IdeBase: diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 603d3cecf3..7fe5a40eb1 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, DebuggerCrossConfig, GdbCrossConfig, GdbServerModes, 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=GdbServerModes.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 @@ -289,6 +286,11 @@ class IdeVSCode(IdeBase): self.dot_code_dir(modified_recipe), prop_file, properties_dicts) def vscode_launch_bin_dbg(self, gdb_cross_config, gdbserver_mode): + """Dispatch to the GDB launch config generator.""" + return self._vscode_launch_bin_dbg_gdb(gdb_cross_config, gdbserver_mode) + + def _vscode_launch_bin_dbg_gdb(self, gdb_cross_config, gdbserver_mode): + """Generate a cppdbg (GDB) launch configuration entry for launch.json.""" modified_recipe = gdb_cross_config.modified_recipe launch_config = { @@ -303,7 +305,7 @@ class IdeVSCode(IdeBase): "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) + "miDebuggerServerAddress": "%s:%d" % (modified_recipe.gdb_cross.host, gdb_cross_config.port) } # Search for header files in recipe-sysroot. @@ -384,7 +386,7 @@ 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(): + for gdbserver_mode in gdb_cross_config.server_modes(): configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config, gdbserver_mode)) launch_dict = { "version": "0.2.0", @@ -415,7 +417,7 @@ class IdeVSCode(IdeBase): for gdb_cross_config in self.gdb_cross_configs: if gdb_cross_config.modified_recipe is not modified_recipe: continue - for gdbserver_mode in gdb_cross_config.gdbserver_modes(): + for gdbserver_mode in gdb_cross_config.server_modes(): new_task = { "label": gdb_cross_config.id_pretty_mode(gdbserver_mode), "type": "shell", @@ -633,7 +635,7 @@ class IdeVSCode(IdeBase): for gdb_cross_config in self.gdb_cross_configs: if gdb_cross_config.modified_recipe is not modified_recipe: continue - for gdbserver_mode in gdb_cross_config.gdbserver_modes(): + for gdbserver_mode in gdb_cross_config.server_modes(): new_task = { "label": gdb_cross_config.id_pretty(gdbserver_mode), "type": "shell", @@ -668,7 +670,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) diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py index ed96afa33c..781e832ee8 100644 --- a/scripts/lib/devtool/ide_plugins/ide_none.py +++ b/scripts/lib/devtool/ide_plugins/ide_none.py @@ -16,17 +16,17 @@ logger = logging.getLogger('devtool') class GdbCrossConfigNone(GdbCrossConfig): def __init__(self, image_recipe, modified_recipe, binary, - gdbserver_default_mode=GdbServerModes.MULTI): + default_mode=GdbServerModes.MULTI): super().__init__(image_recipe, modified_recipe, binary, - gdbserver_default_mode) + default_mode) def _target_gdbserver_stop_cmd(self, gdbserver_mode): """Kill a gdbserver process""" # This is the usual behavior: gdbserver is stopped on demand if gdbserver_mode == GdbServerModes.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(gdbserver_mode), self._gdbserver_pid_file(gdbserver_mode)) + gdbserver_cmd_stop += " rm -rf %s" % self._gdbserver_tmp_dir(gdbserver_mode) # This is unexpected since gdbserver should terminate after each debug session # Just kill all gdbserver instances to keep it simple else: @@ -36,11 +36,11 @@ class GdbCrossConfigNone(GdbCrossConfig): def _gen_gdbserver_start_script(self, gdbserver_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_mode = self.default_mode + gdbserver_cmd_start = self._target_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())) + " ".join(self._target_ssh_args())) gdbserver_cmd = ['#!/bin/sh'] gdbserver_cmd.append('if [ "$1" = "stop" ]; then') gdbserver_cmd.append(' shift') @@ -48,19 +48,19 @@ 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(gdbserver_mode), gdbserver_cmd, True) def _gen_gdbinit_config(self, gdbserver_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 + gdbserver_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 + target_help = '# gdbserver --multi :%d' % self.port remote_cmd = 'target extended-remote' else: target_help = '# gdbserver :%d %s' % ( - self.gdbserver_port, self.binary) + self.port, self.binary) remote_cmd = 'target remote' gdbinit_lines.append('# On the remote target:') gdbinit_lines.append(target_help) @@ -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.gdb_cross.host, self.port)) gdbinit_lines.append('set remote exec-file ' + self.binary.binary_path) gdbinit_lines.append('run ' + self.binary.binary_path) @@ -127,7 +127,7 @@ class GdbCrossConfigNone(GdbCrossConfig): def initialize(self): self._gen_gdbserver_start_script() - if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH: + if self.binary.runs_as_service and self.default_mode != GdbServerModes.ATTACH: self._gen_gdbserver_start_script(GdbServerModes.ATTACH) self._gen_gdbinit_config() self._gen_gdb_start_script() diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 07f5552758..76cbccf618 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -1159,21 +1159,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.gdb_cross = debuggers[debugger_key] finally: tinfoil.shutdown() @@ -1191,7 +1195,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 Wed Mar 18 22:36:15 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83788 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 919D81088E5A for ; Wed, 18 Mar 2026 22:38:34 +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.msgproc01-g2.27343.1773873511005304171 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=S/+OFosT; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.228, mailfrom: fm-1329275-2026031822382805ac13a2f800020711-nejkji@rts-flowmailer.siemens.com) Received: by mta-65-228.siemens.flowmailer.net with ESMTPSA id 2026031822382805ac13a2f800020711 for ; Wed, 18 Mar 2026 23:38:28 +0100 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=+qWFJLbmdyfC5DaqGBtB/XO0H5NT6fdPVUNm1h2qds8=; b=S/+OFosT5O0l7PtkG0ly93srElvqx6KNrAt61sWTekTGM5g3UHQGmUlxWZOcEJFvd/CKCa mUGH78PN4HxsEiJCdPfLbYqunBrH3ewJsqZlG4E0c7s24n+feYOGTYapvda26KgpVN/2uM0x PtaSjwQxUpgkzZemCBOR4EOlRJ/XYh5gewH6b3Z0OhpmTkCu2KXAtMG3jvH/JRMafcVXCNrp wE75Dqjun8UXHbCYUwDs8sAC2tr92GjvXcdL6Rm0aYEI+UaMUyg4zGR7f3xN00Qla2gGwM8M RgRLhXBCxrdAJnucGiTUytbURizqfzYCec64XEZVMMka1aVdlMThqnLQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 7/9] devtool: ide-sdk add LLDB support for clang toolchain Date: Wed, 18 Mar 2026 23:36:15 +0100 Message-ID: <20260318223736.3414885-8-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:34 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233441 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 Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/__init__.py | 63 +++++++++++ scripts/lib/devtool/ide_plugins/ide_code.py | 114 +++++++++++++++++++- scripts/lib/devtool/ide_sdk.py | 61 ++++++++++- 3 files changed, 229 insertions(+), 9 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index 8c41afc640..d7d06567ee 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -181,6 +181,69 @@ class GdbCrossConfig(DebuggerCrossConfig): return "\"kill \\$(pgrep -o -f 'gdbserver --attach :%s') 2>/dev/null || true\"" % self.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=GdbServerModes.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.gdb_cross.gdbserver_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 == GdbServerModes.ONCE: + cmd = "cd /tmp && %s platform --one-shot --server --listen *:%s" % ( + lldb_server, self.port) + elif mode == GdbServerModes.MULTI: + 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 = "test -f %s && exit 0; " % pid_file + cmd += "mkdir -p %s; " % tmp_dir + cmd += "cd %s; " % tmp_dir + cmd += "%s platform --server --listen *:%s > %s 2>&1 & " % ( + lldb_server, self.port, log_file) + cmd += "echo \\$! > %s;" % pid_file + 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(GdbServerModes.MULTI) + tmp_dir = self._lldb_server_tmp_dir(GdbServerModes.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: diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 7fe5a40eb1..1b8434ab1c 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, DebuggerCrossConfig, GdbCrossConfig, GdbServerModes, get_devtool_deploy_opts +from devtool.ide_plugins import BuildTool, IdeBase, DebuggerCrossConfig, GdbCrossConfig, GdbServerModes, 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=GdbServerModes.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 @@ -243,6 +265,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: @@ -286,7 +312,9 @@ class IdeVSCode(IdeBase): self.dot_code_dir(modified_recipe), prop_file, properties_dicts) def vscode_launch_bin_dbg(self, gdb_cross_config, gdbserver_mode): - """Dispatch to the GDB launch config generator.""" + """Dispatch to the GDB or LLDB launch config generator.""" + if isinstance(gdb_cross_config, LldbServerConfig): + return self._vscode_launch_bin_dbg_lldb(gdb_cross_config, gdbserver_mode) return self._vscode_launch_bin_dbg_gdb(gdb_cross_config, gdbserver_mode) def _vscode_launch_bin_dbg_gdb(self, gdb_cross_config, gdbserver_mode): @@ -373,6 +401,80 @@ class IdeVSCode(IdeBase): return launch_config + def _vscode_launch_bin_dbg_lldb(self, lldb_config, gdbserver_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. + """ + modified_recipe = lldb_config.modified_recipe + gdb_cross = modified_recipe.gdb_cross + + init_commands = [ + "platform select remote-linux", + "platform connect connect://%s:%d" % (gdb_cross.host, lldb_config.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 \"\"", + ] + # Point LLDB at the installed files in ${D} so it resolves shared + # library paths automatically (equivalent of GDB's 'set sysroot'). + # target.sysroot is not a valid LLDB setting; use the module search + # path substitution instead, which requires a target to exist and + # therefore must run in preRunCommands, not initCommands. + pre_run_commands = [ + "target modules search-paths add / %s" % modified_recipe.d, + ] + + # 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) + else: + logger.warning( + "Cannot setup debug symbols configuration for LLDB. " + "IMAGE_GEN_DEBUGFS is not enabled.") + + launch_config = { + "name": lldb_config.id_pretty_mode(gdbserver_mode), + "type": "lldb", + "request": "launch", + "program": lldb_config.binary.binary_host_path, + "stopOnEntry": False, + "cwd": "/tmp", + "preLaunchTask": lldb_config.id_pretty_mode(gdbserver_mode), + "initCommands": init_commands, + "preRunCommands": pre_run_commands, + } + if source_map: + launch_config["sourceMap"] = source_map + + return launch_config + def vscode_launch(self, args, modified_recipe): """GDB launch configurations for user-space binaries. @@ -682,8 +784,12 @@ class IdeVSCode(IdeBase): self.vscode_extensions(modified_recipe) self.vscode_c_cpp_properties(modified_recipe) if args.target: - self.initialize_gdb_cross_configs( - image_recipe, modified_recipe, GdbCrossConfigVSCode) + if modified_recipe.toolchain == 'clang': + self.initialize_gdb_cross_configs( + image_recipe, modified_recipe, LldbServerConfigVSCode) + else: + self.initialize_gdb_cross_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 76cbccf618..7c461e6b0e 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 gdbserver_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): @@ -1172,8 +1212,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 @@ -1197,12 +1240,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 Wed Mar 18 22:36:16 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83782 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 55BA01088E4C for ; Wed, 18 Mar 2026 22:38:33 +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.27342.1773873511005143877 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=PiAfP/cT; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-202603182238285d9556a18a000207bf-n4olkx@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 202603182238285d9556a18a000207bf for ; Wed, 18 Mar 2026 23:38:28 +0100 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=LLhmi74+3+9OdxR3EDIbH2tSUAjiOkmRa6t6XFZAXco=; b=PiAfP/cT88xYifjuVeWVjZFzBjdhTq5hXnZZpR29cfOhqahX+QEuo2mpOExRSS1ukkFWTL 26cDo86y9J9Yt7aQ04sic3DyjxZhOXIAFVUG6W1VHaaUdtgfB5ug2ZPtJseA9cocyRLzW+Tv h7mUv2ecQ1P8C+piB2wZiwBEC9WbwFuACcJQad/WHcusGdrHbthGFLUH1C2LMOf5gmhead2t KbHR3Je8uzOsTu+G49AFn0XR2zUWyKj/aIo4NDfrRmNny6mhpriAh2fhS/x12zQVuwwSHo0q aLU+wfODbV1LTzUmXIj/4MpgyJGhE6grF2ToBdRZ8b5RRRZgSECSc79Q==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 8/9] meta-selftest: refactor cpp examples into .inc files and add clang variants Date: Wed, 18 Mar 2026 23:36:16 +0100 Message-ID: <20260318223736.3414885-9-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:33 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233444 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 Wed Mar 18 22:36:17 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Freihofer, Adrian" X-Patchwork-Id: 83784 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 BEEE11088E52 for ; Wed, 18 Mar 2026 22:38:33 +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.27167.1773873511002234684 for ; Wed, 18 Mar 2026 15:38:32 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=TwthB4hg; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.228, mailfrom: fm-1329275-2026031822382869889572b2000207b3-b1wmjn@rts-flowmailer.siemens.com) Received: by mta-64-228.siemens.flowmailer.net with ESMTPSA id 2026031822382869889572b2000207b3 for ; Wed, 18 Mar 2026 23:38:28 +0100 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=vMSEYR+o28e2bg1NR/2XI+R7an3qe4fcuR1VxRCQqRQ=; b=TwthB4hg9JLqG3i9kKLnoa7wbUXjpiMUSdOWAbVaX+4c7YLeqijYjS1jb8uD71pEcUd02t hU8tibojttqYfaMsqMMeypaXW7kSPcr93O7D1ou8SzQQZ91HqCBoIecs6kVkUQ3NdZk9G8uW T5Ovpq+zByZ5RilFyjBz26Sk0tKZuBH0+3pcmmMWOZgGoiZggDgCyk1iPxhcGOBTbPOWcd1v s73qWhM+iUFRLqijC1ETqY5HTntTEK8Uuz3qT8rRPL0FIL3Nd03fJQRdmnNzXutliIpbn053 yYMKT84WerEC8UIKXwbnOAhtCqRH5U1EtqEdMGCa93CqnWkzLaeN2sBw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 9/9] oe-selftest: devtool ide-sdk: add clang/LLDB test Date: Wed, 18 Mar 2026 23:36:17 +0100 Message-ID: <20260318223736.3414885-10-adrian.freihofer@siemens.com> In-Reply-To: <20260318223736.3414885-1-adrian.freihofer@siemens.com> References: <20260318223736.3414885-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 ; Wed, 18 Mar 2026 22:38:33 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/233437 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 | 196 +++++++++++++++++++++++- 1 file changed, 192 insertions(+), 4 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 6c6f22a667..2cd03e68d6 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2590,13 +2590,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 @@ -2938,7 +2940,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(), @@ -3612,6 +3616,190 @@ 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 program path points into the image directory + program = config.get("program", "") + self.assertTrue(program.startswith("/"), + f"Configuration '{config_name}' program should be an absolute path") + self.assertIn("/image/", program, + f"Configuration '{config_name}' program should be in 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 _lldb_server_debugging_once(self, tempdir, qemu, recipe_name, 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 minimal debugging + session and checks that the expected magic string is 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('"'): + ssh_cmd[-1] = ssh_cmd[-1][1:-1] + + # 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, set a breakpoint on main, run + program = lldb_config["program"] + source_map = lldb_config.get("sourceMap", {}) + + pre_run_commands = lldb_config.get("preRunCommands", []) + + lldb_batch = [lldb_binary, "--batch"] + for cmd in init_commands: + lldb_batch += ["-o", cmd] + lldb_batch += ["-o", "target create %s" % program] + for k, v in source_map.items(): + v_resolved = v.replace("${workspaceFolder}", tempdir) + lldb_batch += ["-o", "settings set target.source-map %s %s" % (k, v_resolved)] + for cmd in pre_run_commands: + lldb_batch += ["-o", cmd] + lldb_batch += [ + "-o", "b main", + "-o", "run", + "-o", "continue", + "-o", "exit" + ] + r = runCmd(lldb_batch, output_log=self._cmd_logger) + self.assertEqual(r.status, 0, "lldb batch session failed: %s" % r.output) + self.assertIn("stop reason = breakpoint", r.output, + "lldb should have stopped at main breakpoint") + + @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 + 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) + runCmd(install_deploy_cmd, output_log=self._cmd_logger) + + self._lldb_server_debugging_once(tempdir, qemu, recipe_name, + DevtoolIdeSdkTests.MAGIC_STRING_ORIG) + def test_devtool_ide_sdk_plugins(self): """Test that devtool ide-sdk can use plugins from other layers."""