Openembedded Core Discussions
 help / color / mirror / Atom feed
* [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release
@ 2026-09-13 20:05 AdrianF
  2026-09-13 20:05 ` [PATCH 1/6] oe-selftest: devtool update: recipe drop dead code AdrianF
                   ` (6 more replies)
  0 siblings, 7 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

- Fix: opt-out of automatic IMAGE_INSTALL for recipes providing
  virtual/kernel or virtual/bootloader (deployed via do_deploy, not
  installed into the rootfs).
- Feature: --ide now accepts a comma-separated plugin list
  (--ide code,none), generating multiple IDE configs from one build -
  this is what enables the test refactoring below.
- Test overhaul: consolidate the GCC/Clang, ide=code/none and NFS/slirp
  oe-selftest variants into one shared flow per toolchain, cutting qemu
  boots/image builds while keeping the same coverage, plus dead-code
  cleanup.

Adrian Freihofer (6):
  oe-selftest: devtool update: recipe drop dead code
  devtool: ide-sdk: support opt-out from auto IMAGE_INSTALL
  devtool: ide-sdk: add initialize_modified_recipe() plugin hook
  devtool: ide-sdk: allow --ide to select multiple plugins
  oe-selftest: devtool ide-sdk: consolidate GCC/Clang ide-sdk tests
  devtool: ide-sdk: trim _add_broken_srctree_prefix_map docstring

 meta/lib/oeqa/selftest/cases/devtool.py     | 1028 ++++++++-----------
 scripts/lib/devtool/ide_plugins/__init__.py |    4 +
 scripts/lib/devtool/ide_sdk.py              |  117 +--
 3 files changed, 475 insertions(+), 674 deletions(-)

-- 
2.55.0



^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH 1/6] oe-selftest: devtool update: recipe drop dead code
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
@ 2026-09-13 20:05 ` AdrianF
  2026-09-13 20:05 ` [PATCH 2/6] devtool: ide-sdk: support opt-out from auto IMAGE_INSTALL AdrianF
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

Drop _test_devtool_update_recipe_local_files_2 which is dead code since
commit ce8190c519 "devtool: Drop oe-local-files and simplify", 2024-05-21.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 meta/lib/oeqa/selftest/cases/devtool.py | 40 -------------------------
 1 file changed, 40 deletions(-)

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index ec257f9859..59d9de26a9 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -1685,46 +1685,6 @@ class DevtoolUpdateTests(DevtoolBase):
         self.assertExists(local_file, 'File makedevs.c not created')
         self.assertExists(patchfile, 'File new_local not created')
 
-    def _test_devtool_update_recipe_local_files_2(self):
-        """Check local source files support when editing local files in Git"""
-        testrecipe = 'devtool-test-local'
-        recipefile = get_bb_var('FILE', testrecipe)
-        recipedir = os.path.dirname(recipefile)
-        result = runCmd('git status --porcelain .', cwd=recipedir)
-        if result.output.strip():
-            self.fail('Recipe directory for %s contains uncommitted changes' % testrecipe)
-        # Setup srctree for modifying the recipe
-        tempdir = tempfile.mkdtemp(prefix='devtoolqa')
-        self.track_for_cleanup(tempdir)
-        self.track_for_cleanup(self.workspacedir)
-        self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
-        result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
-        # Check git repo
-        self._check_src_repo(tempdir)
-        # Edit / commit local sources
-        runCmd('echo "# Foobar" >> file1', cwd=tempdir)
-        runCmd('git commit -am "Edit existing file"', cwd=tempdir)
-        runCmd('git rm file2', cwd=tempdir)
-        runCmd('git commit -m"Remove file"', cwd=tempdir)
-        runCmd('echo "Foo" > new-local', cwd=tempdir)
-        runCmd('git add new-local', cwd=tempdir)
-        runCmd('git commit -m "Add new local file"', cwd=tempdir)
-        runCmd('echo "Gar" > new-file', cwd=tempdir)
-        runCmd('git add new-file', cwd=tempdir)
-        runCmd('git commit -m "Add new file"', cwd=tempdir)
-        self.add_command_to_tearDown('cd %s; git clean -fd .; git checkout .' %
-                                     os.path.dirname(recipefile))
-        # Checkout unmodified file to working copy -> devtool should still pick
-        # the modified version from HEAD
-        runCmd('git checkout HEAD^ -- file1', cwd=tempdir)
-        runCmd('devtool update-recipe %s' % testrecipe)
-        expected_status = [(' M', '.*/%s$' % os.path.basename(recipefile)),
-                           (' M', '.*/file1$'),
-                           (' D', '.*/file2$'),
-                           ('??', '.*/new-local$'),
-                           ('??', '.*/0001-Add-new-file.patch$')]
-        self._check_repo_status(os.path.dirname(recipefile), expected_status)
-
     def test_devtool_update_recipe_with_gitignore(self):
         # First, modify the recipe
         testrecipe = 'devtool-test-ignored'
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 2/6] devtool: ide-sdk: support opt-out from auto IMAGE_INSTALL
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
  2026-09-13 20:05 ` [PATCH 1/6] oe-selftest: devtool update: recipe drop dead code AdrianF
@ 2026-09-13 20:05 ` AdrianF
  2026-09-13 20:05 ` [PATCH 3/6] devtool: ide-sdk: add initialize_modified_recipe() plugin hook AdrianF
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

Modified-mode recipes are automatically added to the image's
IMAGE_INSTALL, but recipes providing virtual/kernel or
virtual/bootloader (e.g. the kernel or u-boot) are deployed via
do_deploy into DEPLOY_DIR_IMAGE, not installed into the rootfs, so
adding them to IMAGE_INSTALL or attaching a userspace gdbserver to
them does not make sense.

Auto-detect these recipes via PROVIDES and skip both IMAGE_INSTALL
and gdbserver/lldb-server for them. IDE_SDK_AUTO_IMAGE_INSTALL allows
overriding this per recipe, in either direction.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_sdk.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 21442ce83b..14271ae871 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -406,6 +406,8 @@ class RecipeImage:
         if wants_lldb_server and 'lldb-server' not in self.base_image_install:
             lines.append('IMAGE_INSTALL:append = " lldb-server"')
         for r in recipes_modified:
+            if not r.wants_image_install:
+                continue
             if r.name not in self.base_image_install:
                 lines.append('IMAGE_INSTALL:append = " %s"' % r.name)
             if r.has_ptest and (r.name + '-ptest') not in self.base_image_install:
@@ -754,6 +756,11 @@ class RecipeModified:
 
     MARKER = '# devtool ide-sdk: clangd toolchain support'
 
+    # PROVIDES of recipes that are deployed via do_deploy (e.g. to
+    # DEPLOY_DIR_IMAGE) rather than installed into the rootfs, so adding them
+    # to IMAGE_INSTALL or attaching a userspace gdbserver does not make sense.
+    NON_IMAGE_INSTALL_PROVIDES = {'virtual/kernel', 'virtual/bootloader'}
+
     def __init__(self, name, orig_bbappend_content=None):
         self.name = name
         self.bootstrap_tasks = [name + ':do_install']
@@ -783,6 +790,7 @@ class RecipeModified:
         self.package_debug_split_style = None
         self.path = None
         self.pn = None
+        self.provides = []
         self.recipe_id = None
         self.recipe_sysroot = None
         self.recipe_sysroot_native = None
@@ -808,6 +816,8 @@ class RecipeModified:
         self.build_tool = BuildTool.UNDEFINED
         # Whether this recipe benefits from gdbserver and rootfs-dbg in the image.
         self.wants_gdbserver = True
+        # Whether this recipe should be added to IMAGE_INSTALL automatically
+        self.wants_image_install = True
         # Whether to warn when DEBUG_BUILD is not set.  Kernel modules are built
         # by the kernel's build system and DEBUG_BUILD does not influence them.
         self.wants_debug_build = True
@@ -942,6 +952,14 @@ class RecipeModified:
             self.packages_files[package] = recipe_d.getVar('FILES:' + package) or ''
         self.path = recipe_d.getVar('PATH')
         self.pn = recipe_d.getVar('PN')
+        self.provides = (recipe_d.getVar('PROVIDES') or '').split()
+        if self.NON_IMAGE_INSTALL_PROVIDES.intersection(self.provides):
+            self.wants_image_install = False
+            self.wants_gdbserver = False
+        ide_sdk_auto_image_install = recipe_d.getVar('IDE_SDK_AUTO_IMAGE_INSTALL')
+        if ide_sdk_auto_image_install is not None:
+            self.wants_image_install = bb.utils.to_boolean(
+                ide_sdk_auto_image_install)
         self.recipe_sysroot = os.path.realpath(
             recipe_d.getVar('RECIPE_SYSROOT'))
         self.recipe_sysroot_native = os.path.realpath(
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 3/6] devtool: ide-sdk: add initialize_modified_recipe() plugin hook
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
  2026-09-13 20:05 ` [PATCH 1/6] oe-selftest: devtool update: recipe drop dead code AdrianF
  2026-09-13 20:05 ` [PATCH 2/6] devtool: ide-sdk: support opt-out from auto IMAGE_INSTALL AdrianF
@ 2026-09-13 20:05 ` AdrianF
  2026-09-13 20:05 ` [PATCH 4/6] devtool: ide-sdk: allow --ide to select multiple plugins AdrianF
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

Add IdeBase.initialize_modified_recipe() plugin hook which allows IDE
plugins to run code at recipe initialization time within the shared
tinfoil session.

Default implementation is a no-op, so existing IDE plugins are
unaffected.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_plugins/__init__.py | 4 ++++
 scripts/lib/devtool/ide_sdk.py              | 4 ++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py
index bc0b2ca9b7..333ec9bac8 100644
--- a/scripts/lib/devtool/ide_plugins/__init__.py
+++ b/scripts/lib/devtool/ide_plugins/__init__.py
@@ -353,6 +353,10 @@ class IdeBase:
         logger.warn("Shared sysroot mode is not supported for IDE %s" %
                     self.ide_name)
 
+    def initialize_modified_recipe(self, config, tinfoil, recipe_modified):
+        """Hook called once per modified recipe, inside the shared tinfoil session"""
+        pass
+
     def setup_modified_recipe(self, args, image_recipe, modified_recipe):
         logger.warn("Modified recipe mode is not supported for IDE %s" %
                     self.ide_name)
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 14271ae871..d47906700b 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -1746,6 +1746,7 @@ def ide_setup(args, config, basepath, workspace):
         config, args.recipenames)
     orig_recipe_bbappend_contents = RecipeModified.strip_bbappend_sections(
         config, args.recipenames)
+    ide = ide_plugins[args.ide]()
     tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
     try:
         # define mode depending on recipes which need to be processed
@@ -1868,6 +1869,7 @@ def ide_setup(args, config, basepath, workspace):
                 recipe_modified = RecipeModified(
                     recipe_name, orig_recipe_bbappend_contents.get(recipe_name))
                 recipe_modified.initialize(config, workspace, tinfoil)
+                ide.initialize_modified_recipe(config, tinfoil, recipe_modified)
                 bootstrap_tasks += recipe_modified.bootstrap_tasks
                 recipes_modified.append(recipe_modified)
 
@@ -1926,8 +1928,6 @@ def ide_setup(args, config, basepath, workspace):
                 config.init_path, basepath,
                 bb_cmd + ' '.join(bootstrap_tasks_late), watch=True)
 
-    # Instantiate the active IDE plugin
-    ide = ide_plugins[args.ide]()
     if args.mode == DevtoolIdeMode.shared:
         ide.setup_shared_sysroots(shared_env)
     elif args.mode == DevtoolIdeMode.modified:
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 4/6] devtool: ide-sdk: allow --ide to select multiple plugins
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
                   ` (2 preceding siblings ...)
  2026-09-13 20:05 ` [PATCH 3/6] devtool: ide-sdk: add initialize_modified_recipe() plugin hook AdrianF
@ 2026-09-13 20:05 ` AdrianF
  2026-09-13 20:05 ` [PATCH 5/6] oe-selftest: devtool ide-sdk: consolidate GCC/Clang ide-sdk tests AdrianF
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

Accept a comma separated list of IDE plugin names for --ide (e.g.
--ide code,none) instead of a single name, so the configuration for
several IDEs/tools can be generated in one invocation.

This can be useful when working with multiple IDEs or tools
simultaneously, as it allows for a more streamlined setup process.

It also allows to optimize the oe-selftest process to build once and
test across multiple IDEs rather than building and starting a Qemu
separately for each one.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_sdk.py | 29 ++++++++++++++++++++++-------
 1 file changed, 22 insertions(+), 7 deletions(-)

diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index d47906700b..44e11ce517 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -1746,7 +1746,7 @@ def ide_setup(args, config, basepath, workspace):
         config, args.recipenames)
     orig_recipe_bbappend_contents = RecipeModified.strip_bbappend_sections(
         config, args.recipenames)
-    ide = ide_plugins[args.ide]()
+    ides = [ide_plugins[name]() for name in args.ide]
     tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
     try:
         # define mode depending on recipes which need to be processed
@@ -1869,7 +1869,8 @@ def ide_setup(args, config, basepath, workspace):
                 recipe_modified = RecipeModified(
                     recipe_name, orig_recipe_bbappend_contents.get(recipe_name))
                 recipe_modified.initialize(config, workspace, tinfoil)
-                ide.initialize_modified_recipe(config, tinfoil, recipe_modified)
+                for ide in ides:
+                    ide.initialize_modified_recipe(config, tinfoil, recipe_modified)
                 bootstrap_tasks += recipe_modified.bootstrap_tasks
                 recipes_modified.append(recipe_modified)
 
@@ -1929,15 +1930,17 @@ def ide_setup(args, config, basepath, workspace):
                 bb_cmd + ' '.join(bootstrap_tasks_late), watch=True)
 
     if args.mode == DevtoolIdeMode.shared:
-        ide.setup_shared_sysroots(shared_env)
+        for ide in ides:
+            ide.setup_shared_sysroots(shared_env)
     elif args.mode == DevtoolIdeMode.modified:
         for recipe_modified in recipes_modified:
             if recipe_modified.build_tool is BuildTool.CMAKE:
                 recipe_modified.cmake_preset()
             if recipe_modified.build_tool is BuildTool.MESON:
                 recipe_modified.gen_meson_wrapper()
-            ide.setup_modified_recipe(
-                args, recipe_image, recipe_modified)
+            for ide in ides:
+                ide.setup_modified_recipe(
+                    args, recipe_image, recipe_modified)
 
             if recipe_modified.wants_debug_build and recipe_modified.debug_build != '1':
                 logger.warn(
@@ -2043,9 +2046,21 @@ def register_commands(subparsers, context):
         '  To use this tool-chain the environment-* file found in the deploy..image folder needs to be sourced into a shell.\n'
         '  In case of VSCode and cmake the tool-chain is also exposed as a cmake-kit')
     default_ide = list(ide_plugins.keys())[0]
+
+    def ide_list(value):
+        """argparse type: comma separated list of IDE plugin names"""
+        names = value.split(',')
+        for name in names:
+            if name not in ide_plugins:
+                raise ValueError(
+                    "invalid choice: %r (choose from %s)" % (
+                        name, ', '.join(sorted(ide_plugins.keys()))))
+        return names
     parser_ide_sdk.add_argument(
-        '-i', '--ide', choices=ide_plugins.keys(), default=default_ide,
-        help='Setup the configuration for this IDE (default: %s)' % default_ide)
+        '-i', '--ide', type=ide_list, default=[default_ide],
+        metavar='{%s}' % ','.join(ide_plugins.keys()),
+        help='Comma separated list of IDEs to setup the configuration for '
+        '(choices: %s, default: %s)' % (', '.join(ide_plugins.keys()), default_ide))
     parser_ide_sdk.add_argument(
         '-t', '--target', default='root@192.168.7.2',
         help='Live target machine running an ssh server: user@hostname.')
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 5/6] oe-selftest: devtool ide-sdk: consolidate GCC/Clang ide-sdk tests
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
                   ` (3 preceding siblings ...)
  2026-09-13 20:05 ` [PATCH 4/6] devtool: ide-sdk: allow --ide to select multiple plugins AdrianF
@ 2026-09-13 20:05 ` AdrianF
  2026-09-13 20:05 ` [PATCH 6/6] devtool: ide-sdk: trim _add_broken_srctree_prefix_map docstring AdrianF
  2026-09-16  6:48 ` [OE-core] [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release Antonin Godard
  6 siblings, 0 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

Clang support was mostly added as separate commits from GCC's, leaving
DevtoolIdeSdkGccTests and DevtoolIdeSdkClangTests with their own, mostly
duplicated tests: ide=code and ide=none were each verified with a
separate image build/qemu boot, and the NFS/slirp variants ran their own
thin, single-recipe flows on top of that. That added qemu boots and
image builds for little extra coverage.

Now that a single devtool ide-sdk call can generate configs for multiple
IDEs at once, and Clang/LLDB support is on parity with GCC, consolidate
this into one framework function,
DevtoolIdeSdkTests._test_devtool_ide_sdk_code_and_none_qemu(nfs, slirp).
It covers cmake+meson with --ide=code,none in a single image build/qemu
boot per test (cutting the previous per-toolchain builds/boots from 3 to
1 for GCC and 4 to 1 for Clang for the same coverage), and takes nfs/slirp
flags to cover the NFS-root and slirp-networking variants without
duplicating the whole flow per variant. Per-toolchain differences stay
behind hooks (_ide_sdk_package_opts, _verify_cross_debugger,
_verify_recipe_ide_sdk, _verify_nfs_launch_json) overridden by each
subclass; test cases become thin wrappers calling the shared function
with different parameters.

This cuts autobuilder qemu boots/image builds while keeping equivalent
coverage, and brings Clang up to the same NFS/slirp coverage as GCC.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 meta/lib/oeqa/selftest/cases/devtool.py | 988 ++++++++++--------------
 1 file changed, 412 insertions(+), 576 deletions(-)

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 59d9de26a9..84e020cd3d 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -3112,11 +3112,19 @@ class DevtoolIdeSdkTests(DevtoolBase):
     def _sources_workdir_dir(self, src_dir):
         return os.path.realpath(os.path.join(src_dir, 'oe-workdir'))
 
-    def _workspace_gdbinit_dir(self, recipe_name):
-        return os.path.realpath(os.path.join(self.builddir, 'workspace', 'ide-sdk', recipe_name, 'scripts', 'gdbinit'))
-
-    def _sources_gdbinit_dir(self, src_dir):
-        return os.path.realpath(os.path.join(src_dir, 'oe-gdbinit'))
+    def _find_multi_script_port(self, recipe_name, script_prefix, example_exe):
+        """Discover the port assigned to a generated 'multi' mode debug-server
+        script (gdbserver_*/lldb_server_*), since each --ide plugin allocates
+        its own port and it is not necessarily 1234.
+        """
+        binary_pretty = 'usr-bin-' + example_exe
+        pattern = os.path.join(self._workspace_scripts_dir(recipe_name),
+                               '%s_*_%s_multi' % (script_prefix, binary_pretty))
+        matches = glob.glob(pattern)
+        self.assertEqual(len(matches), 1,
+                         'Expected exactly one script matching %s, got: %s' % (pattern, matches))
+        port_str = os.path.basename(matches[0])[len(script_prefix) + 1:-len('_%s_multi' % binary_pretty)]
+        return int(port_str)
 
     def _devtool_ide_sdk_recipe(self, recipe_name, build_file, testimage):
         """Setup a recipe for working with devtool ide-sdk
@@ -3475,10 +3483,210 @@ class DevtoolIdeSdkTests(DevtoolBase):
 
         run_task(task_label, run_main_task)
 
+    def _ide_sdk_package_opts(self):
+        """Extra `devtool ide-sdk --package` options. Override to scope packages."""
+        return ''
+
+    def _verify_cross_debugger(self):
+        """Verify the target toolchain's cross debugger is usable."""
+        raise NotImplementedError
+
+    def _verify_recipe_ide_sdk(self, tempdir, qemu, recipe_name, example_exe, compile_cmd):
+        """Verify one recipe's ide=code/ide=none configs and remote debugging.
+
+        Must be overridden by the toolchain-specific subclass (GDB/GCC vs
+        LLDB/Clang): the generated configs and the debugging protocol differ
+        completely between toolchains but the procedure for verification is equal.
+        """
+        raise NotImplementedError
+
+    def _verify_nfs_launch_json(self, tempdir, nfs_rootfs):
+        """Verify launch.json's debug configurations reference the NFS debug rootfs.
+
+        Must be overridden by the toolchain-specific subclass (GDB/GCC vs
+        LLDB/Clang): the launch.json keys used to point the debugger at the
+        NFS debug rootfs's shared libraries/sources differ between them.
+        """
+        raise NotImplementedError
+
+    def _verify_slirp_bbappend(self, testimage):
+        """Verify update_qb_slirp_opt() wrote the SSH port-forward to the image bbappend."""
+        bbappend = os.path.join(self.workspacedir, 'appends', testimage + '.bbappend')
+        self.assertExists(bbappend, 'Image bbappend not created at %s' % bbappend)
+        with open(bbappend) as f:
+            bbappend_content = f.read()
+        self.assertIn('QB_SLIRP_OPT', bbappend_content,
+                      'QB_SLIRP_OPT not written to image bbappend')
+        self.assertIn('hostfwd=tcp:127.0.0.1:2222-:22', bbappend_content,
+                      'SSH slirp port forward missing from QB_SLIRP_OPT')
+
+    def _verify_nfs_debug_rootfs(self, testimage, nfs):
+        """Verify the NFS debug rootfs was extracted and its runqemu launch helper generated."""
+        nfs_rootfs = os.path.join(self.workspacedir, 'nfs-exports', testimage, nfs)
+        self.assertExists(nfs_rootfs)
+        self.assertExists(nfs_rootfs + '.pseudo_state')
+
+        runqemu_helper = os.path.join(
+            self.workspacedir, 'nfs-exports', testimage, 'runqemu-' + nfs)
+        self.assertExists(runqemu_helper)
+        self.assertTrue(os.access(runqemu_helper, os.X_OK))
+        with open(runqemu_helper) as helper_file:
+            helper = helper_file.read()
+        self.assertIn('exec runqemu ', helper)
+        self.assertIn(nfs_rootfs, helper)
+
+        bbappend = os.path.join(self.workspacedir, 'appends', testimage + '.bbappend')
+        with open(bbappend) as append_file:
+            self.assertRegex(
+                append_file.read(),
+                r'IMAGE_FSTYPES_DEBUGFS(?::append)? = " ?tar"')
+
+        return nfs_rootfs, runqemu_helper
+
+    def _verify_nfs_binary_deployed(self, nfs_rootfs, example_exe):
+        """Verify a recipe's binary was extracted into the NFS debug rootfs."""
+        self.assertExists(os.path.join(nfs_rootfs, 'usr', 'bin', example_exe))
+
+    def _test_devtool_ide_sdk_code_nfs_debug_rootfs(self):
+        """Verify ide-sdk extracts an NFS debug rootfs for VS Code debugging."""
+        recipe_name = self._cmake_recipe_name
+        build_file = "CMakeLists.txt"
+        testimage = "oe-selftest-image"
+
+        self._check_workspace()
+        self._write_bb_config()
+        tempdir = self._devtool_ide_sdk_recipe(
+            recipe_name, build_file, testimage)
+        runCmd('devtool ide-sdk %s %s -c --ide=code --nfs=rootfs-dbg' %
+               (recipe_name, testimage), output_log=self._cmd_logger)
+
+        nfs_rootfs, _ = self._verify_nfs_debug_rootfs(testimage, 'rootfs-dbg')
+        self._verify_nfs_binary_deployed(nfs_rootfs, recipe_name)
+        self._verify_nfs_launch_json(tempdir, nfs_rootfs)
+
+    def _test_devtool_ide_sdk_code_and_none_qemu(self, nfs=False, slirp=False):
+        """Verify devtool ide-sdk --ide=code,none for cmake, meson and the target toolchain.
+
+        Generating the VS Code (ide=code) and generic (ide=none) IDE
+        configurations from a SINGLE devtool ide-sdk invocation and check
+        these configurations support the full development workflow with
+        the supported IDEs (VS Code and generic) and the target toolchain.
+
+        For each recipe (a cmake and a meson variant, see _cmake_recipe_name /
+        _meson_recipe_name / _meson_example_exe) this:
+        - devtool modifies the recipe, builds/boots the image and runs
+          devtool ide-sdk with both --ide=code and --ide=none together
+          (see _ide_sdk_package_opts() for toolchain-specific --package
+          scoping). Passing only the image (not the recipe) to confirm
+          modified recipes are discovered from workspace appends rather
+          than mistaking the image bbappend for one.
+        - Verifies each recipe's ide=code and ide=none configurations,
+          can recompile and run the unit tests on Qemu user mode on the
+          host.
+        - Verifies that the full remote development cycle works with a
+          Qemu system target. Remote debugging, changing the sources and
+          recompiling, deploying again and debugging should be verified.
+
+        nfs: extract and boot an NFS debug rootfs (--nfs=rootfs-dbg) instead
+             of the image's normal rootfs, and verify the debug rootfs
+             (bbappend, runqemu launch helper, launch.json paths).
+        slirp: boot Qemu with runqemu's slirp (user-mode) networking and an
+               SSH port-forward instead of the default tap networking.
+        """
+        testimage = "oe-selftest-image"
+        nfs_export = 'rootfs-dbg'
+
+        self._check_workspace()
+        self._write_bb_config()
+        if not slirp:
+            self._check_runqemu_prerequisites()
+
+        tempdir_cmake = self._devtool_ide_sdk_recipe(
+            self._cmake_recipe_name, "CMakeLists.txt", None)
+        tempdir_meson = self._devtool_ide_sdk_recipe(
+            self._meson_recipe_name, "meson.build", testimage)
+        package_opts = self._ide_sdk_package_opts()
+        nfs_opts = ' --nfs=%s' % nfs_export if nfs else ''
+        runCmd('devtool ide-sdk %s -c --ide=code,none %s%s' % (testimage, package_opts, nfs_opts),
+               output_log=self._cmd_logger)
+
+        if slirp:
+            self._verify_slirp_bbappend(testimage)
+
+        if nfs:
+            nfs_rootfs, runqemu_helper = self._verify_nfs_debug_rootfs(testimage, nfs_export)
+            self._verify_nfs_binary_deployed(nfs_rootfs, self._cmake_recipe_name)
+            self._verify_nfs_binary_deployed(nfs_rootfs, self._meson_example_exe)
+            self._verify_nfs_launch_json(tempdir_cmake, nfs_rootfs)
+            self._verify_nfs_launch_json(tempdir_meson, nfs_rootfs)
+
+            launch_cmd = '%s nographic' % shlex.quote(runqemu_helper)
+            runqemuparams = ''
+            if slirp:
+                launch_cmd += ' slirp'
+                # QemuTarget uses this only to select its localhost SSH endpoint;
+                # launch_cmd supplies the actual runqemu option.
+                runqemuparams = 'slirp'
+            qemu_cm = runqemu(testimage, runqemuparams=runqemuparams, launch_cmd=launch_cmd)
+        else:
+            runqemuparams = "nographic slirp" if slirp else "nographic"
+            qemu_cm = runqemu(testimage, runqemuparams=runqemuparams)
+
+        with qemu_cm as qemu:
+            if nfs:
+                status, output = qemu.run(
+                    "awk '$2 == \"/\" {print $3}' /proc/mounts")
+                self.assertEqual(status, 0)
+                self.assertEqual(output.strip(), 'nfs')
+
+            if slirp:
+                self.assertIsNotNone(qemu.port, 'No SSH port for the slirp target')
+                target_options = '-t root@%s -P %s' % (qemu.ip, qemu.port)
+            else:
+                target_options = '-t root@%s -c' % qemu.ip
+
+            # Re-run ide-sdk --skip-bitbake against the live Qemu IP which was
+            # not known at the time of the initial ide-sdk invocation.
+            # --skip-bitbake also skips the NFS rootfs (re-)extraction, which
+            # would otherwise wipe the directory the target has mounted.
+            bitbake_sdk_cmd = 'devtool ide-sdk %s %s --skip-bitbake --ide=code,none %s%s' % (
+                testimage, target_options, package_opts, nfs_opts)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
+            # Verify the debugger is available and functional on host
+            self._verify_cross_debugger()
+
+            # CMake: Verify compiliation and unit test execution via Qemu user mode works
+            compile_cmd = self._verify_cmake_preset(tempdir_cmake)
+
+            # CMake: Perform a full remote development cycle with the IDE configurations on Qemu system
+            self._verify_recipe_ide_sdk(
+                tempdir_cmake, qemu, self._cmake_recipe_name, self._cmake_recipe_name, compile_cmd)
+
+            # Meson: Verify compilation and unit test execution via Qemu user mode works with meson
+            compile_cmd = self._verify_meson_build(tempdir_meson, self._meson_recipe_name)
+
+            # Meson: Perform a full remote development cycle with the IDE configurations on Qemu system
+            self._verify_recipe_ide_sdk(
+                tempdir_meson, qemu, self._meson_recipe_name, self._meson_example_exe, compile_cmd)
+
 
 class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
 
-    def _gdb_cross(self):
+    _cmake_recipe_name = "cmake-example"
+    _meson_recipe_name = "meson-example"
+    _meson_example_exe = "mesonex"
+
+    def _ide_sdk_package_opts(self):
+        package_filters = [
+            # Exercise multi-recipe package scoping via repeated --package:
+            # RECIPE:,-ptest expands to RECIPE and RECIPE-ptest.
+            "cmake-example:,-ptest",
+            "meson-example:,-ptest",
+        ]
+        return ' '.join('--package %s' % p for p in package_filters)
+
+    def _verify_cross_debugger(self):
         """Verify gdb-cross is provided by devtool ide-sdk"""
         target_arch = self.td["TARGET_ARCH"]
         target_sys = self.td["TARGET_SYS"]
@@ -3491,6 +3699,56 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
         self.assertEqual(r.status, 0)
         self.assertIn("GNU gdb", r.output)
 
+    def _verify_recipe_ide_sdk(self, tempdir, qemu, recipe_name, example_exe, compile_cmd):
+        """Verify one recipe's service, ide=code launch.json/tasks.json and ide=none scripts.
+
+        Confirms the recipe's service is running on the target and that its
+        config file/binary ownership matches the recipe's static user/group,
+        then exercises ide=code (attach + once debugging) and ide=none
+        (before/after a source change/recompile/redeploy cycle) against the
+        same build.
+        """
+        example_user_group = recipe_name
+        conf_file = "/etc/%s.conf" % recipe_name
+
+        # Verify the service is running on the target and its config file
+        # is owned by the recipe's static user/group
+        self._verify_service_running(qemu, example_exe)
+        self._verify_conf_file(qemu, conf_file, example_user_group, example_user_group)
+
+        if recipe_name == self._meson_recipe_name:
+            # ide=code: settings.json should point at the same meson wrapper
+            # ide=none's _verify_meson_build just exercised, proving both IDE
+            # configs reference the identical, already-working build
+            # integration rather than a separate/duplicated one.
+            with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j:
+                settings_d = json.load(settings_j)
+            self.assertEqual(
+                settings_d["mesonbuild.mesonPath"],
+                os.path.join(self._workspace_scripts_dir(recipe_name), "meson"),
+                "ide=code settings.json should reference the same meson wrapper as ide=none")
+
+        # ide=code: verify tasks.json/launch.json and run one remote debugging pass
+        # (attach + once) through them, against the unmodified example.
+        self._verify_install_script_code(
+            tempdir, recipe_name, ['%s:,-ptest' % recipe_name])
+        self._verify_launch_json(tempdir)
+        self._verify_launch_json_debugging(tempdir, qemu, example_exe)
+
+        # ide=none: verify the generated scripts also work, before and
+        # after a code change/recompile/redeploy cycle. Proves both IDE
+        # configs generated from the same invocation debug the identical
+        # build correctly.
+        self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe, compile_cmd)
+
+        # Verify the oe-scripts sym-link is valid
+        self.assertEqual(self._workspace_scripts_dir(
+            recipe_name), self._sources_scripts_dir(tempdir))
+
+        # Verify the config file is still owned by the recipe's user
+        # after the install and deploy scripts updated the file
+        self._verify_conf_file(qemu, conf_file, example_user_group, example_user_group)
+
     def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run",
                               exe_break_line=136, exe_list_line=129, hpp_break_line=24,
                               lib_break_line=31):
@@ -3578,10 +3836,11 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
         numbers after recompiling, to prove the breakpoints resolve via the
         freshly rebuilt debug info.
         """
+        port = self._find_multi_script_port(recipe_name, 'gdbserver', example_exe)
         gdbserver_script = os.path.join(self._workspace_scripts_dir(
-            recipe_name), 'gdbserver_1234_usr-bin-' + example_exe + '_multi')
+            recipe_name), 'gdbserver_%d_usr-bin-%s_multi' % (port, example_exe))
         gdb_script = os.path.join(self._workspace_scripts_dir(
-            recipe_name), 'gdb_1234_usr-bin-' + example_exe)
+            recipe_name), 'gdb_%d_usr-bin-%s' % (port, example_exe))
 
         # Start a gdbserver
         r = runCmd(gdbserver_script, output_log=self._cmd_logger)
@@ -3591,7 +3850,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
         # an NFS-root target (--nfs=rootfs-dbg) the pid file it wrote has been
         # observed to become readable over the next SSH connection only a
         # moment later, so poll for it.
-        pid_file = '/tmp/gdbserver_1234_usr-bin-%s_multi/gdbserver.pid' % example_exe
+        pid_file = '/tmp/gdbserver_%d_usr-bin-%s_multi/gdbserver.pid' % (port, example_exe)
         status, output = qemu.run('cat %s' % pid_file)
         for _ in range(10):
             if status == 0:
@@ -3607,7 +3866,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
         self.assertEqual(status, 0)
         self.assertIn("gdbserver", output)
         self.assertIn("--multi", output)
-        self.assertIn("1234", output)
+        self.assertIn(str(port), output)
 
         # Test remote debugging works
         gdb_batch_cmd = " --batch " + self._gdb_debug_cpp_example(
@@ -3629,88 +3888,6 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
             gdbserver_pid, pid_file))
         self.assertEqual(status, 0)
 
-    @OETestTag("runqemu")
-    def test_devtool_ide_sdk_none_qemu(self):
-        """Start qemu-system and run tests for multiple recipes. ide=none is used."""
-        recipe_names = ["cmake-example", "meson-example"]
-        package_filters = [
-            # Exercise multi-recipe package scoping via repeated --package:
-            # RECIPE:,-ptest expands to RECIPE and RECIPE-ptest.
-            "cmake-example:,-ptest",
-            "meson-example:,-ptest",
-        ]
-        testimage = "oe-selftest-image"
-
-        self._check_workspace()
-        self._write_bb_config()
-        self._check_runqemu_prerequisites()
-
-        # Verify deployment to Qemu (system mode) works
-        tempdir_cmake = self._devtool_ide_sdk_recipe("cmake-example", "CMakeLists.txt", None)
-        tempdir_meson = self._devtool_ide_sdk_recipe("meson-example", "meson.build", testimage)
-        package_opts = ' '.join('--package %s' % p for p in package_filters)
-        runCmd('devtool ide-sdk cmake-example meson-example %s -c --ide=none %s' % (testimage, package_opts),
-               output_log=self._cmd_logger)
-
-        with runqemu(testimage, runqemuparams="nographic") as qemu:
-            # cmake-example recipe
-            recipe_name = "cmake-example"
-            example_exe = "cmake-example"
-            example_user_group = "cmake-example"
-            conf_file = "/etc/cmake-example.conf"
-
-            # Verify the cmake-example service is running on the target
-            self._verify_service_running(qemu, example_exe)
-            # Verify /etc/cmake-example.conf is owned by the cmake-example user
-            self._verify_conf_file(qemu, conf_file, example_user_group, example_user_group)
-
-            # Re-run ide-sdk with only the image name. Modified recipes must
-            # be discovered from workspace appends; the image bbappend itself
-            # must not be mistaken for a modified recipe.
-            bitbake_sdk_cmd = 'devtool ide-sdk %s -t root@%s -c --skip-bitbake --ide=none %s' % (
-                testimage, qemu.ip, package_opts)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            self._gdb_cross()
-            compile_cmd = self._verify_cmake_preset(tempdir_cmake)
-            self._devtool_ide_sdk_qemu(tempdir_cmake, qemu, recipe_name, example_exe, compile_cmd)
-
-            # Verify the oe-scripts sym-link is valid
-            self.assertEqual(self._workspace_scripts_dir(
-                recipe_name), self._sources_scripts_dir(tempdir_cmake))
-
-            # 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)
-
-            # meson-example recipe
-            recipe_name = "meson-example"
-            example_exe = "mesonex"
-            example_user_group = "meson-example"
-            conf_file = "/etc/meson-example.conf"
-
-            # Verify the meson-example service is running on the target
-            self._verify_service_running(qemu, example_exe)
-            # Verify /etc/meson-example.conf is owned by the meson-example user
-            self._verify_conf_file(qemu, conf_file, example_user_group, example_user_group)
-
-            # Re-run ide-sdk with the actual QEMU IP for this recipe
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=none %s' % (
-                recipe_name, testimage, qemu.ip, package_opts)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            self._gdb_cross()
-            compile_cmd = self._verify_meson_build(tempdir_meson, recipe_name)
-            self._devtool_ide_sdk_qemu(tempdir_meson, qemu, recipe_name, example_exe, compile_cmd)
-
-            # Verify the oe-scripts sym-link is valid
-            self.assertEqual(self._workspace_scripts_dir(
-                recipe_name), self._sources_scripts_dir(tempdir_meson))
-
-            # Verify /etc/meson-example.conf is still owned by the meson-example user
-            # after the install and deploy scripts updated the file
-            self._verify_conf_file(qemu, conf_file, example_user_group, example_user_group)
-
     def _verify_launch_json(self, tempdir):
         """Verify the launch.json file created is valid and contains proper debug configurations"""
         launch_json_path = os.path.join(tempdir, '.vscode', 'launch.json')
@@ -4037,41 +4214,8 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
                 self.assertEqual(r.status, 0)
                 debug_check_func(r.output, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
 
-    def test_devtool_ide_sdk_code_nfs_debug_rootfs(self):
-        """Verify ide-sdk extracts an NFS debug rootfs for VS Code debugging."""
-        recipe_name = "cmake-example"
-        build_file = "CMakeLists.txt"
-        testimage = "oe-selftest-image"
-
-        self._check_workspace()
-        self._write_bb_config()
-        tempdir = self._devtool_ide_sdk_recipe(
-            recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=code --nfs=rootfs-dbg' %
-               (recipe_name, testimage), output_log=self._cmd_logger)
-
-        nfs_rootfs = os.path.join(
-            self.workspacedir, 'nfs-exports', testimage, 'rootfs-dbg')
-        self.assertExists(nfs_rootfs)
-        self.assertExists(nfs_rootfs + '.pseudo_state')
-        self.assertExists(os.path.join(nfs_rootfs, 'usr', 'bin', recipe_name))
-        runqemu_helper = os.path.join(
-            self.workspacedir, 'nfs-exports', testimage,
-            'runqemu-rootfs-dbg')
-        self.assertExists(runqemu_helper)
-        self.assertTrue(os.access(runqemu_helper, os.X_OK))
-        with open(runqemu_helper) as helper_file:
-            helper = helper_file.read()
-        self.assertIn('exec runqemu ', helper)
-        self.assertIn(nfs_rootfs, helper)
-
-        bbappend = os.path.join(
-            self.workspacedir, 'appends', testimage + '.bbappend')
-        with open(bbappend) as append_file:
-            self.assertRegex(
-                append_file.read(),
-                r'IMAGE_FSTYPES_DEBUGFS(?::append)? = " ?tar"')
-
+    def _verify_nfs_launch_json(self, tempdir, nfs_rootfs):
+        """Verify launch.json's cppdbg configurations reference the NFS debug rootfs."""
         with open(os.path.join(tempdir, '.vscode', 'launch.json')) as launch_file:
             launch_configurations = json.load(launch_file)['configurations']
         for configuration in launch_configurations:
@@ -4081,169 +4225,24 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
                 os.path.join(nfs_rootfs, 'usr', 'src', 'debug'),
                 configuration['sourceFileMap']['/usr/src/debug'])
 
-    def _test_devtool_ide_sdk_nfs_debug_rootfs_qemu(self, slirp=False):
-        """Boot the extracted NFS debug rootfs and exercise remote GDB."""
-        recipe_name = "cmake-example"
-        build_file = "CMakeLists.txt"
-        testimage = "oe-selftest-image"
+    def test_devtool_ide_sdk_code_nfs_debug_rootfs(self):
+        """Verify ide-sdk extracts an NFS debug rootfs for VS Code debugging."""
+        self._test_devtool_ide_sdk_code_nfs_debug_rootfs()
 
-        self._check_workspace()
-        self._write_bb_config()
-        if not slirp:
-            self._check_runqemu_prerequisites()
-
-        tempdir = self._devtool_ide_sdk_recipe(
-            recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=none --nfs=rootfs-dbg' %
-               (recipe_name, testimage),
-               output_log=self._cmd_logger)
-
-        nfs_rootfs = os.path.join(
-            self.workspacedir, 'nfs-exports', testimage, 'rootfs-dbg')
-        runqemu_helper = os.path.join(
-            self.workspacedir, 'nfs-exports', testimage,
-            'runqemu-rootfs-dbg')
-        self.assertExists(nfs_rootfs)
-        self.assertExists(runqemu_helper)
-
-        launch_cmd = '%s nographic' % shlex.quote(runqemu_helper)
-        runqemuparams = ''
-        if slirp:
-            launch_cmd += ' slirp'
-            # QemuTarget uses this only to select its localhost SSH endpoint;
-            # launch_cmd supplies the actual runqemu option.
-            runqemuparams = 'slirp'
-
-        with runqemu(testimage, runqemuparams=runqemuparams,
-                     launch_cmd=launch_cmd) as qemu:
-            status, output = qemu.run(
-                "awk '$2 == \"/\" {print $3}' /proc/mounts")
-            self.assertEqual(status, 0)
-            self.assertEqual(output.strip(), 'nfs')
-
-            # Re-run with the real QEMU target address, instead of 192.168.7.2 IP.
-            # --skip-bitbake also skips the rootfs extraction, which would
-            # otherwise wipe the directory the target has mounted right now.
-            if slirp:
-                self.assertIsNotNone(qemu.port, 'No SSH port for the slirp target')
-                target_options = '-t root@%s -P %s ' % (qemu.ip, qemu.port)
-            else:
-                target_options = '-t root@%s -c ' % qemu.ip
-            runCmd('devtool ide-sdk %s %s %s--skip-bitbake --ide=none --nfs=rootfs-dbg' %
-                   (recipe_name, testimage, target_options),
-                   output_log=self._cmd_logger)
-
-            self._gdb_cross()
-            compile_cmd = self._verify_cmake_preset(tempdir)
-            self._devtool_ide_sdk_qemu(
-                tempdir, qemu, recipe_name, recipe_name, compile_cmd)
+    @OETestTag("runqemu")
+    def test_devtool_ide_sdk_code_and_none_qemu(self):
+        """Verify devtool ide-sdk --ide=code,none for cmake-example/meson-example with GCC and GDB."""
+        self._test_devtool_ide_sdk_code_and_none_qemu()
 
     @OETestTag("runqemu")
     def test_devtool_ide_sdk_none_nfs_qemu(self):
-        """Verify remote GDB debugging through an NFS-root QEMU target."""
-        self._test_devtool_ide_sdk_nfs_debug_rootfs_qemu()
+        """Verify the full ide=code,none workflow through an NFS-root QEMU target."""
+        self._test_devtool_ide_sdk_code_and_none_qemu(nfs=True)
 
     @OETestTag("runqemu")
     def test_devtool_ide_sdk_none_nfs_qemu_slirp(self):
-        """Verify remote GDB debugging through an NFS-root slirp target."""
-        self._test_devtool_ide_sdk_nfs_debug_rootfs_qemu(slirp=True)
-
-    @OETestTag("runqemu")
-    def test_devtool_ide_sdk_code_cmake(self):
-        """Verify a cmake recipe works with ide=code mode"""
-        recipe_name = "cmake-example"
-        example_exe = "cmake-example"
-        package_opt = '--package %s:,-ptest' % recipe_name
-        build_file = "CMakeLists.txt"
-        testimage = "oe-selftest-image"
-        build_file = "CMakeLists.txt"
-
-        self._check_workspace()
-        self._write_bb_config()
-
-        # Build image with debug settings before starting QEMU
-        self._check_runqemu_prerequisites()
-        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=code %s' % (recipe_name, testimage, package_opt),
-               output_log=self._cmd_logger)
-
-        with runqemu(testimage, runqemuparams="nographic") as qemu:
-            # Re-run with actual QEMU IP; image is already built
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=code %s' % (
-                recipe_name, testimage, qemu.ip, package_opt)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-            self._verify_cmake_preset(tempdir)
-            self._verify_install_script_code(
-                tempdir, recipe_name, ['%s:,-ptest' % recipe_name])
-            self._gdb_cross()
-
-            # Verify the launch.json file created is valid
-            self._verify_launch_json(tempdir)
-
-            # Verify deployment and remote debugging works
-            self._verify_launch_json_debugging(tempdir, qemu, example_exe)
-
-    @OETestTag("runqemu")
-    def test_devtool_ide_sdk_code_meson(self):
-        """Verify a meson recipe works with ide=code mode"""
-        recipe_name = "meson-example"
-        example_exe = "mesonex"
-        package_opt = '--package %s:,-ptest' % recipe_name
-        build_file = "meson.build"
-        testimage = "oe-selftest-image"
-
-        self._check_workspace()
-        self._write_bb_config()
-
-        # Build image with debug settings before starting QEMU
-        self._check_runqemu_prerequisites()
-        tempdir = self._devtool_ide_sdk_recipe(
-            recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=code %s' % (recipe_name, testimage, package_opt),
-               output_log=self._cmd_logger)
-
-        with runqemu(testimage, runqemuparams="nographic") as qemu:
-            # Re-run with actual QEMU IP; image is already built
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=code %s' % (
-                recipe_name, testimage, qemu.ip, package_opt)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            with open(os.path.join(tempdir, '.vscode', 'settings.json')) as settings_j:
-                settings_d = json.load(settings_j)
-            meson_exe = settings_d["mesonbuild.mesonPath"]
-            meson_build_folder = settings_d["mesonbuild.buildFolder"]
-
-            # Verify the wrapper for meson native is available
-            self.assertExists(meson_exe)
-
-            # Verify meson re-uses the o files compiled by bitbake
-            result = runCmd('%s compile -C  %s' %
-                            (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger)
-            self.assertIn("ninja: no work to do.", result.output)
-
-            # Verify the unit tests work (in Qemu user mode)
-            runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir,
-                   output_log=self._cmd_logger)
-
-            # Verify re-building and testing works again
-            result = runCmd('%s compile -C  %s --clean' %
-                            (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger)
-            self.assertIn("Cleaning...", result.output)
-            result = runCmd('%s compile -C  %s' %
-                            (meson_exe, meson_build_folder), cwd=tempdir, output_log=self._cmd_logger)
-            self.assertIn("Linking target", result.output)
-            runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir,
-                   output_log=self._cmd_logger)
-
-            self._verify_install_script_code(
-                tempdir, recipe_name, ['%s:,-ptest' % recipe_name])
-            self._gdb_cross()
-
-            # Verify the launch.json file created is valid
-            self._verify_launch_json(tempdir)
-
-            # Verify deployment and remote debugging works
-            self._verify_launch_json_debugging(tempdir, qemu, example_exe)
+        """Verify the full ide=code,none workflow through an NFS-root slirp target."""
+        self._test_devtool_ide_sdk_code_and_none_qemu(nfs=True, slirp=True)
 
     @OETestTag("runqemu")
     def test_devtool_ide_sdk_none_qemu_slirp(self):
@@ -4256,44 +4255,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
           - runqemu boots the image in slirp mode.
           - devtool deploy-target reaches the target via localhost:2222.
         """
-        recipe_name = "cmake-example"
-        build_file = "CMakeLists.txt"
-        testimage = "oe-selftest-image"
-
-        self._check_workspace()
-        self._write_bb_config()
-
-        # devtool modify and build image; QB_SLIRP_OPT is written to the bbappend.
-        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, None)
-        runCmd('devtool ide-sdk %s %s -c --ide=none' % (recipe_name, testimage),
-               output_log=self._cmd_logger)
-
-        # Verify QB_SLIRP_OPT was written to the workspace bbappend.
-        appends_dir = os.path.join(self.workspacedir, 'appends')
-        bbappend = os.path.join(appends_dir, testimage + '.bbappend')
-        self.assertExists(bbappend, 'Image bbappend not created at %s' % bbappend)
-        with open(bbappend) as f:
-            bbappend_content = f.read()
-        self.assertIn('QB_SLIRP_OPT', bbappend_content,
-                      'QB_SLIRP_OPT not written to image bbappend')
-        self.assertIn('hostfwd=tcp:127.0.0.1:2222-:22', bbappend_content,
-                      'SSH slirp port forward missing from QB_SLIRP_OPT')
-
-        with runqemu(testimage, runqemuparams="nographic slirp") as qemu:
-            slirp_host = qemu.ip
-            self.assertIsNotNone(qemu.port, 'No SSH port for the slirp target')
-            slirp_port = qemu.port
-
-            # Re-run ide-sdk with the actual slirp address; image is already built.
-            bitbake_sdk_cmd = (
-                'devtool ide-sdk %s %s -t root@%s -P %s --skip-bitbake --ide=none'
-                % (recipe_name, testimage, slirp_host, slirp_port))
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            self._gdb_cross()
-            compile_cmd = self._verify_cmake_preset(tempdir)
-            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name,
-                                        recipe_name, compile_cmd)
+        self._test_devtool_ide_sdk_code_and_none_qemu(slirp=True)
 
 
 class DevtoolIdeSdkKernelTests(DevtoolIdeSdkTests):
@@ -4578,6 +4540,83 @@ class DevtoolIdeSdkSharedTests(DevtoolIdeSdkTests):
 
 class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests):
 
+    _cmake_recipe_name = "cmake-example-clang"
+    _meson_recipe_name = "meson-example-clang"
+    _meson_example_exe = "mesonex-clang"
+
+    def _verify_cross_debugger(self):
+        """Verify lldb-native is provided by devtool ide-sdk"""
+        lldb_binary = self._lldb_native_binary()
+        r = runCmd("%s --version" % lldb_binary, output_log=self._cmd_logger)
+        self.assertEqual(r.status, 0)
+        self.assertIn("lldb", r.output.lower())
+
+    def _lldb_native_binary(self):
+        """Locate the lldb binary in the 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")
+        return lldb_binary
+
+    def _verify_recipe_ide_sdk(self, tempdir, qemu, recipe_name, example_exe, compile_cmd):
+        """Verify one recipe's ide=code/ide=none LLDB configs and remote debugging.
+
+        Confirms the generated configs select lldb-native/lldb-server instead
+        of gdb-cross/gdbserver: extensions.json recommends vadimcn.vscode-lldb,
+        launch.json uses "type": "lldb" instead of "cppdbg", and ide=none
+        generates lldb_server_*/lldbinit/lldb_* scripts with no GDB scripts
+        alongside them. Then exercises ide=code (one lldb --batch debugging
+        pass) and ide=none (before/after a source change/recompile/redeploy
+        cycle) against the same build.
+        """
+        # 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 ide=none scripts were also generated alongside ide=code.
+        # The port is not necessarily 1234: ide=code's own config for the
+        # same binary is created first and consumes a port too.
+        scripts_dir = self._workspace_scripts_dir(recipe_name)
+        binary_pretty = 'usr-bin-' + example_exe
+        port = self._find_multi_script_port(recipe_name, 'lldb_server', example_exe)
+        self.assertExists(os.path.join(
+            scripts_dir, 'lldb_server_%d_%s_multi' % (port, binary_pretty)))
+        self.assertExists(os.path.join(
+            scripts_dir, 'lldbinit', 'lldbinit_%d_%s' % (port, binary_pretty)))
+        self.assertExists(os.path.join(
+            scripts_dir, 'lldb_%d_%s' % (port, binary_pretty)))
+        # No GDB scripts should have been generated for a clang recipe
+        self.assertFalse(glob.glob(os.path.join(
+            scripts_dir, 'gdbserver_*_%s_multi' % binary_pretty)),
+            'gdbserver script should not be generated for clang recipe')
+
+        # Verify the oe-scripts sym-link is valid
+        self.assertEqual(self._workspace_scripts_dir(recipe_name),
+                         self._sources_scripts_dir(tempdir))
+
+        # ide=code: one lldb debugging pass through launch.json/tasks.json,
+        # against the unmodified example.
+        self._lldb_server_debugging_once(
+            tempdir, qemu, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
+
+        # ide=none: full debugging cycle through the generated scripts,
+        # before and after a code change/recompile/redeploy cycle (see
+        # _lldb_cross_debugging_multi). Proves both IDE configs generated
+        # from the same invocation debug the identical build correctly.
+        self._lldb_cross_debugging_multi(
+            tempdir, recipe_name, compile_cmd,
+            lambda magic_string: self._lldb_none_debugging_multi(
+                tempdir, qemu, recipe_name, example_exe, magic_string))
+
     def _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')
@@ -4896,10 +4935,7 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests):
         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")
+        lldb_binary = self._lldb_native_binary()
 
         self.logger.debug("Starting lldb-server via SSH: %s", " ".join(ssh_cmd))
         runCmd(ssh_cmd, output_log=self._cmd_logger)
@@ -4937,127 +4973,18 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests):
         self.assertEqual(r.status, 0, "lldb batch session failed: %s" % r.output)
         self._lldb_debug_cpp_example_check(r.output, magic_string)
 
-    @OETestTag("runqemu")
-    def test_devtool_ide_sdk_code_cmake(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()
-
-        # Build image with debug settings (lldb-server for clang) before starting QEMU
-        self._check_runqemu_prerequisites()
-        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=code' % (recipe_name, testimage),
-               output_log=self._cmd_logger)
-
-        with runqemu(testimage, runqemuparams="nographic") as qemu:
-            # Re-run with actual QEMU IP; image is already built
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=code' % (
-                recipe_name, testimage, qemu.ip)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            # Verify the cmake preset still works (build system unchanged)
-            compile_cmd = self._verify_cmake_preset(tempdir)
-
-            # Verify the install && deploy-target task script exists
-            self._verify_install_script_code(tempdir, recipe_name)
-
-            # Verify extensions.json recommends CodeLLDB instead of / alongside cpptools
-            with open(os.path.join(tempdir, '.vscode', 'extensions.json')) as ext_j:
-                ext_d = json.load(ext_j)
-            recommendations = ext_d.get('recommendations', [])
-            self.assertIn('vadimcn.vscode-lldb', recommendations,
-                          'vadimcn.vscode-lldb should be recommended for clang recipes')
-
-            # Verify launch.json uses CodeLLDB format
-            self._verify_launch_json_lldb(tempdir)
-
-            # Verify deployment and lldb batch remote debugging work end-to-end,
-            # before and after a code change/recompile/redeploy cycle (see
-            # _lldb_cross_debugging_multi)
-            self._lldb_cross_debugging_multi(
-                tempdir, recipe_name, compile_cmd,
-                lambda magic_string: self._lldb_server_debugging_once(
-                    tempdir, qemu, magic_string))
-
-    @OETestTag("runqemu")
-    def test_devtool_ide_sdk_code_meson(self):
-        """Verify a meson recipe built with clang works with ide=code (CodeLLDB debugging).
-
-        This is the meson/ninja counterpart of test_devtool_ide_sdk_code_cmake_clang.
-        It matters as its own test (rather than being covered by the cmake/clang
-        test alone) because meson/ninja invoke the compiler with source paths
-        relative to the build directory, unlike cmake (with the Ninja or
-        Makefiles generators used here), which normally passes absolute source
-        paths. That relative-path compilation is what previously caused
-        -fdebug-prefix-map/-ffile-prefix-map underflow (DW_AT_name climbing
-        above DW_AT_comp_dir with excess dot-dot components) for devtool
-        workspaces, breaking source-level breakpoint resolution in CodeLLDB.
-        The cmake/clang test alone would not catch that regression.
-
-        This test uses the meson-example-clang recipe which is a meson-example
-        variant built with clang. It installs a separate binary
-        (mesonex-clang) so all four recipe variants (cmake/meson x gcc/clang)
-        can be installed in the same image without conflicts.
-        """
-        recipe_name = "meson-example-clang"
-        build_file = "meson.build"
-        testimage = "oe-selftest-image"
-
-        self._check_workspace()
-        self._write_bb_config()
-
-        # Build image with debug settings (lldb-server for clang) before starting QEMU
-        self._check_runqemu_prerequisites()
-        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=code' % (recipe_name, testimage),
-               output_log=self._cmd_logger)
-
-        with runqemu(testimage, runqemuparams="nographic") as qemu:
-            # Re-run with actual QEMU IP; image is already built
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=code' % (
-                recipe_name, testimage, qemu.ip)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            # Verify the meson build system still works (unchanged by clang/lldb support)
-            compile_cmd = self._verify_meson_build(tempdir, recipe_name)
-
-            # Verify the install && deploy-target task script exists
-            self._verify_install_script_code(tempdir, recipe_name)
-
-            # Verify extensions.json recommends CodeLLDB instead of / alongside cpptools
-            with open(os.path.join(tempdir, '.vscode', 'extensions.json')) as ext_j:
-                ext_d = json.load(ext_j)
-            recommendations = ext_d.get('recommendations', [])
-            self.assertIn('vadimcn.vscode-lldb', recommendations,
-                          'vadimcn.vscode-lldb should be recommended for clang recipes')
-
-            # Verify launch.json uses CodeLLDB format
-            self._verify_launch_json_lldb(tempdir)
-
-            # Verify deployment and lldb batch remote debugging work end-to-end,
-            # before and after a code change/recompile/redeploy cycle (see
-            # _lldb_cross_debugging_multi)
-            self._lldb_cross_debugging_multi(
-                tempdir, recipe_name, compile_cmd,
-                lambda magic_string: self._lldb_server_debugging_once(
-                    tempdir, qemu, magic_string))
+    def _verify_nfs_launch_json(self, tempdir, nfs_rootfs):
+        """Verify launch.json's CodeLLDB configurations reference the NFS debug rootfs."""
+        with open(os.path.join(tempdir, '.vscode', 'launch.json')) as launch_file:
+            launch_configurations = json.load(launch_file)['configurations']
+        for configuration in launch_configurations:
+            self.assertEqual(
+                os.path.join(nfs_rootfs, 'usr', 'src', 'debug'),
+                configuration['sourceMap']['/usr/src/debug'])
+            init_commands = configuration.get('initCommands', [])
+            self.assertTrue(
+                any(nfs_rootfs in cmd for cmd in init_commands),
+                "initCommands should point debug-file-search-paths at the NFS debug rootfs")
 
     def _lldb_none_debugging_multi(self, tempdir, qemu, recipe_name, example_exe, magic_string):
         """Verify lldb-server scripts generated by ide=none work end-to-end.
@@ -5072,11 +4999,15 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests):
         lldbinit source map / debug-file-search-paths setup for the library's
         own debug info specifically.
         """
+        # The port is not necessarily 1234: when combined with --ide=code the
+        # ONCE/MULTI-mode server ide=code uses gets ports allocated first,
+        # pushing this MULTI-mode server to the next one allocated.
+        port = self._find_multi_script_port(recipe_name, 'lldb_server', example_exe)
         scripts_dir = self._workspace_scripts_dir(recipe_name)
         binary_pretty = 'usr-bin-' + example_exe
         lldb_server_script = os.path.join(
-            scripts_dir, 'lldb_server_1234_%s_multi' % binary_pretty)
-        lldb_script = os.path.join(scripts_dir, 'lldb_1234_%s' % binary_pretty)
+            scripts_dir, 'lldb_server_%d_%s_multi' % (port, binary_pretty))
+        lldb_script = os.path.join(scripts_dir, 'lldb_%d_%s' % (port, binary_pretty))
 
         self.assertExists(lldb_server_script)
         self.assertExists(lldb_script)
@@ -5085,7 +5016,7 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests):
         r = runCmd(lldb_server_script, output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
 
-        pid_file = '/tmp/lldb_server_1234_%s_multi/lldb_server.pid' % binary_pretty
+        pid_file = '/tmp/lldb_server_%d_%s_multi/lldb_server.pid' % (port, binary_pretty)
         status, output = qemu.run('cat %s' % pid_file)
         self.assertEqual(status, 0)
         lldb_server_pid = output.strip()
@@ -5114,142 +5045,47 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests):
             lldb_server_pid, pid_file))
         self.assertEqual(status, 0)
 
-    @OETestTag("runqemu")
-    def test_devtool_ide_sdk_none_cmake(self):
-        """Verify ide=none generates correct LLDB scripts for a clang cmake recipe.
-
-        Uses cmake-example-clang (TOOLCHAIN = "clang") which is built with the
-        clang toolchain.  devtool ide-sdk with --ide=none should produce:
-          - lldb_server_<port>_<binary>_multi  (start/stop script)
-          - lldbinit/lldbinit_<port>_<binary>  (platform connect + source maps)
-          - lldb_<port>_<binary>               (lldb wrapper)
-
-        The test verifies that lldb-server can be started via the generated
-        script, and that a basic lldb --batch debugging session reaches main.
-        """
-        recipe_name = 'cmake-example-clang'
-        example_exe = 'cmake-example-clang'
-        build_file = 'CMakeLists.txt'
-        testimage = 'oe-selftest-image'
-
-        self._check_workspace()
-        self._write_bb_config()
-        self._check_runqemu_prerequisites()
-
-        # Build image with debug settings (lldb-server for clang) before starting QEMU
-        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=none' % (recipe_name, testimage),
-               output_log=self._cmd_logger)
-
-        with runqemu(testimage, runqemuparams='nographic') as qemu:
-            # Re-run with actual QEMU IP; image is already built
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=none' % (
-                recipe_name, testimage, qemu.ip)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-
-            # Verify cmake preset still works (build system unchanged)
-            compile_cmd = self._verify_cmake_preset(tempdir)
-
-            # Verify install && deploy script exists
-            # (_verify_install_script_code opens .vscode/tasks.json which is not
-            # generated for ide=none; check the script path directly instead)
-            recipe_id, _ = self._get_recipe_ids(recipe_name)
-            scripts_dir = self._workspace_scripts_dir(recipe_name)
-            self.assertExists(os.path.join(
-                scripts_dir, 'install_and_deploy_' + recipe_id))
-
-            # Verify LLDB scripts were generated (not GDB scripts)
-            binary_pretty = 'usr-bin-' + example_exe
-            self.assertExists(os.path.join(
-                scripts_dir, 'lldb_server_1234_%s_multi' % binary_pretty))
-            self.assertExists(os.path.join(
-                scripts_dir, 'lldbinit', 'lldbinit_1234_%s' % binary_pretty))
-            self.assertExists(os.path.join(
-                scripts_dir, 'lldb_1234_%s' % binary_pretty))
-            # No GDB scripts should have been generated for a clang recipe
-            self.assertFalse(os.path.exists(os.path.join(
-                scripts_dir, 'gdbserver_1234_%s_multi' % binary_pretty)),
-                'gdbserver script should not be generated for clang recipe')
-
-            # Verify the oe-scripts sym-link is valid
-            self.assertEqual(self._workspace_scripts_dir(recipe_name),
-                             self._sources_scripts_dir(tempdir))
-
-            # Verify end-to-end lldb debugging, before and after a code
-            # change/recompile/redeploy cycle (see _lldb_cross_debugging_multi)
-            self._lldb_cross_debugging_multi(
-                tempdir, recipe_name, compile_cmd,
-                lambda magic_string: self._lldb_none_debugging_multi(
-                    tempdir, qemu, recipe_name, example_exe, magic_string))
+    def test_devtool_ide_sdk_code_nfs_debug_rootfs(self):
+        """Verify ide-sdk extracts an NFS debug rootfs for VS Code (CodeLLDB) debugging."""
+        self._test_devtool_ide_sdk_code_nfs_debug_rootfs()
 
     @OETestTag("runqemu")
-    def test_devtool_ide_sdk_none_meson(self):
-        """Verify ide=none generates correct LLDB scripts for a clang meson recipe.
+    def test_devtool_ide_sdk_code_and_none_qemu(self):
+        """Verify devtool ide-sdk --ide=code,none for cmake/meson-example-clang with Clang and LLDB.
 
-        This is the meson/ninja counterpart of test_devtool_ide_sdk_none_cmake_clang.
-        It is required in addition to the cmake/clang test because meson/ninja
-        (unlike cmake here) invoke the compiler with source paths relative to
-        the build directory, which is what previously caused source-level
-        breakpoints to be left unresolved (pending) for devtool workspaces due
-        to -fdebug-prefix-map/-ffile-prefix-map underflow; see
-        _lldb_none_debugging_multi.
-
-        Uses meson-example-clang (TOOLCHAIN = "clang").
+        See DevtoolIdeSdkTests._test_devtool_ide_sdk_code_and_none_qemu for
+        the shared workflow. Clang-specific here: the generated configs
+        select lldb-native/lldb-server instead of gdb-cross/gdbserver
+        (extensions.json recommends vadimcn.vscode-lldb, launch.json uses
+        "type": "lldb", and ide=none generates lldb_server_*/lldbinit/lldb_*
+        scripts with no GDB scripts alongside them), and debugging goes
+        through one lldb --batch pass (ide=code) plus the
+        lldb-server/lldb scripts (ide=none). The meson recipe matters as
+        its own coverage since meson/ninja compile with source paths
+        relative to the build directory, which requires a workaround for
+        -fdebug-prefix-map/-ffile-prefix-map underflow breaking
+        source-level breakpoint resolution in CodeLLDB.
         """
-        recipe_name = 'meson-example-clang'
-        example_exe = 'mesonex-clang'
-        build_file = 'meson.build'
-        testimage = 'oe-selftest-image'
+        self._test_devtool_ide_sdk_code_and_none_qemu()
 
-        self._check_workspace()
-        self._write_bb_config()
-        self._check_runqemu_prerequisites()
+    @OETestTag("runqemu")
+    def test_devtool_ide_sdk_none_nfs_qemu(self):
+        """Verify the full ide=code,none workflow through an NFS-root QEMU target."""
+        self._test_devtool_ide_sdk_code_and_none_qemu(nfs=True)
 
-        # Build image with debug settings (lldb-server for clang) before starting QEMU
-        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
-        runCmd('devtool ide-sdk %s %s -c --ide=none' % (recipe_name, testimage),
-               output_log=self._cmd_logger)
+    @OETestTag("runqemu")
+    def test_devtool_ide_sdk_none_nfs_qemu_slirp(self):
+        """Verify the full ide=code,none workflow through an NFS-root slirp target."""
+        self._test_devtool_ide_sdk_code_and_none_qemu(nfs=True, slirp=True)
 
-        with runqemu(testimage, runqemuparams='nographic') as qemu:
-            # Re-run with actual QEMU IP; image is already built
-            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=none' % (
-                recipe_name, testimage, qemu.ip)
-            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+    @OETestTag("runqemu")
+    def test_devtool_ide_sdk_none_qemu_slirp(self):
+        """Verify devtool ide-sdk works with runqemu slirp networking.
 
-            # Verify the meson build system still works (unchanged by clang/lldb support)
-            compile_cmd = self._verify_meson_build(tempdir, recipe_name)
+        Slirp mode uses SSH port forwarding (default: localhost:2222 -> guest:22).
+        """
+        self._test_devtool_ide_sdk_code_and_none_qemu(slirp=True)
 
-            # Verify install && deploy script exists
-            # (_verify_install_script_code opens .vscode/tasks.json which is not
-            # generated for ide=none; check the script path directly instead)
-            recipe_id, _ = self._get_recipe_ids(recipe_name)
-            scripts_dir = self._workspace_scripts_dir(recipe_name)
-            self.assertExists(os.path.join(
-                scripts_dir, 'install_and_deploy_' + recipe_id))
-
-            # Verify LLDB scripts were generated (not GDB scripts)
-            binary_pretty = 'usr-bin-' + example_exe
-            self.assertExists(os.path.join(
-                scripts_dir, 'lldb_server_1234_%s_multi' % binary_pretty))
-            self.assertExists(os.path.join(
-                scripts_dir, 'lldbinit', 'lldbinit_1234_%s' % binary_pretty))
-            self.assertExists(os.path.join(
-                scripts_dir, 'lldb_1234_%s' % binary_pretty))
-            # No GDB scripts should have been generated for a clang recipe
-            self.assertFalse(os.path.exists(os.path.join(
-                scripts_dir, 'gdbserver_1234_%s_multi' % binary_pretty)),
-                'gdbserver script should not be generated for clang recipe')
-
-            # Verify the oe-scripts sym-link is valid
-            self.assertEqual(self._workspace_scripts_dir(recipe_name),
-                             self._sources_scripts_dir(tempdir))
-
-            # Verify end-to-end lldb debugging, before and after a code
-            # change/recompile/redeploy cycle (see _lldb_cross_debugging_multi)
-            self._lldb_cross_debugging_multi(
-                tempdir, recipe_name, compile_cmd,
-                lambda magic_string: self._lldb_none_debugging_multi(
-                    tempdir, qemu, recipe_name, example_exe, magic_string))
 
 class DevtoolIdeSdkMiscTests(DevtoolIdeSdkTests):
 
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6/6] devtool: ide-sdk: trim _add_broken_srctree_prefix_map docstring
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
                   ` (4 preceding siblings ...)
  2026-09-13 20:05 ` [PATCH 5/6] oe-selftest: devtool ide-sdk: consolidate GCC/Clang ide-sdk tests AdrianF
@ 2026-09-13 20:05 ` AdrianF
  2026-09-16  6:48 ` [OE-core] [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release Antonin Godard
  6 siblings, 0 replies; 9+ messages in thread
From: AdrianF @ 2026-09-13 20:05 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

From: Adrian Freihofer <adrian.freihofer@siemens.com>

The docstring referenced test_devtool_ide_sdk_none_qemu and
test_devtool_ide_sdk_{code,none}_meson_clang, which no longer exist
since the previous commit merged the --ide=code/--ide=none tests. Point
at the current DevtoolIdeSdkGccTests/DevtoolIdeSdkClangTests classes
instead and shorten the docstring to the essentials.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_sdk.py | 70 +++++++++-------------------------
 1 file changed, 19 insertions(+), 51 deletions(-)

diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 44e11ce517..8f2e768a62 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -1169,61 +1169,29 @@ class RecipeModified:
         return mappings
 
     def _add_broken_srctree_prefix_map(self, mappings):
-        """Work around a -f*-prefix-map / DWARF path resolution issue affecting
-        out-of-tree devtool workspaces (e.g. meson recipes built via 'devtool modify'
-        with the clang toolchain).
+        """Work around broken DWARF paths for out-of-tree meson+clang workspaces.
 
-        meson/ninja may invoke the compiler with a *relative* source file path
-        when the build directory B (under WORKDIR) and the source directory S
-        (relocated outside WORKDIR by 'devtool modify') only share a distant
-        common ancestor. -fdebug-prefix-map/-ffile-prefix-map only rewrite
-        paths that literally start with the mapped host prefix, so a relative
-        path argument is never rewritten: only DW_AT_comp_dir (which is
-        absolute) gets rewritten, DW_AT_name stays relative and unrewritten.
-
-        This has only been observed to actually happen with the clang
-        toolchain: clang's meson/ninja invocation embeds a relative DW_AT_name
-        for out-of-tree sources, while gcc, even via meson/ninja, embeds an
-        absolute (and correctly -fdebug-prefix-map-rewritten) DW_AT_name, so
-        no underflow can happen there - confirmed empirically:
-        oe-selftest's test_devtool_ide_sdk_none_qemu (gcc toolchain, covering
-        both cmake-example and meson-example) fails when this workaround is
-        applied unconditionally to meson, while the dedicated clang tests
-        (test_devtool_ide_sdk_{code,none}_meson_clang) require it. cmake
-        (with the Ninja or Makefiles generators used here) always passes
-        absolute source paths to the compiler regardless of toolchain, so it
-        never needs this workaround either. Applying this workaround outside
-        of the meson+clang combination would incorrectly discard the correct
-        (and, for gcc/cmake, already working) comp_dir-based mapping - see the
-        'del mappings[target_path]' below - falling back to the generic
-        '/usr/src/debug' mapping to the image's (stale, whole-image-build-time)
-        rootfs-dbg instead of the live source tree.
+        meson/ninja invoke clang with a *relative* source path when B (under
+        WORKDIR) and S (relocated by 'devtool modify') only share a distant
+        common ancestor. -f*-prefix-map only rewrites paths starting with the
+        mapped host prefix, so only DW_AT_comp_dir (absolute) gets rewritten;
+        DW_AT_name stays relative. gcc always emits an absolute DW_AT_name
+        here, and cmake always passes absolute source paths regardless of
+        toolchain, so neither needs this workaround (verified by
+        oe-selftest's DevtoolIdeSdkGccTests/DevtoolIdeSdkClangTests).
 
         Debuggers resolve the compile unit path by joining DW_AT_comp_dir with
-        the relative DW_AT_name, popping one path component per leading "..".
-        If DW_AT_name contains more ".." components than DW_AT_comp_dir has
-        path components, the extra ".." are no-ops once the root is reached
-        (they can't go above "/"), so the final resolved path becomes "/"
-        followed by the leftover (non-"..") components of DW_AT_name - i.e. a
-        suffix of the real, absolute source directory rather than the
-        "/usr/src/debug/<pn>/<pv>" prefix that DEBUG_PREFIX_MAP and the
-        generated sourceMap/sourceFileMap assume.
+        DW_AT_name, popping one path component per leading "..". Once DW_AT_name
+        has more ".." than DW_AT_comp_dir has components, the extra ".." are
+        no-ops at "/", leaving a suffix of the real source directory instead of
+        the expected "/usr/src/debug/<pn>/<pv>" prefix.
 
-        This computes that resolved suffix for the recipe's own source
-        directory (S) and replaces the (now dead, since every file under S is
-        affected the same way) comp_dir-based mapping with it, so debuggers
-        relying on prefix matching (e.g. CodeLLDB, GDB) can still locate the
-        sources.
-
-        Note: the original comp_dir-based target_path is removed rather than
-        kept alongside the new one. Keeping both would mean two different
-        target paths map to the same host path (S), which is ambiguous when a
-        debugger needs to go the other way round: translating a local file
-        (opened from the host/workspace) back into a debug-info path in order
-        to resolve a source breakpoint. CodeLLDB in particular appears to
-        pick the first-registered ("normal", comp_dir-based) mapping in that
-        case, which never matches any real compile unit here, leaving the
-        breakpoint pending with 0 locations.
+        This computes that resolved suffix for S and replaces the (now dead)
+        comp_dir-based mapping with it, so prefix-matching debuggers (CodeLLDB,
+        GDB) can still find the sources. The old target_path is removed rather
+        than kept alongside: keeping both would let a debugger's reverse
+        lookup (host file -> debug-info path, e.g. to resolve a breakpoint)
+        pick the comp_dir-based one, which never matches any compile unit.
         """
         if self.build_tool is not BuildTool.MESON or self.toolchain != "clang":
             return
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [OE-core] [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release
  2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
                   ` (5 preceding siblings ...)
  2026-09-13 20:05 ` [PATCH 6/6] devtool: ide-sdk: trim _add_broken_srctree_prefix_map docstring AdrianF
@ 2026-09-16  6:48 ` Antonin Godard
  2026-09-16 14:48   ` Freihofer, Adrian
  6 siblings, 1 reply; 9+ messages in thread
From: Antonin Godard @ 2026-09-16  6:48 UTC (permalink / raw)
  To: adrian.freihofer, openembedded-core

Hi,

On Sun Sep 13, 2026 at 10:05 PM CEST, Adrian Freihofer via lists.openembedded.org wrote:
> From: Adrian Freihofer <adrian.freihofer@siemens.com>
>
> - Fix: opt-out of automatic IMAGE_INSTALL for recipes providing
>   virtual/kernel or virtual/bootloader (deployed via do_deploy, not
>   installed into the rootfs).
> - Feature: --ide now accepts a comma-separated plugin list
>   (--ide code,none), generating multiple IDE configs from one build -
>   this is what enables the test refactoring below.
> - Test overhaul: consolidate the GCC/Clang, ide=code/none and NFS/slirp
>   oe-selftest variants into one shared flow per toolchain, cutting qemu
>   boots/image builds while keeping the same coverage, plus dead-code
>   cleanup.
>
> Adrian Freihofer (6):
>   oe-selftest: devtool update: recipe drop dead code
>   devtool: ide-sdk: support opt-out from auto IMAGE_INSTALL
>   devtool: ide-sdk: add initialize_modified_recipe() plugin hook
>   devtool: ide-sdk: allow --ide to select multiple plugins
>   oe-selftest: devtool ide-sdk: consolidate GCC/Clang ide-sdk tests
>   devtool: ide-sdk: trim _add_broken_srctree_prefix_map docstring
>
>  meta/lib/oeqa/selftest/cases/devtool.py     | 1028 ++++++++-----------
>  scripts/lib/devtool/ide_plugins/__init__.py |    4 +
>  scripts/lib/devtool/ide_sdk.py              |  117 +--
>  3 files changed, 475 insertions(+), 674 deletions(-)

I've hit this issue at different occasion now:
https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/8936796/raw_inline
Parent build: https://autobuilder.yoctoproject.org/valkyrie/#/builders/23/builds/4895

Also:
https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/8936698/raw_inline

I'm on this branch:
https://git.openembedded.org/openembedded-core-contrib/log/?id=2b273bd92e22d31b751d9c0f06df3f73182ae00b

To me there's nothing that stands out that could be related to these issues,
apart from this serie which was already merged to master.

Can you have a look?

Antonin


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [OE-core] [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release
  2026-09-16  6:48 ` [OE-core] [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release Antonin Godard
@ 2026-09-16 14:48   ` Freihofer, Adrian
  0 siblings, 0 replies; 9+ messages in thread
From: Freihofer, Adrian @ 2026-09-16 14:48 UTC (permalink / raw)
  To: Antonin Godard, openembedded-core@lists.openembedded.org

Hi Antonin

The issue seams to be that we end up with too many Qemu instances in
paralle. I need to investigate further how we can solve that. But the
MAC address of Qemu is no longer valid with the 124 at the end.

qemu-system-aarch64: -device virtio-net-
pci,netdev=net0,mac=52:54:00:12:35:124

I will look at that this evening.

Regards,
Adrian

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-09-16 14:48 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-13 20:05 [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release AdrianF
2026-09-13 20:05 ` [PATCH 1/6] oe-selftest: devtool update: recipe drop dead code AdrianF
2026-09-13 20:05 ` [PATCH 2/6] devtool: ide-sdk: support opt-out from auto IMAGE_INSTALL AdrianF
2026-09-13 20:05 ` [PATCH 3/6] devtool: ide-sdk: add initialize_modified_recipe() plugin hook AdrianF
2026-09-13 20:05 ` [PATCH 4/6] devtool: ide-sdk: allow --ide to select multiple plugins AdrianF
2026-09-13 20:05 ` [PATCH 5/6] oe-selftest: devtool ide-sdk: consolidate GCC/Clang ide-sdk tests AdrianF
2026-09-13 20:05 ` [PATCH 6/6] devtool: ide-sdk: trim _add_broken_srctree_prefix_map docstring AdrianF
2026-09-16  6:48 ` [OE-core] [PATCH 0/6] devtool ide-sdk: cleanups for the upcoming release Antonin Godard
2026-09-16 14:48   ` Freihofer, Adrian

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox