public inbox for openembedded-core@lists.openembedded.org
 help / color / mirror / Atom feed
* [PATCH v2 00/14] IDE SDK Improvements
@ 2025-12-31 11:46 AdrianF
  2025-12-31 11:46 ` [PATCH v2 01/14] devtool: ide-sdk find bitbake-setup init-build-env AdrianF
                   ` (14 more replies)
  0 siblings, 15 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Changes in comparison to v1:
- Try to fix https://autobuilder.yoctoproject.org/valkyrie/#/builders/23/builds/3070
  The tests does remote debugging in Qemu. It sets a breakpoint on a line
  which is optimized out by the compiler. That's now fixed by setting the
  breakpoint on the next line, which is always there.
  So far this is understood and fixed. But what is not yet fully understood
  is: Why does it pass on x86 hosts but not on arm hosts?

- Improve remote debugging
  - Added GDB pretty-printing for C++ STL types (e.g., std::vector) to
    improve visibility during debugging.
  - Evaluated DEBUG_PREFIX_MAP for accurate source mapping in debug
    sessions.
  - Introduced gdbserver attach mode for more flexible remote debugging.
  - Moved the code that starts GDB sessions on the remote target from
    wrapper scripts into VSCode JSON files. This simplifies
    customization and improves transparency.

- Improve test coverage for ide-sdk features
  - Test the complete VSCode remote debugging workflow by reading VSCode
    JSON files.
  - Added example code using std::vector and tests for pretty-printing.
  - Extended the CMake and Meson examples with a service and added test
    coverage for the new GDB attach mode.
  - Added debug logging in DevtoolIdeSdkTests to aid troubleshooting.

- Fixes
  - Located and integrated bitbake-setup init-build-env for consistent
    environments.

- Misc
  - Added a compile step in ide-sdk tests to ensure builds are up to
    date. This is required when bitbake supports running do_install
    without dependent tasks.

Adrian Freihofer (14):
  devtool: ide-sdk find bitbake-setup init-build-env
  oe-selftest: devtool: DevtoolIdeSdkTests debug logging
  cpp-example: run as a service
  oe-selftest: devtool: check example services are running
  devtool: ide-sdk: add gdbserver attach mode support
  devtool: ide-sdk: move code to ide_none
  devtool: ide-sdk: make install_and_deploy script pass target arg
  devtool: ide-sdk: vscode replace scripts
  oe-selftest: devtool ide-sdk cover vscode remote debugging
  devtool: ide-sdk: evaluate DEBUG_PREFIX_MAP
  cpp-example: Add std::vector example
  devtool: ide-sdk: Support GDB pretty-printing for C++ STL types
  oe-selftest: devtool: add test for gdb pretty-printing
  oe-selftest: devtool: add compile step in ide-sdk tests

 .../recipes-test/cpp/cpp-example.inc          |  52 +-
 .../recipes-test/cpp/files/CMakeLists.txt     |  14 +-
 .../recipes-test/cpp/files/config.h.in        |  10 +
 .../cpp/files/cpp-example-lib.cpp             |  29 +
 .../cpp/files/cpp-example-lib.hpp             |   3 +
 .../recipes-test/cpp/files/cpp-example.conf   |   3 +
 .../recipes-test/cpp/files/cpp-example.cpp    |  46 +-
 .../recipes-test/cpp/files/cpp-example.init   |  84 +++
 .../cpp/files/cpp-example.service             |  12 +
 .../recipes-test/cpp/files/meson.build        |  18 +-
 .../cpp/files/test-cpp-example.cpp            |   2 +
 .../recipes-test/cpp/meson-example.bb         |   2 +
 meta/lib/oeqa/selftest/cases/devtool.py       | 539 +++++++++++++++---
 scripts/lib/devtool/ide_plugins/__init__.py   | 231 ++++----
 scripts/lib/devtool/ide_plugins/ide_code.py   | 159 ++++--
 scripts/lib/devtool/ide_plugins/ide_none.py   | 140 ++++-
 scripts/lib/devtool/ide_sdk.py                | 285 ++++++++-
 scripts/lib/devtool/standard.py               |   7 +-
 18 files changed, 1369 insertions(+), 267 deletions(-)
 create mode 100644 meta-selftest/recipes-test/cpp/files/config.h.in
 create mode 100644 meta-selftest/recipes-test/cpp/files/cpp-example.conf
 create mode 100644 meta-selftest/recipes-test/cpp/files/cpp-example.init
 create mode 100644 meta-selftest/recipes-test/cpp/files/cpp-example.service

-- 
2.52.0



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

* [PATCH v2 01/14] devtool: ide-sdk find bitbake-setup init-build-env
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 02/14] oe-selftest: devtool: DevtoolIdeSdkTests debug logging AdrianF
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

With poky the oe-init-build-env script from the top level directory of
the layer with the higher priority is used to setup the build
environment.
This does no longer work with bitbake-setup. The directory layout
changed and the script is now called init-build-env. Skip the old
implementation if $TOPDIR/init-build-env exists and use it instead.

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

diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 87a4c13ec5..9df88454c7 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -269,6 +269,7 @@ class RecipeNotModified:
 class RecipeModified:
     """Handling of recipes in the workspace created by devtool modify"""
     OE_INIT_BUILD_ENV = 'oe-init-build-env'
+    INIT_BUILD_ENV = 'init-build-env'
 
     VALID_BASH_ENV_NAME_CHARS = re.compile(r"^[a-zA-Z0-9_]*$")
 
@@ -743,7 +744,13 @@ class RecipeModified:
 
     @property
     def oe_init_build_env(self):
-        """Find the oe-init-build-env used for this setup"""
+        """Find the init-build-env used for this setup"""
+        # bitbake-setup mode
+        bb_setup_init = os.path.join(self.topdir, RecipeModified.INIT_BUILD_ENV)
+        if os.path.exists(bb_setup_init):
+            return os.path.abspath(bb_setup_init)
+
+        # poky mode
         oe_init_dir = self.oe_init_dir
         if oe_init_dir:
             return os.path.join(oe_init_dir, RecipeModified.OE_INIT_BUILD_ENV)
-- 
2.52.0



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

* [PATCH v2 02/14] oe-selftest: devtool: DevtoolIdeSdkTests debug logging
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
  2025-12-31 11:46 ` [PATCH v2 01/14] devtool: ide-sdk find bitbake-setup init-build-env AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 03/14] cpp-example: run as a service AdrianF
                   ` (12 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Add optional debug logging to all runCmd calls in DevtoolIdeSdkTests
to improve debugging capabilities when tests fail. The logging is only
enabled when the test logger is set to DEBUG level.

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

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index cf5ac6e9d7..9bbba2ec89 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -13,6 +13,7 @@ import glob
 import fnmatch
 import unittest
 import json
+import logging
 
 from oeqa.selftest.case import OESelftestTestCase
 from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer
@@ -2524,6 +2525,13 @@ class DevtoolUpgradeTests(DevtoolBase):
 
 
 class DevtoolIdeSdkTests(DevtoolBase):
+
+    def setUp(self):
+        super().setUp()
+        self._cmd_logger = None
+        if self.logger.isEnabledFor(logging.DEBUG):
+            self._cmd_logger = self.logger
+
     def _write_bb_config(self, recipe_names):
         """Helper to write the bitbake local.conf file"""
         conf_lines = [
@@ -2563,7 +2571,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.track_for_cleanup(tempdir)
         self.add_command_to_tearDown('bitbake -c clean %s' % recipe_name)
 
-        result = runCmd('devtool modify %s -x %s --debug-build' % (recipe_name, tempdir))
+        result = runCmd('devtool modify %s -x %s --debug-build' % (recipe_name, tempdir), output_log=self._cmd_logger)
         self.assertExists(os.path.join(tempdir, build_file),
                           'Extracted source could not be found')
         self.assertExists(os.path.join(self.workspacedir, 'conf',
@@ -2573,7 +2581,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertTrue(matches, 'bbappend not created %s' % result.output)
 
         # Test devtool status
-        result = runCmd('devtool status')
+        result = runCmd('devtool status', output_log=self._cmd_logger)
         self.assertIn(recipe_name, result.output)
         self.assertIn(tempdir, result.output)
         self._check_src_repo(tempdir)
@@ -2629,7 +2637,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             self._workspace_scripts_dir(recipe_name), i_and_d_script)
         self.assertExists(install_deploy_cmd,
                           '%s script not found' % install_deploy_cmd)
-        runCmd(install_deploy_cmd)
+        runCmd(install_deploy_cmd, output_log=self._cmd_logger)
 
         MAGIC_STRING_ORIG = "Magic: 123456789"
         MAGIC_STRING_NEW = "Magic: 987654321"
@@ -2662,7 +2670,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             cpp_code = cpp_code.replace(MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
         with open(cpp_example_lib_hpp, 'w') as file:
             file.write(cpp_code)
-        runCmd(install_deploy_cmd, cwd=tempdir)
+        runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger)
 
         # Verify the modified example prints the modified magic string
         status, output = qemu.run(example_exe)
@@ -2689,7 +2697,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
 
         native_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", gdb_recipe)
         r = runCmd("%s --version" % gdb_binary,
-                   native_sysroot=native_sysroot, target_sys=target_sys)
+                   native_sysroot=native_sysroot, target_sys=target_sys, output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
         self.assertIn("GNU gdb", r.output)
 
@@ -2717,18 +2725,18 @@ class DevtoolIdeSdkTests(DevtoolBase):
             recipe_name), 'gdb_1234_usr-bin-' + example_exe)
 
         # Start a gdbserver
-        r = runCmd(gdbserver_script)
+        r = runCmd(gdbserver_script, output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
 
         # Check there is a gdbserver running
-        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'))
+        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'), output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
         self.assertIn("gdbserver ", r.output)
 
         # Check the pid file is correct
         test_cmd = "cat /proc/$(cat /tmp/gdbserver_1234_usr-bin-" + \
             example_exe + "/pid)/cmdline"
-        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd))
+        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd), output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
         self.assertIn("gdbserver", r.output)
 
@@ -2739,7 +2747,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
         gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:13,13'"
         gdb_batch_cmd += " -ex 'continue'"
-        r = runCmd(gdb_script + gdb_batch_cmd)
+        r = runCmd(gdb_script + gdb_batch_cmd, output_log=self._cmd_logger)
         self.logger.debug("%s %s returned: %s", gdb_script,
                           gdb_batch_cmd, r.output)
         self.assertEqual(r.status, 0)
@@ -2751,11 +2759,11 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("exited normally", r.output)
 
         # Stop the gdbserver
-        r = runCmd(gdbserver_script + ' stop')
+        r = runCmd(gdbserver_script + ' stop', output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
 
         # Check there is no gdbserver running
-        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'))
+        r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'), output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
         self.assertNotIn("gdbserver ", r.output)
 
@@ -2776,29 +2784,29 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertExists(cmake_exe)
 
         # Verify the cmake preset generated by devtool ide-sdk is available
-        result = runCmd('%s --list-presets' % cmake_exe, cwd=tempdir)
+        result = runCmd('%s --list-presets' % cmake_exe, cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn(preset_name, result.output)
 
         # Verify cmake re-uses the o files compiled by bitbake
         result = runCmd('%s --build --preset %s' %
-                        (cmake_exe, preset_name), cwd=tempdir)
+                        (cmake_exe, preset_name), 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)
         result = runCmd('%s --build --preset %s --target test' %
-                        (cmake_exe, preset_name), cwd=tempdir)
+                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("100% tests passed", result.output)
 
         # Verify re-building and testing works again
         result = runCmd('%s --build --preset %s --target clean' %
-                        (cmake_exe, preset_name), cwd=tempdir)
+                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("Cleaning", result.output)
         result = runCmd('%s --build --preset %s' %
-                        (cmake_exe, preset_name), cwd=tempdir)
+                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("Building", result.output)
         self.assertIn("Linking", result.output)
         result = runCmd('%s --build --preset %s --target test' %
-                        (cmake_exe, preset_name), cwd=tempdir)
+                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("Running tests...", result.output)
         self.assertIn("100% tests passed", result.output)
 
@@ -2823,7 +2831,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
                 recipe_name, build_file, testimage)
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
                 recipe_name, testimage, qemu.ip)
-            runCmd(bitbake_sdk_cmd)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
             self._gdb_cross()
             self._verify_cmake_preset(tempdir)
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
@@ -2839,7 +2847,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
                 recipe_name, build_file, testimage)
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
                 recipe_name, testimage, qemu.ip)
-            runCmd(bitbake_sdk_cmd)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
             self._gdb_cross()
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
             # Verify the oe-scripts sym-link is valid
@@ -2858,7 +2866,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             recipe_name, build_file, testimage)
         bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
             recipe_name, testimage)
-        runCmd(bitbake_sdk_cmd)
+        runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
         self._verify_cmake_preset(tempdir)
         self._verify_install_script_code(tempdir,  recipe_name)
         self._gdb_cross()
@@ -2875,7 +2883,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             recipe_name, build_file, testimage)
         bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
             recipe_name, testimage)
-        runCmd(bitbake_sdk_cmd)
+        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)
@@ -2887,20 +2895,22 @@ class DevtoolIdeSdkTests(DevtoolBase):
 
         # Verify meson re-uses the o files compiled by bitbake
         result = runCmd('%s compile -C  %s' %
-                        (meson_exe, meson_build_folder), cwd=tempdir)
+                        (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)
-        runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir)
+        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)
+                        (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)
+                        (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)
+        runCmd('%s test -C %s' % (meson_exe, meson_build_folder), cwd=tempdir,
+               output_log=self._cmd_logger)
 
         self._verify_install_script_code(tempdir,  recipe_name)
         self._gdb_cross()
@@ -2912,7 +2922,8 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self._check_workspace()
 
         result_init = runCmd(
-            'devtool ide-sdk -m shared oe-selftest-image cmake-example meson-example --ide=code')
+            'devtool ide-sdk -m shared oe-selftest-image cmake-example meson-example --ide=code',
+            output_log=self._cmd_logger)
         bb_vars = get_bb_vars(
             ['REAL_MULTIMACH_TARGET_SYS', 'DEPLOY_DIR_IMAGE', 'COREBASE'], "meta-ide-support")
         environment_script = 'environment-setup-%s' % bb_vars['REAL_MULTIMACH_TARGET_SYS']
@@ -2924,21 +2935,21 @@ class DevtoolIdeSdkTests(DevtoolBase):
         # Verify the cross environment script is available
         self.assertExists(environment_script_path)
 
-        def runCmdEnv(cmd, cwd):
+        def runCmdEnv(cmd, cwd, output_log=self._cmd_logger):
             cmd = '/bin/sh -c ". %s > /dev/null && %s"' % (
                 environment_script_path, cmd)
-            return runCmd(cmd, cwd)
+            return runCmd(cmd, cwd, output_log=output_log)
 
         # Verify building the C++ example works with CMake
         tempdir_cmake = tempfile.mkdtemp(prefix='devtoolqa')
         self.track_for_cleanup(tempdir_cmake)
 
-        result_cmake = runCmdEnv("which cmake", cwd=tempdir_cmake)
+        result_cmake = runCmdEnv("which cmake", cwd=tempdir_cmake, output_log=self._cmd_logger)
         cmake_native = os.path.normpath(result_cmake.output.strip())
         self.assertExists(cmake_native)
 
-        runCmdEnv('cmake %s' % cpp_example_src, cwd=tempdir_cmake)
-        runCmdEnv('cmake --build %s' % tempdir_cmake, cwd=tempdir_cmake)
+        runCmdEnv('cmake %s' % cpp_example_src, cwd=tempdir_cmake, output_log=self._cmd_logger)
+        runCmdEnv('cmake --build %s' % tempdir_cmake, cwd=tempdir_cmake, output_log=self._cmd_logger)
 
         # Verify the printed note really referres to a cmake executable
         cmake_native_code = ""
@@ -2954,12 +2965,12 @@ class DevtoolIdeSdkTests(DevtoolBase):
         tempdir_meson = tempfile.mkdtemp(prefix='devtoolqa')
         self.track_for_cleanup(tempdir_meson)
 
-        result_cmake = runCmdEnv("which meson", cwd=tempdir_meson)
+        result_cmake = runCmdEnv("which meson", cwd=tempdir_meson, output_log=self._cmd_logger)
         meson_native = os.path.normpath(result_cmake.output.strip())
         self.assertExists(meson_native)
 
-        runCmdEnv('meson setup %s' % tempdir_meson, cwd=cpp_example_src)
-        runCmdEnv('meson compile', cwd=tempdir_meson)
+        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 test_devtool_ide_sdk_plugins(self):
         """Test that devtool ide-sdk can use plugins from other layers."""
@@ -2982,7 +2993,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             return m.group(1).split(',')
 
         # verify the default plugins are available but the foo plugin is not
-        result = runCmd('devtool ide-sdk -h')
+        result = runCmd('devtool ide-sdk -h', output_log=self._cmd_logger)
         found_ides = get_ides_from_help(result.output)
         self.assertIn('code', found_ides)
         self.assertIn('none', found_ides)
@@ -3013,7 +3024,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             plugin_file.write(plugin_code)
 
         # Verify the foo plugin is available as well
-        result = runCmd('devtool ide-sdk -h')
+        result = runCmd('devtool ide-sdk -h', output_log=self._cmd_logger)
         found_ides = get_ides_from_help(result.output)
         self.assertIn('code', found_ides)
         self.assertIn('none', found_ides)
@@ -3021,14 +3032,15 @@ class DevtoolIdeSdkTests(DevtoolBase):
 
         # Verify the foo plugin generates a shared config
         result = runCmd(
-            'devtool ide-sdk -m shared --skip-bitbake --ide foo %s' % shared_recipe_name)
+            'devtool ide-sdk -m shared --skip-bitbake --ide foo %s' % shared_recipe_name,
+            output_log=self._cmd_logger)
         with open(shared_config_file) as shared_config:
             shared_config_new = shared_config.read()
         self.assertEqual(shared_config_str, shared_config_new)
 
         # Verify the foo plugin generates a modified config
         result = runCmd('devtool ide-sdk --skip-bitbake --ide foo %s %s' %
-                        (modified_recipe_name, testimage))
+                        (modified_recipe_name, testimage), output_log=self._cmd_logger)
         with open(modified_config_file) as modified_config:
             modified_config_new = modified_config.read()
         self.assertEqual(modified_config_str, modified_config_new)
-- 
2.52.0



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

* [PATCH v2 03/14] cpp-example: run as a service
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
  2025-12-31 11:46 ` [PATCH v2 01/14] devtool: ide-sdk find bitbake-setup init-build-env AdrianF
  2025-12-31 11:46 ` [PATCH v2 02/14] oe-selftest: devtool: DevtoolIdeSdkTests debug logging AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 04/14] oe-selftest: devtool: check example services are running AdrianF
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Extend the C++ example to run as systemd/SysV services

This change adds service capability to the existing C++ example without
modifying its original behavior. The example can now run either as:
- One-shot executables (existing behavior)
- Long-running services via systemd or SysV init

The service runs as an unprivileged user/group, demonstrating security
best practices for service development. This introduces additional
complexity to the build process, particularly around proper pseudo usage
in development builds. The implementation includes:
- Service configuration files (systemd .service and SysV init script)
- Dedicated user/group creation with appropriate permissions
- JSON configuration file for runtime customization, owned by the
  service user
- Command-line --endless flag to enable service mode
- Full support for both CMake and Meson build systems

This enhancement enables testing debugger configurations that attach to
running processes, expanding the examples' utility for development tools.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 .../recipes-test/cpp/cpp-example.inc          | 52 +++++++++++-
 .../recipes-test/cpp/files/CMakeLists.txt     | 14 +++-
 .../recipes-test/cpp/files/config.h.in        | 10 +++
 .../cpp/files/cpp-example-lib.cpp             | 29 +++++++
 .../cpp/files/cpp-example-lib.hpp             |  3 +
 .../recipes-test/cpp/files/cpp-example.conf   |  3 +
 .../recipes-test/cpp/files/cpp-example.cpp    | 38 ++++++++-
 .../recipes-test/cpp/files/cpp-example.init   | 84 +++++++++++++++++++
 .../cpp/files/cpp-example.service             | 12 +++
 .../recipes-test/cpp/files/meson.build        | 18 +++-
 .../cpp/files/test-cpp-example.cpp            |  2 +
 .../recipes-test/cpp/meson-example.bb         |  2 +
 meta/lib/oeqa/selftest/cases/devtool.py       |  4 +-
 13 files changed, 264 insertions(+), 7 deletions(-)
 create mode 100644 meta-selftest/recipes-test/cpp/files/config.h.in
 create mode 100644 meta-selftest/recipes-test/cpp/files/cpp-example.conf
 create mode 100644 meta-selftest/recipes-test/cpp/files/cpp-example.init
 create mode 100644 meta-selftest/recipes-test/cpp/files/cpp-example.service

diff --git a/meta-selftest/recipes-test/cpp/cpp-example.inc b/meta-selftest/recipes-test/cpp/cpp-example.inc
index 76ff64e87f..2653f45e90 100644
--- a/meta-selftest/recipes-test/cpp/cpp-example.inc
+++ b/meta-selftest/recipes-test/cpp/cpp-example.inc
@@ -16,9 +16,59 @@ SRC_URI = "\
     file://cpp-example-lib.hpp \
     file://cpp-example-lib.cpp \
     file://test-cpp-example.cpp \
+    file://cpp-example.conf \
+    file://config.h.in \
+    file://cpp-example.service \
+    file://cpp-example.init \
     file://run-ptest \
 "
 
 S = "${UNPACKDIR}"
 
-inherit ptest
+inherit ptest useradd systemd update-rc.d
+
+# Systemd and SysV init support
+SYSTEMD_SERVICE:${PN} = "${BPN}.service"
+
+INITSCRIPT_NAME = "${BPN}"
+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}"
+
+EX_BINARY_NAME ?= "${BPN}"
+
+do_install:append() {
+    # Install configuration file owned by unprivileged user
+    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
+
+    # Install service files or init scripts and substitute placeholders in service files
+    if ${@bb.utils.contains('DISTRO_FEATURES', 'systemd', 'true', 'false', d)}; then
+        install -d ${D}${systemd_system_unitdir}
+        install -m 0644 ${S}/cpp-example.service ${D}${systemd_system_unitdir}/${BPN}.service
+        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' \
+            ${D}${systemd_system_unitdir}/${BPN}.service
+    else
+        install -d ${D}${sysconfdir}/init.d
+        install -m 0755 ${S}/cpp-example.init ${D}${sysconfdir}/init.d/${BPN}
+        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' \
+            ${D}${sysconfdir}/init.d/${BPN}
+    fi
+}
+
+FILES:${PN} += " \
+    ${systemd_system_unitdir}/${BPN}.service \
+    ${sysconfdir}/${BPN}.conf \
+"
diff --git a/meta-selftest/recipes-test/cpp/files/CMakeLists.txt b/meta-selftest/recipes-test/cpp/files/CMakeLists.txt
index 6fa6917d89..e363f31af2 100644
--- a/meta-selftest/recipes-test/cpp/files/CMakeLists.txt
+++ b/meta-selftest/recipes-test/cpp/files/CMakeLists.txt
@@ -20,15 +20,25 @@ 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")
+
+# Generate config.h from config.h.in
+configure_file(config.h.in config.h @ONLY)
+
 # Linking a small library makes the example more useful for testing.
 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 
+set_target_properties(cmake-example-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_link_libraries(cmake-example-lib PRIVATE json-c::json-c)
 
 install(TARGETS cmake-example-lib
@@ -39,6 +49,7 @@ install(TARGETS cmake-example-lib
 
 # 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)
 
 install(TARGETS cmake-example
@@ -47,6 +58,7 @@ install(TARGETS cmake-example
 
 # 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)
 
 if (FAILING_TEST)
diff --git a/meta-selftest/recipes-test/cpp/files/config.h.in b/meta-selftest/recipes-test/cpp/files/config.h.in
new file mode 100644
index 0000000000..174e266847
--- /dev/null
+++ b/meta-selftest/recipes-test/cpp/files/config.h.in
@@ -0,0 +1,10 @@
+/*
+ * Copyright OpenEmbedded Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+/* Configuration file path */
+#define EXAMPLE_CONFIG_PATH "@CPP_EXAMPLE_CONFIG_PATH@"
diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.cpp b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.cpp
index d3dc976864..c510a13893 100644
--- a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.cpp
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.cpp
@@ -6,6 +6,7 @@
 
 #include <iostream>
 #include <string>
+#include <fstream>
 #include <json-c/json.h>
 #include "cpp-example-lib.hpp"
 
@@ -31,3 +32,31 @@ void CppExample::print_json()
 
     json_object_put(jobj); // Delete the json object
 }
+
+std::string CppExample::read_config_message(const std::string &config_path)
+{
+    std::ifstream config_file(config_path);
+    if (!config_file.is_open()) {
+        return "Error: Could not open config file: " + config_path;
+    }
+
+    std::string config_content((std::istreambuf_iterator<char>(config_file)),
+                               std::istreambuf_iterator<char>());
+    config_file.close();
+
+    struct json_object *jobj = json_tokener_parse(config_content.c_str());
+    if (!jobj) {
+        return "Error: Invalid JSON in config file";
+    }
+
+    struct json_object *message_obj;
+    if (json_object_object_get_ex(jobj, "hello_world_message", &message_obj)) {
+        const char *message = json_object_get_string(message_obj);
+        std::string result = message ? message : "Error: Invalid message format";
+        json_object_put(jobj);
+        return result;
+    }
+
+    json_object_put(jobj);
+    return "Error: 'hello_world_message' not found in config file";
+}
diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp
index 0ad9e7b7b2..24dd0defb6 100644
--- a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp
@@ -7,6 +7,7 @@
 #pragma once
 
 #include <string>
+#include "config.h"
 
 struct CppExample
 {
@@ -18,4 +19,6 @@ struct CppExample
     const char *get_json_c_version();
     /* Call a more advanced function from a library */
     void print_json();
+    /* Read hello world message from config file */
+    std::string read_config_message(const std::string &config_path = EXAMPLE_CONFIG_PATH);
 };
diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.conf b/meta-selftest/recipes-test/cpp/files/cpp-example.conf
new file mode 100644
index 0000000000..4a666e5cdd
--- /dev/null
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example.conf
@@ -0,0 +1,3 @@
+{
+  "hello_world_message": "Hello World from @BINARY_NAME@ example config file!"
+}
diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp
index 9889554e0c..dbf82f15d9 100644
--- a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp
@@ -7,12 +7,48 @@
 #include "cpp-example-lib.hpp"
 
 #include <iostream>
+#include <unistd.h>
+#include <string>
 
-int main()
+int main(int argc, char* argv[])
 {
+    bool endless_mode = false;
+
+    // Parse command line arguments
+    for (int i = 1; i < argc; i++) {
+        if (std::string(argv[i]) == "--endless") {
+            endless_mode = true;
+        } else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
+            std::cout << "Usage: " << argv[0] << " [OPTIONS]" << std::endl;
+            std::cout << "Options:" << std::endl;
+            std::cout << "  --endless    Run in endless loop mode (for service)" << std::endl;
+            std::cout << "  --help, -h   Show this help message" << std::endl;
+            return 0;
+        }
+    }
+
     auto cpp_example = CppExample();
+
+    if (endless_mode) {
+        std::cout << "Starting cpp-example service in endless mode..." << std::endl;
+    } else {
+        std::cout << "Running cpp-example once..." << std::endl;
+    }
+
     std::cout << "C++ example linking " << cpp_example.get_string() << std::endl;
     std::cout << "Linking json-c version " << cpp_example.get_json_c_version() << std::endl;
     cpp_example.print_json();
+
+    do {
+        // Read and print message from config file
+        std::string config_message = cpp_example.read_config_message();
+        std::cout << "Config file message: " << config_message << std::endl;
+
+        if (endless_mode) {
+            // Sleep for 1 second
+            sleep(1);
+        }
+    } while (endless_mode);
+
     return 0;
 }
diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.init b/meta-selftest/recipes-test/cpp/files/cpp-example.init
new file mode 100644
index 0000000000..c154fd1126
--- /dev/null
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example.init
@@ -0,0 +1,84 @@
+#!/bin/sh
+#
+# cpp-example        C++ Example Service
+#
+# chkconfig: 35 99 99
+# description: C++ Example Service daemon
+#
+
+USER="@USER@"
+DAEMON="@BINARY_NAME@"
+DAEMON_PATH="@BINDIR@/$DAEMON"
+DAEMON_ARGS="--endless"
+PIDFILE="/var/run/$DAEMON.pid"
+LOCK_FILE="/var/lock/subsys/$DAEMON"
+
+start() {
+    if [ -f $PIDFILE ]; then
+        echo "$DAEMON is already running."
+        return 1
+    fi
+
+    echo -n "Starting $DAEMON: "
+    start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \
+        --background --chuid $USER --exec $DAEMON_PATH -- $DAEMON_ARGS
+    RETVAL=$?
+    if [ $RETVAL -eq 0 ]; then
+        echo "OK"
+        touch $LOCK_FILE
+    else
+        echo "FAILED"
+    fi
+    return $RETVAL
+}
+
+stop() {
+    echo -n "Stopping $DAEMON: "
+    start-stop-daemon --stop --quiet --pidfile $PIDFILE
+    RETVAL=$?
+    if [ $RETVAL -eq 0 ]; then
+        echo "OK"
+        rm -f $PIDFILE $LOCK_FILE
+    else
+        echo "FAILED"
+    fi
+    return $RETVAL
+}
+
+status() {
+    if [ -f $PIDFILE ]; then
+        PID=$(cat $PIDFILE)
+        if ps -p $PID > /dev/null 2>&1; then
+            echo "$DAEMON is running (PID: $PID)"
+            return 0
+        else
+            echo "$DAEMON is not running (stale PID file)"
+            return 1
+        fi
+    else
+        echo "$DAEMON is not running"
+        return 1
+    fi
+}
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    status)
+        status
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|restart|status}"
+        exit 1
+        ;;
+esac
+
+exit $?
diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.service b/meta-selftest/recipes-test/cpp/files/cpp-example.service
new file mode 100644
index 0000000000..4022fa291a
--- /dev/null
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=C++ Example Service
+After=network.target
+
+[Service]
+Type=simple
+User=@USER@
+Group=@GROUP@
+ExecStart=@BINDIR@/@BINARY_NAME@ --endless
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meta-selftest/recipes-test/cpp/files/meson.build b/meta-selftest/recipes-test/cpp/files/meson.build
index 74a0e0173c..53248c4380 100644
--- a/meta-selftest/recipes-test/cpp/files/meson.build
+++ b/meta-selftest/recipes-test/cpp/files/meson.build
@@ -16,23 +16,37 @@ if get_option('FAILING_TEST').enabled()
     add_project_arguments('-DFAIL_COMPARISON_STR=foo', language: 'cpp')
 endif
 
+# Generate config.h from config.h.in
+config_path = get_option('sysconfdir') / 'meson-example.conf'
+conf_data = configuration_data()
+conf_data.set('CPP_EXAMPLE_CONFIG_PATH', config_path)
+configure_file(input : 'config.h.in',
+               output : 'config.h',
+               configuration : conf_data)
+
+# Include the build directory for config.h
+inc_dir = include_directories('.')
+
 mesonexlib = shared_library('mesonexlib',
     'cpp-example-lib.cpp', 'cpp-example-lib.hpp',
-	version: meson.project_version(),
-	soversion: meson.project_version().split('.')[0],
+    version: meson.project_version(),
+    soversion: meson.project_version().split('.')[0],
     dependencies : jsoncdep,
+    include_directories : inc_dir,
     install : true
     )
 
 executable('mesonex',
     'cpp-example.cpp',
     link_with : mesonexlib,
+    include_directories : inc_dir,
     install : true
     )
 
 test_mesonex = executable('test-mesonex',
     'test-cpp-example.cpp',
     link_with : mesonexlib,
+    include_directories : inc_dir,
     install : true
 )
 
diff --git a/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp b/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp
index 83c9bfa844..e1909c3168 100644
--- a/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp
+++ b/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp
@@ -22,4 +22,6 @@ int main() {
         std::cout << "FAIL: " << ret_string << " != " << CppExample::test_string << std::endl;
         return 1;
     }
+
+    return 0;
 }
diff --git a/meta-selftest/recipes-test/cpp/meson-example.bb b/meta-selftest/recipes-test/cpp/meson-example.bb
index 14a7ca8dc9..da0ea18376 100644
--- a/meta-selftest/recipes-test/cpp/meson-example.bb
+++ b/meta-selftest/recipes-test/cpp/meson-example.bb
@@ -25,3 +25,5 @@ do_run_tests () {
 do_run_tests[doc] = "Run meson test using qemu-user"
 
 addtask do_run_tests after do_compile
+
+EX_BINARY_NAME = "mesonex"
diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 9bbba2ec89..681dcee08c 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -2714,7 +2714,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         $1 = 0
         print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")
         $2 = -3
-        list cpp-example-lib.hpp:13,13
+        list cpp-example-lib.hpp:14,14
         13	    inline static const std::string test_string = "cpp-example-lib Magic: 123456789";
         continue
         """
@@ -2745,7 +2745,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
         gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
         gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
-        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:13,13'"
+        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
         gdb_batch_cmd += " -ex 'continue'"
         r = runCmd(gdb_script + gdb_batch_cmd, output_log=self._cmd_logger)
         self.logger.debug("%s %s returned: %s", gdb_script,
-- 
2.52.0



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

* [PATCH v2 04/14] oe-selftest: devtool: check example services are running
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (2 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 03/14] cpp-example: run as a service AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 05/14] devtool: ide-sdk: add gdbserver attach mode support AdrianF
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

When running the devtool ide-sdk test with qemu, verify that the example
services are actually running on the target by using pgrep to check for
the example executable names.

Also verify that the configuration files in /etc are owned by the proper
user and group, both before and after the install_and_deploy scripts
have run. This is also a check that the install_and_deploy scripts
are working correctly with pseudo.

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

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 681dcee08c..e78e6ed226 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -2810,6 +2810,22 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("Running tests...", result.output)
         self.assertIn("100% tests passed", result.output)
 
+    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)
+        self.assertEqual(status, 0, msg="%s service not running: %s" %
+                         (service_name, output))
+        self.assertTrue(output.strip().isdigit(),
+                        f"pgrep output should be a PID integer, got: {output.strip()}")
+
+    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))
+
     @OETestTag("runqemu")
     def test_devtool_ide_sdk_none_qemu(self):
         """Start qemu-system and run tests for multiple recipes. ide=none is used."""
@@ -2826,7 +2842,16 @@ class DevtoolIdeSdkTests(DevtoolBase):
             # cmake-example recipe
             recipe_name = "cmake-example"
             example_exe = "cmake-example"
+            example_user_group = "cmake-example"
+            conf_file = "/etc/cmake-example.conf"
             build_file = "CMakeLists.txt"
+
+            # 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)
+
+            # Setup the recipe with devtool ide-sdk cmake-example ...
             tempdir = self._devtool_ide_sdk_recipe(
                 recipe_name, build_file, testimage)
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
@@ -2835,14 +2860,29 @@ class DevtoolIdeSdkTests(DevtoolBase):
             self._gdb_cross()
             self._verify_cmake_preset(tempdir)
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
+
             # Verify the oe-scripts sym-link is valid
             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
+            # 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"
             build_file = "meson.build"
+
+            # 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)
+
+            # Setup the recipe with devtool ide-sdk meson-example ...
             tempdir = self._devtool_ide_sdk_recipe(
                 recipe_name, build_file, testimage)
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
@@ -2850,10 +2890,15 @@ class DevtoolIdeSdkTests(DevtoolBase):
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
             self._gdb_cross()
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
+
             # Verify the oe-scripts sym-link is valid
             self.assertEqual(self._workspace_scripts_dir(
                 recipe_name), self._sources_scripts_dir(tempdir))
 
+            # 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 test_devtool_ide_sdk_code_cmake(self):
         """Verify a cmake recipe works with ide=code mode"""
         recipe_name = "cmake-example"
-- 
2.52.0



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

* [PATCH v2 05/14] devtool: ide-sdk: add gdbserver attach mode support
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (3 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 04/14] oe-selftest: devtool: check example services are running AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 06/14] devtool: ide-sdk: move code to ide_none AdrianF
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Enhance remote debugging configuration to support multiple modes
per executable binary. This adds support for gdbserver's attach
mode as an additional debug configuration.

When the binary is detected to run as a systemd service or SysV
init script, an attach debug configuration is generated alongside
the regular configuration that starts the process via gdbserver.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 meta/lib/oeqa/selftest/cases/devtool.py     |  16 +-
 scripts/lib/devtool/ide_plugins/__init__.py | 218 +++++++++++++-------
 scripts/lib/devtool/ide_plugins/ide_code.py |  99 ++++++---
 scripts/lib/devtool/ide_plugins/ide_none.py |  14 +-
 scripts/lib/devtool/ide_sdk.py              | 172 ++++++++++++++-
 5 files changed, 406 insertions(+), 113 deletions(-)

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index e78e6ed226..b40323c109 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -2660,7 +2660,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("PASS: cpp-example-lib", output)
 
         # Verify remote debugging works
-        self._gdb_cross_debugging(
+        self._gdb_cross_debugging_multi(
             qemu, recipe_name, example_exe, MAGIC_STRING_ORIG)
 
         # Replace the Magic String in the code, compile and deploy to Qemu
@@ -2685,7 +2685,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("PASS: cpp-example-lib", output)
 
         # Verify remote debugging works wit the modified magic string
-        self._gdb_cross_debugging(
+        self._gdb_cross_debugging_multi(
             qemu, recipe_name, example_exe, MAGIC_STRING_NEW)
 
     def _gdb_cross(self):
@@ -2701,7 +2701,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertEqual(r.status, 0)
         self.assertIn("GNU gdb", r.output)
 
-    def _gdb_cross_debugging(self, qemu, recipe_name, example_exe, magic_string):
+    def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string):
         """Verify gdb-cross is working
 
         Test remote debugging:
@@ -2720,7 +2720,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         """
         sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
         gdbserver_script = os.path.join(self._workspace_scripts_dir(
-            recipe_name), 'gdbserver_1234_usr-bin-' + example_exe + '_m')
+            recipe_name), 'gdbserver_1234_usr-bin-' + example_exe + '_multi')
         gdb_script = os.path.join(self._workspace_scripts_dir(
             recipe_name), 'gdb_1234_usr-bin-' + example_exe)
 
@@ -2734,11 +2734,13 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("gdbserver ", r.output)
 
         # Check the pid file is correct
-        test_cmd = "cat /proc/$(cat /tmp/gdbserver_1234_usr-bin-" + \
-            example_exe + "/pid)/cmdline"
+        test_cmd = "'cat /proc/$(cat /tmp/gdbserver_1234_usr-bin-" + \
+            example_exe + "_multi/gdbserver.pid)/cmdline'"
         r = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, test_cmd), output_log=self._cmd_logger)
         self.assertEqual(r.status, 0)
         self.assertIn("gdbserver", r.output)
+        self.assertIn("--multi", r.output)
+        self.assertIn("1234", r.output)
 
         # Test remote debugging works
         gdb_batch_cmd = " --batch -ex 'break main' -ex 'run'"
@@ -2747,6 +2749,8 @@ class DevtoolIdeSdkTests(DevtoolBase):
         gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
         gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
         gdb_batch_cmd += " -ex 'continue'"
+        return gdb_batch_cmd
+
         r = runCmd(gdb_script + gdb_batch_cmd, output_log=self._cmd_logger)
         self.logger.debug("%s %s returned: %s", gdb_script,
                           gdb_batch_cmd, r.output)
diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py
index 19c2f61c5f..70f47d6e68 100644
--- a/scripts/lib/devtool/ide_plugins/__init__.py
+++ b/scripts/lib/devtool/ide_plugins/__init__.py
@@ -31,88 +31,155 @@ class BuildTool(Enum):
         return False
 
 
+class GdbServerModes(Enum):
+    ONCE = auto()
+    ATTACH = auto()
+    MULTI = auto()
+
+
 class GdbCrossConfig:
     """Base class defining the GDB configuration generator interface
 
     Generate a GDB configuration for a binary on the target device.
-    Only one instance per binary is allowed. This allows to assign unique port
-    numbers for all gdbserver instances.
     """
     _gdbserver_port_next = 1234
-    _binaries = []
+    _gdb_cross_configs = {}
 
-    def __init__(self, image_recipe, modified_recipe, binary, gdbserver_multi=True):
+    def __init__(self, image_recipe, modified_recipe, binary, gdbserver_default_mode):
         self.image_recipe = image_recipe
         self.modified_recipe = modified_recipe
         self.gdb_cross = modified_recipe.gdb_cross
         self.binary = binary
-        if binary in GdbCrossConfig._binaries:
-            raise DevtoolError(
-                "gdbserver config for binary %s is already generated" % binary)
-        GdbCrossConfig._binaries.append(binary)
-        self.script_dir = modified_recipe.ide_sdk_scripts_dir
-        self.gdbinit_dir = os.path.join(self.script_dir, 'gdbinit')
-        self.gdbserver_multi = gdbserver_multi
-        self.binary_pretty = self.binary.replace(os.sep, '-').lstrip('-')
+        self.gdbserver_default_mode = gdbserver_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)
-        # gdbserver start script
-        gdbserver_script_file = 'gdbserver_' + self.id_pretty
-        if self.gdbserver_multi:
-            gdbserver_script_file += "_m"
-        self.gdbserver_script = os.path.join(
-            self.script_dir, gdbserver_script_file)
-        # gdbinit file
-        self.gdbinit = os.path.join(
+
+        # Track all generated gdbserver configs to avoid duplicates
+        if self.id_pretty in GdbCrossConfig._gdb_cross_configs:
+            raise DevtoolError(
+                "gdbserver config for binary %s is already generated" % binary)
+        GdbCrossConfig._gdb_cross_configs[self.id_pretty] = self
+
+    def id_pretty_mode(self, gdbserver_mode):
+        return "%s_%s" % (self.id_pretty, gdbserver_mode.name.lower())
+
+    # GDB and gdbserver script on the host
+    @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 gdbserver_script_file(self, gdbserver_mode):
+        return 'gdbserver_' + self.id_pretty_mode(gdbserver_mode)
+
+    def gdbserver_script(self, gdbserver_mode):
+        return os.path.join(self.script_dir, self.gdbserver_script_file(gdbserver_mode))
+
+    @property
+    def gdbinit(self):
+        return os.path.join(
             self.gdbinit_dir, 'gdbinit_' + self.id_pretty)
-        # gdb start script
-        self.gdb_script = os.path.join(
+
+    @property
+    def gdb_script(self):
+        return os.path.join(
             self.script_dir, 'gdb_' + self.id_pretty)
 
-    def _gen_gdbserver_start_script(self):
-        """Generate a shell command starting the gdbserver on the remote device via ssh
+    # 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))
 
-        GDB supports two modes:
-        multi: gdbserver remains running over several debug sessions
-        once: gdbserver terminates after the debugged process terminates
+    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'\""
         """
-        cmd_lines = ['#!/bin/sh']
-        if self.gdbserver_multi:
-            temp_dir = "TEMP_DIR=/tmp/gdbserver_%s; " % self.id_pretty
-            gdbserver_cmd_start = temp_dir
-            gdbserver_cmd_start += "test -f \\$TEMP_DIR/pid && exit 0; "
-            gdbserver_cmd_start += "mkdir -p \\$TEMP_DIR; "
-            gdbserver_cmd_start += "%s --multi :%s > \\$TEMP_DIR/log 2>&1 & " % (
-                self.gdb_cross.gdbserver_path, self.gdbserver_port)
-            gdbserver_cmd_start += "echo \\$! > \\$TEMP_DIR/pid;"
-
-            gdbserver_cmd_stop = temp_dir
-            gdbserver_cmd_stop += "test -f \\$TEMP_DIR/pid && kill \\$(cat \\$TEMP_DIR/pid); "
-            gdbserver_cmd_stop += "rm -rf \\$TEMP_DIR; "
-
-            gdbserver_cmd_l = []
-            gdbserver_cmd_l.append('if [ "$1" = "stop" ]; then')
-            gdbserver_cmd_l.append('  shift')
-            gdbserver_cmd_l.append("  %s %s %s %s 'sh -c \"%s\"'" % (
-                self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_stop))
-            gdbserver_cmd_l.append('else')
-            gdbserver_cmd_l.append("  %s %s %s %s 'sh -c \"%s\"'" % (
-                self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_start))
-            gdbserver_cmd_l.append('fi')
-            gdbserver_cmd = os.linesep.join(gdbserver_cmd_l)
-        else:
+        if gdbserver_mode == GdbServerModes.ONCE:
             gdbserver_cmd_start = "%s --once :%s %s" % (
-                self.gdb_cross.gdbserver_path, self.gdbserver_port, self.binary)
-            gdbserver_cmd = "%s %s %s %s 'sh -c \"%s\"'" % (
-                self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_start)
-        cmd_lines.append(gdbserver_cmd)
-        GdbCrossConfig.write_file(self.gdbserver_script, cmd_lines, True)
+                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 _gen_gdbinit_config(self):
+    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)
+        # This is unexpected since gdbserver should terminate after each debug session
+        # Just kill all gdbserver instances to keep it simple
+        else:
+            gdbserver_cmd_stop = "killall gdbserver"
+        return "\"/bin/sh -c '" + gdbserver_cmd_stop + "'\""
+
+    def _target_gdbserver_kill_cmd(self):
+        """Get the ssh command to kill gdbserver on the target device"""
+        return "\"kill \\$(pgrep -o -f 'gdbserver --attach :%s') 2>/dev/null || true\"" % self.gdbserver_port
+
+    def _target_ssh_gdbserver_args(self):
+        ssh_args = []
+        if self.gdb_cross.target_device.ssh_port:
+            ssh_args += ["-p", self.gdb_cross.target_device.ssh_port]
+        if self.gdb_cross.target_device.extraoptions:
+            ssh_args.extend(self.gdb_cross.target_device.extraoptions)
+        if self.gdb_cross.target_device.target:
+            ssh_args.append(self.gdb_cross.target_device.target)
+        return ssh_args
+
+    def _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_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()))
+        gdbserver_cmd = ['#!/bin/sh']
+        gdbserver_cmd.append('if [ "$1" = "stop" ]; then')
+        gdbserver_cmd.append('  shift')
+        gdbserver_cmd.append("  %s %s" % (remote_ssh, gdbserver_cmd_stop))
+        gdbserver_cmd.append('else')
+        gdbserver_cmd.append("  %s %s" % (remote_ssh, gdbserver_cmd_start))
+        gdbserver_cmd.append('fi')
+        GdbCrossConfig.write_file(self.gdbserver_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
         gdbinit_lines = ['# This file is generated by devtool ide-sdk']
-        if self.gdbserver_multi:
+        if gdbserver_mode == GdbServerModes.MULTI:
             target_help = '#   gdbserver --multi :%d' % self.gdbserver_port
             remote_cmd = 'target extended-remote'
         else:
@@ -125,15 +192,15 @@ class GdbCrossConfig:
         gdbinit_lines.append('#   cd ' + self.modified_recipe.real_srctree)
         gdbinit_lines.append(
             '#   ' + self.gdb_cross.gdb + ' -ix ' + self.gdbinit)
-
         gdbinit_lines.append('set sysroot ' + self.modified_recipe.d)
-        gdbinit_lines.append('set substitute-path "/usr/include" "' +
-                             os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"')
-        # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir.
-        gdbinit_lines.append('set debuginfod enabled off')
+
         if self.image_recipe.rootfs_dbg:
             gdbinit_lines.append(
                 'set solib-search-path "' + self.modified_recipe.solib_search_path_str(self.image_recipe) + '"')
+
+        gdbinit_lines.append('set substitute-path "/usr/include" "' +
+                             os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"')
+        if self.image_recipe.rootfs_dbg:
             # First: Search for sources of this recipe in the workspace folder
             if self.modified_recipe.pn in self.modified_recipe.target_dbgsrc_dir:
                 gdbinit_lines.append('set substitute-path "%s" "%s"' %
@@ -151,11 +218,12 @@ class GdbCrossConfig:
         else:
             logger.warning(
                 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
+        # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir.
+        gdbinit_lines.append('set debuginfod enabled off')
         gdbinit_lines.append(
             '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port))
-        gdbinit_lines.append('set remote exec-file ' + self.binary)
-        gdbinit_lines.append(
-            'run ' + os.path.join(self.modified_recipe.d, self.binary))
+        gdbinit_lines.append('set remote exec-file ' + self.binary.binary_path)
+        gdbinit_lines.append('run ' + self.binary.binary_path)
 
         GdbCrossConfig.write_file(self.gdbinit, gdbinit_lines)
 
@@ -169,9 +237,18 @@ class GdbCrossConfig:
 
     def initialize(self):
         self._gen_gdbserver_start_script()
+        if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH:
+            self._gen_gdbserver_start_script(GdbServerModes.ATTACH)
         self._gen_gdbinit_config()
         self._gen_gdb_start_script()
 
+    def gdbserver_modes(self):
+        """Get the list of gdbserver modes for which scripts are generated"""
+        modes = [self.gdbserver_default_mode]
+        if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH:
+            modes.append(GdbServerModes.ATTACH)
+        return modes
+
     @staticmethod
     def write_file(script_file, cmd_lines, executable=False):
         script_dir = os.path.dirname(script_file)
@@ -206,15 +283,14 @@ class IdeBase:
                     self.ide_name)
 
     def initialize_gdb_cross_configs(self, image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfig):
-        binaries = modified_recipe.find_installed_binaries()
-        for binary in binaries:
+        for _, exec_bin in modified_recipe.installed_binaries.items():
             gdb_cross_config = gdb_cross_config_class(
-                image_recipe, modified_recipe, binary)
+                image_recipe, modified_recipe, exec_bin)
             gdb_cross_config.initialize()
             self.gdb_cross_configs.append(gdb_cross_config)
 
     @staticmethod
-    def gen_oe_scrtips_sym_link(modified_recipe):
+    def gen_oe_scripts_sym_link(modified_recipe):
         # create a sym-link from sources to the scripts directory
         if os.path.isdir(modified_recipe.ide_sdk_scripts_dir):
             IdeBase.symlink_force(modified_recipe.ide_sdk_scripts_dir,
diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py
index ee5bb57265..67bd341347 100644
--- a/scripts/lib/devtool/ide_plugins/ide_code.py
+++ b/scripts/lib/devtool/ide_plugins/ide_code.py
@@ -9,14 +9,26 @@ import json
 import logging
 import os
 import shutil
-from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, get_devtool_deploy_opts
+from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, GdbServerModes, get_devtool_deploy_opts
 
 logger = logging.getLogger('devtool')
 
 
 class GdbCrossConfigVSCode(GdbCrossConfig):
-    def __init__(self, image_recipe, modified_recipe, binary):
-        super().__init__(image_recipe, modified_recipe, binary, False)
+    def __init__(self, image_recipe, modified_recipe, binary,
+                 gdbserver_default_mode=GdbServerModes.ONCE):
+        super().__init__(image_recipe, modified_recipe, binary,
+                         gdbserver_default_mode)
+
+    def target_ssh_gdbserver_kill_args(self):
+        """Get the ssh command arguments to kill gdbserver on the target device
+
+        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()
+        ]
 
     def initialize(self):
         self._gen_gdbserver_start_script()
@@ -207,20 +219,20 @@ class IdeVSCode(IdeBase):
         IdeBase.update_json_file(
             self.dot_code_dir(modified_recipe), prop_file, properties_dicts)
 
-    def vscode_launch_bin_dbg(self, gdb_cross_config):
+    def vscode_launch_bin_dbg(self, gdb_cross_config, gdbserver_mode):
         modified_recipe = gdb_cross_config.modified_recipe
 
         launch_config = {
-            "name": gdb_cross_config.id_pretty,
+            "name": gdb_cross_config.id_pretty_mode(gdbserver_mode),
             "type": "cppdbg",
             "request": "launch",
-            "program": os.path.join(modified_recipe.d, gdb_cross_config.binary.lstrip('/')),
+            "program": gdb_cross_config.binary.binary_host_path,
             "stopAtEntry": True,
             "cwd": "${workspaceFolder}",
             "environment": [],
             "externalConsole": False,
             "MIMode": "gdb",
-            "preLaunchTask": gdb_cross_config.id_pretty,
+            "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)
         }
@@ -260,6 +272,12 @@ class IdeVSCode(IdeBase):
 
         launch_config['sourceFileMap'] = src_file_map
         launch_config['setupCommands'] = setup_commands
+
+        # Add postDebugTask for attach mode to clean up gdbserver
+        if gdbserver_mode == GdbServerModes.ATTACH:
+            kill_task_label = "kill_gdbserver_" + gdb_cross_config.id_pretty_mode(gdbserver_mode)
+            launch_config["postDebugTask"] = kill_task_label
+
         return launch_config
 
     def vscode_launch(self, modified_recipe):
@@ -268,7 +286,8 @@ class IdeVSCode(IdeBase):
         configurations = []
         for gdb_cross_config in self.gdb_cross_configs:
             if gdb_cross_config.modified_recipe is modified_recipe:
-                configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config))
+                for gdbserver_mode in gdb_cross_config.gdbserver_modes():
+                    configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config, gdbserver_mode))
         launch_dict = {
             "version": "0.2.0",
             "configurations": configurations
@@ -294,15 +313,12 @@ class IdeVSCode(IdeBase):
         for gdb_cross_config in self.gdb_cross_configs:
             if gdb_cross_config.modified_recipe is not modified_recipe:
                 continue
-            tasks_dict['tasks'].append(
-                {
-                    "label": gdb_cross_config.id_pretty,
+            for gdbserver_mode in gdb_cross_config.gdbserver_modes():
+                new_task = {
+                    "label": gdb_cross_config.id_pretty_mode(gdbserver_mode),
                     "type": "shell",
                     "isBackground": True,
-                    "dependsOn": [
-                        install_task_name
-                    ],
-                    "command": gdb_cross_config.gdbserver_script,
+                    "command": gdb_cross_config.gdbserver_script(gdbserver_mode),
                     "problemMatcher": [
                         {
                             "pattern": [
@@ -320,7 +336,38 @@ class IdeVSCode(IdeBase):
                             }
                         }
                     ]
-                })
+                }
+                # Deploy the artifacts to the target before starting gdbserver if not already running
+                if gdbserver_mode != GdbServerModes.ATTACH:
+                    new_task['dependsOn'] = [
+                        install_task_name
+                    ]
+
+                tasks_dict['tasks'].append(new_task)
+
+                # For attach mode, add a kill task to stop a previously running gdbserver
+                # This is a known issue with gdbserver --attach that it does not terminate
+                # after detaching. With this helper task, it is possible to:
+                # 1. Start debugging in attach mode
+                # 2. Add breakpoints, step, continue, etc.
+                # 3. Press the Continue button
+                # 4. Press the Stop button which detaches gdbserver from the debugged process
+                # 5. Start debugging again in attach mode
+                # Without this kill task, step 5 would fail because gdbserver is still running
+                if gdbserver_mode == GdbServerModes.ATTACH:
+                    new_task_kill_label = "kill_gdbserver_"+ gdb_cross_config.id_pretty_mode(gdbserver_mode)
+                    new_task_kill = {
+                        "label": new_task_kill_label,
+                        "type": "shell",
+                        "command": gdb_cross_config.gdb_cross.target_device.ssh_sshexec,
+                        "args": gdb_cross_config.target_ssh_gdbserver_kill_args(),
+                        "presentation": {
+                            "close": True
+                        },
+                        "problemMatcher": []
+                    }
+                    tasks_dict['tasks'].append(new_task_kill)
+
         tasks_file = 'tasks.json'
         IdeBase.update_json_file(
             self.dot_code_dir(modified_recipe), tasks_file, tasks_dict)
@@ -410,15 +457,12 @@ class IdeVSCode(IdeBase):
             for gdb_cross_config in self.gdb_cross_configs:
                 if gdb_cross_config.modified_recipe is not modified_recipe:
                     continue
-                tasks_dict['tasks'].append(
-                    {
-                        "label": gdb_cross_config.id_pretty,
+                for gdbserver_mode in gdb_cross_config.gdbserver_modes():
+                    new_task = {
+                        "label": gdb_cross_config.id_pretty(gdbserver_mode),
                         "type": "shell",
                         "isBackground": True,
-                        "dependsOn": [
-                            dt_build_deploy_label
-                        ],
-                        "command": gdb_cross_config.gdbserver_script,
+                        "command": gdb_cross_config.gdbserver_script(gdbserver_mode),
                         "problemMatcher": [
                             {
                                 "pattern": [
@@ -436,7 +480,12 @@ class IdeVSCode(IdeBase):
                                 }
                             }
                         ]
-                    })
+                    }
+                    if gdbserver_mode != GdbServerModes.ATTACH:
+                        new_task['dependsOn'] = [
+                            dt_build_deploy_label
+                        ]
+                    tasks_dict['tasks'].append(new_task)
         tasks_file = 'tasks.json'
         IdeBase.update_json_file(
             self.dot_code_dir(modified_recipe), tasks_file, tasks_dict)
@@ -453,7 +502,7 @@ class IdeVSCode(IdeBase):
         self.vscode_c_cpp_properties(modified_recipe)
         if args.target:
             self.initialize_gdb_cross_configs(
-                image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfigVSCode)
+                image_recipe, modified_recipe, GdbCrossConfigVSCode)
             self.vscode_launch(modified_recipe)
             self.vscode_tasks(args, modified_recipe)
 
diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py
index f106c5a026..04677aba9d 100644
--- a/scripts/lib/devtool/ide_plugins/ide_none.py
+++ b/scripts/lib/devtool/ide_plugins/ide_none.py
@@ -7,11 +7,18 @@
 
 import os
 import logging
-from devtool.ide_plugins import IdeBase, GdbCrossConfig
+from devtool.ide_plugins import IdeBase, GdbCrossConfig, GdbServerModes
 
 logger = logging.getLogger('devtool')
 
 
+class GdbCrossConfigNone(GdbCrossConfig):
+    def __init__(self, image_recipe, modified_recipe, binary,
+                 gdbserver_default_mode=GdbServerModes.MULTI):
+        super().__init__(image_recipe, modified_recipe, binary,
+                         gdbserver_default_mode)
+
+
 class IdeNone(IdeBase):
     """Generate some generic helpers for other IDEs
 
@@ -44,9 +51,10 @@ class IdeNone(IdeBase):
         script_path = modified_recipe.gen_install_deploy_script(args)
         logger.info("Created: %s" % script_path)
 
-        self.initialize_gdb_cross_configs(image_recipe, modified_recipe)
+        self.initialize_gdb_cross_configs(
+            image_recipe, modified_recipe, GdbCrossConfigNone)
 
-        IdeBase.gen_oe_scrtips_sym_link(modified_recipe)
+        IdeBase.gen_oe_scripts_sym_link(modified_recipe)
 
 
 def register_ide_plugin(ide_plugins):
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 9df88454c7..2af4dca256 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -46,17 +46,17 @@ class TargetDevice:
     """SSH remote login parameters"""
 
     def __init__(self, args):
-        self.extraoptions = ''
+        self.extraoptions = []
         if args.no_host_check:
-            self.extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+            self.extraoptions += ['-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no']
         self.ssh_sshexec = 'ssh'
         if args.ssh_exec:
             self.ssh_sshexec = args.ssh_exec
         self.ssh_port = ''
         if args.port:
-            self.ssh_port = "-p %s" % args.port
+            self.ssh_port = ['-p', args.port]
         if args.key:
-            self.extraoptions += ' -i %s' % args.key
+            self.extraoptions += ['-i', args.key]
 
         self.target = args.target
         target_sp = args.target.split('@')
@@ -265,6 +265,112 @@ class RecipeNotModified:
         self.name = name
         self.bootstrap_tasks = [name + ':do_populate_sysroot']
 
+class ExecutableBinary:
+    """Represent an installed executable binary of a modified recipe"""
+
+    def __init__(self, image_dir_d, binary_path,
+                 systemd_services, init_scripts):
+        self.image_dir_d = image_dir_d
+        self.binary_path = binary_path
+        self.init_script = None
+        self.systemd_service = None
+
+        self._init_service_for_binary(systemd_services)
+        self._init_init_script_for_binary(init_scripts)
+
+    def _init_service_for_binary(self, systemd_services):
+        """Find systemd service file that handles this binary"""
+        service_dirs = [
+            'etc/systemd/system',
+            'lib/systemd/system',
+            'usr/lib/systemd/system'
+        ]
+        for _, services in systemd_services.items():
+            for service in services:
+                for service_dir in service_dirs:
+                    service_path = os.path.join(self.image_dir_d, service_dir, service)
+                    if os.path.exists(service_path):
+                        try:
+                            with open(service_path, 'r') as f:
+                                for line in f:
+                                    if line.strip().startswith('ExecStart='):
+                                        exec_start = line.strip()[10:].strip()  # Remove 'ExecStart='
+                                        # Remove any leading modifiers like '-' or '@'
+                                        exec_start = exec_start.lstrip('-@')
+                                        # Get the first word (the executable path)
+                                        exec_binary = exec_start.split()[0] if exec_start.split() else ''
+                                        if exec_binary == self.binary_path or exec_binary.endswith('/' + self.binary_path.lstrip('/')):
+                                            logger.debug("Found systemd service for binary %s: %s" % (self.binary_path, service))
+                                            self.systemd_service = service
+                        except (IOError, OSError):
+                            continue
+
+    def _init_init_script_for_binary(self, init_scripts):
+        """Find SysV init script that handles this binary"""
+        init_dirs = [
+            'etc/init.d',
+            'etc/rc.d/init.d'
+        ]
+        for _, init_scripts in init_scripts.items():
+            for init_script in init_scripts:
+                for init_dir in init_dirs:
+                    init_path = os.path.join(self.image_dir_d, init_dir, init_script)
+                    if os.path.exists(init_path):
+                        init_script_path = os.path.join("/", init_dir, init_script)
+                        binary_name = os.path.basename(self.binary_path)
+                        # if the init script file name is equal to the binary file name, return it directly
+                        if os.path.basename(init_script) == binary_name:
+                            logger.debug("Found SysV init script for binary %s: %s" % (self.binary_path, init_script_path))
+                            self.init_script = init_script_path
+                        # Otherwise check if the script containes a reference to the binary
+                        try:
+                            with open(init_path, 'r') as f:
+                                content = f.read()
+                                pattern = r'\b' + re.escape(binary_name) + r'\b'
+                                if re.search(pattern, content):
+                                    logger.debug("Found SysV init script for binary %s: %s" % (self.binary_path, init_script_path))
+                                    self.init_script = init_script_path
+                                    return
+                        except (IOError, OSError):
+                            continue
+
+    @property
+    def binary_host_path(self):
+        """Get the absolute path of this binary on the host"""
+        return os.path.join(self.image_dir_d, self.binary_path.lstrip('/'))
+
+    @property
+    def runs_as_service(self):
+        """Check if this binary is run by a service or init script"""
+        return self.systemd_service is not None or self.init_script is not None
+
+    @property
+    def start_command(self):
+        """Get the command to start this binary"""
+        if self.systemd_service:
+            return "systemctl start %s" % self.systemd_service
+        if self.init_script:
+            return "%s start" % self.init_script
+        return None
+
+    @property
+    def stop_command(self):
+        """Get the command to stop this binary"""
+        if self.systemd_service:
+            return "systemctl stop %s" % self.systemd_service
+        if self.init_script:
+            return "%s stop" % self.init_script
+        return None
+
+    @property
+    def pid_command(self):
+        """Get the command to get the PID of this binary"""
+        if self.systemd_service:
+            return "systemctl show --property MainPID --value %s" % self.systemd_service
+        if self.init_script:
+            return "pidof %s" % os.path.basename(self.binary_path)
+        return None
+
 
 class RecipeModified:
     """Handling of recipes in the workspace created by devtool modify"""
@@ -308,6 +414,9 @@ class RecipeModified:
         self.target_dbgsrc_dir = None
         self.topdir = None
         self.workdir = None
+        # Service management
+        self.systemd_services = {}
+        self.init_scripts = {}
         # replicate bitbake build environment
         self.exported_vars = None
         self.cmd_compile = None
@@ -324,6 +433,9 @@ class RecipeModified:
         self.extra_oemeson = None
         self.meson_cross_file = None
 
+        # Populated after bitbake built all the recipes
+        self._installed_binaries = None
+
     def initialize(self, config, workspace, tinfoil):
         recipe_d = parse_recipe(
             config, tinfoil, self.name, appends=True, filter_workspace=False)
@@ -378,6 +490,8 @@ class RecipeModified:
         self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR'))
 
         self.__init_exported_variables(recipe_d)
+        self.__init_systemd_services(recipe_d)
+        self.__init_init_scripts(recipe_d)
 
         if bb.data.inherits_class('cmake', recipe_d):
             self.oecmake_generator = recipe_d.getVar('OECMAKE_GENERATOR')
@@ -498,6 +612,41 @@ class RecipeModified:
 
         self.exported_vars = exported_vars
 
+    def __init_systemd_services(self, d):
+        """Find all systemd service files for the recipe."""
+        services = {}
+        if bb.data.inherits_class('systemd', d):
+            systemd_packages = d.getVar('SYSTEMD_PACKAGES')
+            if systemd_packages:
+                for package in systemd_packages.split():
+                    services[package] = d.getVar('SYSTEMD_SERVICE:' + package).split()
+        self.systemd_services = services
+
+    def __init_init_scripts(self, d):
+        """Find all SysV init scripts for the recipe."""
+        init_scripts = {}
+        if bb.data.inherits_class('update-rc.d', d):
+            script_packages = d.getVar('INITSCRIPT_PACKAGES')
+            if script_packages:
+                for package in script_packages.split():
+                    initscript_name = d.getVar('INITSCRIPT_NAME:' + package)
+                    if initscript_name:
+                        # Handle both single script and multiple scripts
+                        scripts = initscript_name.split()
+                        if scripts:
+                            init_scripts[package] = scripts
+            else:
+                # If INITSCRIPT_PACKAGES is not set, check for default INITSCRIPT_NAME
+                initscript_name = d.getVar('INITSCRIPT_NAME')
+                if initscript_name:
+                    scripts = initscript_name.split()
+                    if scripts:
+                        # Use PN as the default package name when INITSCRIPT_PACKAGES is not set
+                        pn = d.getVar('PN')
+                        if pn:
+                            init_scripts[pn] = scripts
+        self.init_scripts = init_scripts
+
     def __init_cmake_preset_cache(self, d):
         """Get the arguments passed to cmake
 
@@ -662,9 +811,12 @@ class RecipeModified:
             return True
         return False
 
-    def find_installed_binaries(self):
+    @property
+    def installed_binaries(self):
         """find all executable elf files in the image directory"""
-        binaries = []
+        if self._installed_binaries:
+            return self._installed_binaries
+        binaries = {}
         d_len = len(self.d)
         re_so = re.compile(r'.*\.so[.0-9]*$')
         for root, _, files in os.walk(self.d, followlinks=False):
@@ -675,8 +827,12 @@ class RecipeModified:
                     continue
                 abs_name = os.path.join(root, file)
                 if os.access(abs_name, os.X_OK) and RecipeModified.is_elf_file(abs_name):
-                    binaries.append(abs_name[d_len:])
-        return sorted(binaries)
+                    binary_path = abs_name[d_len:]
+                    binary = ExecutableBinary(self.d, binary_path,
+                                              self.systemd_services, self.init_scripts)
+                    binaries[binary_path] = binary
+        self._installed_binaries = dict(sorted(binaries.items()))
+        return self._installed_binaries
 
     def gen_deploy_target_script(self, args):
         """Generate a script which does what devtool deploy-target does
-- 
2.52.0



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

* [PATCH v2 06/14] devtool: ide-sdk: move code to ide_none
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (4 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 05/14] devtool: ide-sdk: add gdbserver attach mode support AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 07/14] devtool: ide-sdk: make install_and_deploy script pass target arg AdrianF
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Move code which is used by the ide_none plugin from the base class
to the ide_none plugin. This is just a refactoring, no functional
change.

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

diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py
index 70f47d6e68..80dfc1e235 100644
--- a/scripts/lib/devtool/ide_plugins/__init__.py
+++ b/scripts/lib/devtool/ide_plugins/__init__.py
@@ -9,10 +9,8 @@ import errno
 import json
 import logging
 import os
-import stat
 from enum import Enum, auto
 from devtool import DevtoolError
-from bb.utils import mkdirhier
 
 logger = logging.getLogger('devtool')
 
@@ -130,19 +128,6 @@ class GdbCrossConfig:
             raise DevtoolError("Unsupported gdbserver mode: %s" % gdbserver_mode)
         return "\"/bin/sh -c '" + gdbserver_cmd_start + "'\""
 
-    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)
-        # This is unexpected since gdbserver should terminate after each debug session
-        # Just kill all gdbserver instances to keep it simple
-        else:
-            gdbserver_cmd_stop = "killall gdbserver"
-        return "\"/bin/sh -c '" + gdbserver_cmd_stop + "'\""
-
     def _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
@@ -157,91 +142,6 @@ class GdbCrossConfig:
             ssh_args.append(self.gdb_cross.target_device.target)
         return ssh_args
 
-    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_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()))
-        gdbserver_cmd = ['#!/bin/sh']
-        gdbserver_cmd.append('if [ "$1" = "stop" ]; then')
-        gdbserver_cmd.append('  shift')
-        gdbserver_cmd.append("  %s %s" % (remote_ssh, gdbserver_cmd_stop))
-        gdbserver_cmd.append('else')
-        gdbserver_cmd.append("  %s %s" % (remote_ssh, gdbserver_cmd_start))
-        gdbserver_cmd.append('fi')
-        GdbCrossConfig.write_file(self.gdbserver_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
-        gdbinit_lines = ['# This file is generated by devtool ide-sdk']
-        if gdbserver_mode == GdbServerModes.MULTI:
-            target_help = '#   gdbserver --multi :%d' % self.gdbserver_port
-            remote_cmd = 'target extended-remote'
-        else:
-            target_help = '#   gdbserver :%d %s' % (
-                self.gdbserver_port, self.binary)
-            remote_cmd = 'target remote'
-        gdbinit_lines.append('# On the remote target:')
-        gdbinit_lines.append(target_help)
-        gdbinit_lines.append('# On the build machine:')
-        gdbinit_lines.append('#   cd ' + self.modified_recipe.real_srctree)
-        gdbinit_lines.append(
-            '#   ' + self.gdb_cross.gdb + ' -ix ' + self.gdbinit)
-        gdbinit_lines.append('set sysroot ' + self.modified_recipe.d)
-
-        if self.image_recipe.rootfs_dbg:
-            gdbinit_lines.append(
-                'set solib-search-path "' + self.modified_recipe.solib_search_path_str(self.image_recipe) + '"')
-
-        gdbinit_lines.append('set substitute-path "/usr/include" "' +
-                             os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"')
-        if self.image_recipe.rootfs_dbg:
-            # First: Search for sources of this recipe in the workspace folder
-            if self.modified_recipe.pn in self.modified_recipe.target_dbgsrc_dir:
-                gdbinit_lines.append('set substitute-path "%s" "%s"' %
-                                     (self.modified_recipe.target_dbgsrc_dir, self.modified_recipe.real_srctree))
-            else:
-                logger.error(
-                    "TARGET_DBGSRC_DIR must contain the recipe name PN.")
-            # Second: Search for sources of other recipes in the rootfs-dbg
-            if self.modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"):
-                gdbinit_lines.append('set substitute-path "/usr/src/debug" "%s"' % os.path.join(
-                    self.image_recipe.rootfs_dbg, "usr", "src", "debug"))
-            else:
-                logger.error(
-                    "TARGET_DBGSRC_DIR must start with /usr/src/debug.")
-        else:
-            logger.warning(
-                "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
-        # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir.
-        gdbinit_lines.append('set debuginfod enabled off')
-        gdbinit_lines.append(
-            '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port))
-        gdbinit_lines.append('set remote exec-file ' + self.binary.binary_path)
-        gdbinit_lines.append('run ' + self.binary.binary_path)
-
-        GdbCrossConfig.write_file(self.gdbinit, gdbinit_lines)
-
-    def _gen_gdb_start_script(self):
-        """Generate a script starting GDB with the corresponding gdbinit configuration."""
-        cmd_lines = ['#!/bin/sh']
-        cmd_lines.append('cd ' + self.modified_recipe.real_srctree)
-        cmd_lines.append(self.gdb_cross.gdb + ' -ix ' +
-                         self.gdbinit + ' "$@"')
-        GdbCrossConfig.write_file(self.gdb_script, cmd_lines, True)
-
-    def initialize(self):
-        self._gen_gdbserver_start_script()
-        if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH:
-            self._gen_gdbserver_start_script(GdbServerModes.ATTACH)
-        self._gen_gdbinit_config()
-        self._gen_gdb_start_script()
-
     def gdbserver_modes(self):
         """Get the list of gdbserver modes for which scripts are generated"""
         modes = [self.gdbserver_default_mode]
@@ -249,17 +149,10 @@ class GdbCrossConfig:
             modes.append(GdbServerModes.ATTACH)
         return modes
 
-    @staticmethod
-    def write_file(script_file, cmd_lines, executable=False):
-        script_dir = os.path.dirname(script_file)
-        mkdirhier(script_dir)
-        with open(script_file, 'w') as script_f:
-            script_f.write(os.linesep.join(cmd_lines))
-            script_f.write(os.linesep)
-        if executable:
-            st = os.stat(script_file)
-            os.chmod(script_file, st.st_mode | stat.S_IEXEC)
-        logger.info("Created: %s" % script_file)
+    def initialize(self):
+        """Interface function to initialize the gdb config generation"""
+        pass
+
 
 
 class IdeBase:
diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py
index 04677aba9d..8284c4e0a5 100644
--- a/scripts/lib/devtool/ide_plugins/ide_none.py
+++ b/scripts/lib/devtool/ide_plugins/ide_none.py
@@ -7,6 +7,8 @@
 
 import os
 import logging
+import stat
+from bb.utils import mkdirhier
 from devtool.ide_plugins import IdeBase, GdbCrossConfig, GdbServerModes
 
 logger = logging.getLogger('devtool')
@@ -18,6 +20,116 @@ class GdbCrossConfigNone(GdbCrossConfig):
         super().__init__(image_recipe, modified_recipe, binary,
                          gdbserver_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)
+        # This is unexpected since gdbserver should terminate after each debug session
+        # Just kill all gdbserver instances to keep it simple
+        else:
+            gdbserver_cmd_stop = "killall gdbserver"
+        return "\"/bin/sh -c '" + gdbserver_cmd_stop + "'\""
+
+    def _gen_gdbserver_start_script(self, gdbserver_mode=None):
+        """Generate a shell script starting the gdbserver on the remote device via ssh"""
+        if gdbserver_mode is None:
+            gdbserver_mode = self.gdbserver_default_mode
+        gdbserver_cmd_start = self._target_gdbserver_start_cmd(gdbserver_mode)
+        gdbserver_cmd_stop = self._target_gdbserver_stop_cmd(gdbserver_mode)
+        remote_ssh = "%s %s" % (self.gdb_cross.target_device.ssh_sshexec,
+                                " ".join(self._target_ssh_gdbserver_args()))
+        gdbserver_cmd = ['#!/bin/sh']
+        gdbserver_cmd.append('if [ "$1" = "stop" ]; then')
+        gdbserver_cmd.append('  shift')
+        gdbserver_cmd.append("  %s %s" % (remote_ssh, gdbserver_cmd_stop))
+        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)
+
+    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
+        gdbinit_lines = ['# This file is generated by devtool ide-sdk']
+        if gdbserver_mode == GdbServerModes.MULTI:
+            target_help = '#   gdbserver --multi :%d' % self.gdbserver_port
+            remote_cmd = 'target extended-remote'
+        else:
+            target_help = '#   gdbserver :%d %s' % (
+                self.gdbserver_port, self.binary)
+            remote_cmd = 'target remote'
+        gdbinit_lines.append('# On the remote target:')
+        gdbinit_lines.append(target_help)
+        gdbinit_lines.append('# On the build machine:')
+        gdbinit_lines.append('#   cd ' + self.modified_recipe.real_srctree)
+        gdbinit_lines.append(
+            '#   ' + self.gdb_cross.gdb + ' -ix ' + self.gdbinit)
+        gdbinit_lines.append('set sysroot ' + self.modified_recipe.d)
+
+        if self.image_recipe.rootfs_dbg:
+            gdbinit_lines.append(
+                'set solib-search-path "' + self.modified_recipe.solib_search_path_str(self.image_recipe) + '"')
+
+        gdbinit_lines.append('set substitute-path "/usr/include" "' +
+                             os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"')
+        if self.image_recipe.rootfs_dbg:
+            # First: Search for sources of this recipe in the workspace folder
+            if self.modified_recipe.pn in self.modified_recipe.target_dbgsrc_dir:
+                gdbinit_lines.append('set substitute-path "%s" "%s"' %
+                                     (self.modified_recipe.target_dbgsrc_dir, self.modified_recipe.real_srctree))
+            else:
+                logger.error(
+                    "TARGET_DBGSRC_DIR must contain the recipe name PN.")
+            # Second: Search for sources of other recipes in the rootfs-dbg
+            if self.modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"):
+                gdbinit_lines.append('set substitute-path "/usr/src/debug" "%s"' % os.path.join(
+                    self.image_recipe.rootfs_dbg, "usr", "src", "debug"))
+            else:
+                logger.error(
+                    "TARGET_DBGSRC_DIR must start with /usr/src/debug.")
+        else:
+            logger.warning(
+                "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
+        # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir.
+        gdbinit_lines.append('set debuginfod enabled off')
+        gdbinit_lines.append(
+            '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port))
+        gdbinit_lines.append('set remote exec-file ' + self.binary.binary_path)
+        gdbinit_lines.append('run ' + self.binary.binary_path)
+
+        GdbCrossConfigNone.write_file(self.gdbinit, gdbinit_lines)
+
+    def _gen_gdb_start_script(self):
+        """Generate a script starting GDB with the corresponding gdbinit configuration."""
+        cmd_lines = ['#!/bin/sh']
+        cmd_lines.append('cd ' + self.modified_recipe.real_srctree)
+        cmd_lines.append(self.gdb_cross.gdb + ' -ix ' +
+                         self.gdbinit + ' "$@"')
+        GdbCrossConfigNone.write_file(self.gdb_script, cmd_lines, True)
+
+    def initialize(self):
+        self._gen_gdbserver_start_script()
+        if self.binary.runs_as_service and self.gdbserver_default_mode != GdbServerModes.ATTACH:
+            self._gen_gdbserver_start_script(GdbServerModes.ATTACH)
+        self._gen_gdbinit_config()
+        self._gen_gdb_start_script()
+
+    @staticmethod
+    def write_file(script_file, cmd_lines, executable=False):
+        script_dir = os.path.dirname(script_file)
+        mkdirhier(script_dir)
+        with open(script_file, 'w') as script_f:
+            script_f.write(os.linesep.join(cmd_lines))
+            script_f.write(os.linesep)
+        if executable:
+            st = os.stat(script_file)
+            os.chmod(script_file, st.st_mode | stat.S_IEXEC)
+        logger.info("Created: %s" % script_file)
+
 
 class IdeNone(IdeBase):
     """Generate some generic helpers for other IDEs
-- 
2.52.0



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

* [PATCH v2 07/14] devtool: ide-sdk: make install_and_deploy script pass target arg
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (5 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 06/14] devtool: ide-sdk: move code to ide_none AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 08/14] devtool: ide-sdk: vscode replace scripts AdrianF
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Previously, the target was hardcoded in the install_and_deploy script,
limiting flexibility. This change allows passing the target as a
command-line argument, enabling IDEs to configure the target dynamically
rather than only at IDE configuration generation time.

This is a first step towards making the target configurable from the IDE.

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

diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py
index 67bd341347..3f8c1a44a3 100644
--- a/scripts/lib/devtool/ide_plugins/ide_code.py
+++ b/scripts/lib/devtool/ide_plugins/ide_code.py
@@ -306,6 +306,10 @@ class IdeVSCode(IdeBase):
                     "label": install_task_name,
                     "type": "shell",
                     "command": run_install_deploy,
+                    "args": [
+                        "--target",
+                        args.target
+                    ],
                     "problemMatcher": []
                 }
             ]
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 2af4dca256..96d60ad4f1 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -859,6 +859,9 @@ class RecipeModified:
         cmd_lines.append('        for key in my_dict:')
         cmd_lines.append('            setattr(self, key, my_dict[key])')
         cmd_lines.append('filtered_args = Dict2Class(filtered_args_dict)')
+        cmd_lines.append('if len(sys.argv) > 2:')
+        cmd_lines.append('    if sys.argv[1] == "-t" or sys.argv[1] == "--target":')
+        cmd_lines.append('        setattr(filtered_args, "target", sys.argv[2])')
         cmd_lines.append(
             'setattr(filtered_args, "recipename", "%s")' % self.bpn)
         cmd_lines.append('deploy_no_d("%s", "%s", "%s", "%s", "%s", "%s", %d, "%s", "%s", filtered_args)' %
@@ -884,7 +887,7 @@ class RecipeModified:
             'bitbake %s -c install --force || { echo "bitbake %s -c install --force failed"; exit 1; }' % (self.bpn, self.bpn))
 
         # Self contained devtool deploy-target
-        cmd_lines.append(self.gen_deploy_target_script(args))
+        cmd_lines.append(self.gen_deploy_target_script(args) + ' "$@"')
 
         return self.write_script(cmd_lines, 'install_and_deploy')
 
-- 
2.52.0



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

* [PATCH v2 08/14] devtool: ide-sdk: vscode replace scripts
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (6 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 07/14] devtool: ide-sdk: make install_and_deploy script pass target arg AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 09/14] oe-selftest: devtool ide-sdk cover vscode remote debugging AdrianF
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Write the ssh command to start gdbserver on target directly into the
tasks.json. This avoids the need to create one more script file on the
host. It also simplifies manual modifications of VSCode's standard
tasks.json which is much more handy than modifying multiple proprietary
scripts used to launch gdbserver.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_plugins/ide_code.py | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py
index 3f8c1a44a3..05d513f160 100644
--- a/scripts/lib/devtool/ide_plugins/ide_code.py
+++ b/scripts/lib/devtool/ide_plugins/ide_code.py
@@ -20,6 +20,18 @@ class GdbCrossConfigVSCode(GdbCrossConfig):
         super().__init__(image_recipe, modified_recipe, binary,
                          gdbserver_default_mode)
 
+    def target_ssh_gdbserver_start_args(self, gdbserver_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)
+        ]
+
     def target_ssh_gdbserver_kill_args(self):
         """Get the ssh command arguments to kill gdbserver on the target device
 
@@ -31,7 +43,7 @@ class GdbCrossConfigVSCode(GdbCrossConfig):
         ]
 
     def initialize(self):
-        self._gen_gdbserver_start_script()
+        pass
 
 
 class IdeVSCode(IdeBase):
@@ -322,7 +334,8 @@ class IdeVSCode(IdeBase):
                     "label": gdb_cross_config.id_pretty_mode(gdbserver_mode),
                     "type": "shell",
                     "isBackground": True,
-                    "command": gdb_cross_config.gdbserver_script(gdbserver_mode),
+                    "command": gdb_cross_config.gdb_cross.target_device.ssh_sshexec,
+                    "args": gdb_cross_config.target_ssh_gdbserver_start_args(gdbserver_mode),
                     "problemMatcher": [
                         {
                             "pattern": [
@@ -466,7 +479,8 @@ class IdeVSCode(IdeBase):
                         "label": gdb_cross_config.id_pretty(gdbserver_mode),
                         "type": "shell",
                         "isBackground": True,
-                        "command": gdb_cross_config.gdbserver_script(gdbserver_mode),
+                        "command": gdb_cross_config.gdb_cross.target_device.ssh_sshexec,
+                        "args": gdb_cross_config.target_ssh_gdbserver_start_args(gdbserver_mode),
                         "problemMatcher": [
                             {
                                 "pattern": [
-- 
2.52.0



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

* [PATCH v2 09/14] oe-selftest: devtool ide-sdk cover vscode remote debugging
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (7 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 08/14] devtool: ide-sdk: vscode replace scripts AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 10/14] devtool: ide-sdk: evaluate DEBUG_PREFIX_MAP AdrianF
                   ` (5 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

This adds more test coverage for devtool ide-sdk, with VSCode. The cmake
test case has now a full remote debugging test on Qemu. The test checks
the generated launch.json and tasks.json files, starts gdbserver and
connects to it. The test verifies breakpoints, variables and source file
listing.

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

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index b40323c109..df5c863a85 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -9,20 +9,23 @@ import os
 import re
 import shutil
 import tempfile
+import time
 import glob
 import fnmatch
 import unittest
 import json
 import logging
+import shlex
 
 from oeqa.selftest.case import OESelftestTestCase
-from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer
+from oeqa.utils.commands import runCmd, Command, bitbake, get_bb_var, create_temp_layer
 from oeqa.utils.commands import get_bb_vars, runqemu, runqemu_check_taps, get_test_layer
 from oeqa.core.decorator import OETestTag
 from bb.utils import mkdirhier, edit_bblayers_conf
 
 oldmetapath = None
 
+
 def setUpModule():
     global templayerdir
     templayerdir = tempfile.mkdtemp(prefix='devtoolqa')
@@ -2524,8 +2527,23 @@ class DevtoolUpgradeTests(DevtoolBase):
         runCmd("grep %s %s" % (modconfopt, codeconfigfile))
 
 
+
+class RunCmdBackground:
+    """Context manager to manage a background subprocess"""
+    def __init__(self, command, output_log=None, **options):
+        self.cmd = Command(command, bg=True, output_log=output_log, **options)
+
+    def __enter__(self):
+        self.cmd.run()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.cmd.stop()
+
+
 class DevtoolIdeSdkTests(DevtoolBase):
 
+    MAGIC_STRING_ORIG = "Magic: 123456789"
+
     def setUp(self):
         super().setUp()
         self._cmd_logger = None
@@ -2639,7 +2657,6 @@ class DevtoolIdeSdkTests(DevtoolBase):
                           '%s script not found' % install_deploy_cmd)
         runCmd(install_deploy_cmd, output_log=self._cmd_logger)
 
-        MAGIC_STRING_ORIG = "Magic: 123456789"
         MAGIC_STRING_NEW = "Magic: 987654321"
         ptest_cmd = "ptest-runner " + recipe_name
 
@@ -2652,7 +2669,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         status, output = qemu.run(example_exe)
         self.assertEqual(status, 0, msg="%s failed: %s" %
                          (example_exe, output))
-        self.assertIn(MAGIC_STRING_ORIG, output)
+        self.assertIn(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, output)
 
         # Verify the unmodified ptests work
         status, output = qemu.run(ptest_cmd)
@@ -2661,13 +2678,13 @@ class DevtoolIdeSdkTests(DevtoolBase):
 
         # Verify remote debugging works
         self._gdb_cross_debugging_multi(
-            qemu, recipe_name, example_exe, MAGIC_STRING_ORIG)
+            qemu, recipe_name, example_exe, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
 
         # Replace the Magic String in the code, compile and deploy to Qemu
         cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp')
         with open(cpp_example_lib_hpp, 'r') as file:
             cpp_code = file.read()
-            cpp_code = cpp_code.replace(MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
+            cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
         with open(cpp_example_lib_hpp, 'w') as file:
             file.write(cpp_code)
         runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger)
@@ -2676,7 +2693,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         status, output = qemu.run(example_exe)
         self.assertEqual(status, 0, msg="%s failed: %s" %
                          (example_exe, output))
-        self.assertNotIn(MAGIC_STRING_ORIG, output)
+        self.assertNotIn(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, output)
         self.assertIn(MAGIC_STRING_NEW, output)
 
         # Verify the modified example ptests work
@@ -2701,6 +2718,24 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertEqual(r.status, 0)
         self.assertIn("GNU gdb", r.output)
 
+    def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run"):
+        """Get a series of gdb commands to debug the cpp-example-lib example"""
+        gdb_batch_cmd = " -ex 'break main' -ex '%s'" % gdb_start_cmd
+        gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
+        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
+        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
+        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
+        gdb_batch_cmd += " -ex 'continue'"
+        return gdb_batch_cmd
+
+    def _gdb_debug_cpp_example_check(self, gdb_output, magic_string):
+        self.assertIn("Breakpoint 1, main", gdb_output)
+        self.assertIn("$1 = 0", gdb_output)  # test.string.compare equal
+        self.assertIn("$2 = -3", gdb_output)  # test.string.compare longer
+        self.assertIn(
+            'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, gdb_output)
+        self.assertIn("exited normally", gdb_output)
+
     def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string):
         """Verify gdb-cross is working
 
@@ -2743,24 +2778,12 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("1234", r.output)
 
         # Test remote debugging works
-        gdb_batch_cmd = " --batch -ex 'break main' -ex 'run'"
-        gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
-        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
-        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
-        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
-        gdb_batch_cmd += " -ex 'continue'"
-        return gdb_batch_cmd
-
+        gdb_batch_cmd = " --batch " + self._gdb_debug_cpp_example(magic_string)
         r = runCmd(gdb_script + gdb_batch_cmd, output_log=self._cmd_logger)
         self.logger.debug("%s %s returned: %s", gdb_script,
                           gdb_batch_cmd, r.output)
         self.assertEqual(r.status, 0)
-        self.assertIn("Breakpoint 1, main", r.output)
-        self.assertIn("$1 = 0", r.output)  # test.string.compare equal
-        self.assertIn("$2 = -3", r.output)  # test.string.compare longer
-        self.assertIn(
-            'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, r.output)
-        self.assertIn("exited normally", r.output)
+        self._gdb_debug_cpp_example_check(r.output, magic_string=magic_string)
 
         # Stop the gdbserver
         r = runCmd(gdbserver_script + ' stop', output_log=self._cmd_logger)
@@ -2861,6 +2884,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
                 recipe_name, testimage, qemu.ip)
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
             self._gdb_cross()
             self._verify_cmake_preset(tempdir)
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
@@ -2892,6 +2916,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
                 recipe_name, testimage, qemu.ip)
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
             self._gdb_cross()
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
 
@@ -2903,22 +2928,266 @@ class DevtoolIdeSdkTests(DevtoolBase):
             # 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')
+        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.assertEqual(len(configurations), 3, "Should have exactly three debug configurations")
+
+        # Track configurations found
+        once_configs = []
+        attach_configs = []
+
+        for config in configurations:
+            # Verify required fields exist
+            required_fields = ["name", "type", "request", "program", "cwd", "MIMode",
+                             "miDebuggerPath", "miDebuggerServerAddress"]
+            for field in required_fields:
+                self.assertIn(field, config, f"Configuration '{config.get('name', 'Unknown')}' missing required field: {field}")
+
+            # Verify common configuration values
+            self.assertEqual(config["type"], "cppdbg", f"Configuration '{config['name']}' should use cppdbg type")
+            self.assertEqual(config["request"], "launch", f"Configuration '{config['name']}' should be launch type")
+            self.assertEqual(config["cwd"], "${workspaceFolder}", f"Configuration '{config['name']}' should use workspaceFolder as cwd")
+            self.assertEqual(config["MIMode"], "gdb", f"Configuration '{config['name']}' should use gdb MIMode")
+            self.assertEqual(config.get("externalConsole", False), False, f"Configuration '{config['name']}' should not use external console")
+            self.assertEqual(config.get("stopAtEntry", True), True, f"Configuration '{config['name']}' should stop at entry")
+
+            # Verify program path is absolute and exists conceptually
+            program = config["program"]
+            self.assertTrue(program.startswith("/"), f"Configuration '{config['name']}' program path should be absolute: {program}")
+            self.assertIn("/image/usr/bin/", program, f"Configuration '{config['name']}' program should be in image/usr/bin")
+
+            # Verify debugger path
+            debugger_path = config["miDebuggerPath"]
+            self.assertTrue(debugger_path.endswith("-gdb"), f"Configuration '{config['name']}' debugger should end with -gdb: {debugger_path}")
+            self.assertIn("/recipe-sysroot-native/usr/bin/", debugger_path, f"Configuration '{config['name']}' debugger should be in sysroot-native")
+
+            # Verify server address format
+            server_addr = config["miDebuggerServerAddress"]
+            self.assertRegex(server_addr, r"^\d+\.\d+\.\d+\.\d+:\d+$", f"Configuration '{config['name']}' server address should be IP:PORT format: {server_addr}")
+
+            # Verify additional SO lib search path exists and contains debug paths
+            so_paths = config.get("additionalSOLibSearchPath", [])
+            self.assertIn("/.debug", so_paths, f"Configuration '{config['name']}' should include debug symbol paths")
+            self.assertIn("/rootfs-dbg/", so_paths, f"Configuration '{config['name']}' should include rootfs-dbg paths")
+
+            # Verify source file mappings
+            source_map = config.get("sourceFileMap", {})
+            self.assertIsInstance(source_map, dict, f"Configuration '{config['name']}' sourceFileMap should be a dictionary")
+            self.assertIn("/usr/src/debug", source_map, f"Configuration '{config['name']}' should map /usr/src/debug")
+            self.assertIn("${workspaceFolder}", str(source_map), f"Configuration '{config['name']}' should map to workspaceFolder")
+
+            # Verify setup commands for sysroot
+            setup_commands = config.get("setupCommands", [])
+            self.assertTrue(len(setup_commands) >= 1, f"Configuration '{config['name']}' should have setup commands")
+            sysroot_cmd = setup_commands[0]
+            self.assertIn("sysroot", sysroot_cmd.get("text", ""), f"Configuration '{config['name']}' should set sysroot in setup commands")
+
+            # Verify preLaunchTask exists and matches name pattern
+            task = config.get("preLaunchTask", "")
+            self.assertTrue(task, f"Configuration '{config['name']}' preLaunchTask should not be empty")
+            # Task should contain port number and executable name from config name
+            config_name = config["name"]
+            if "_once" in config_name:
+                self.assertIn("_once", task, f"once configuration '{config_name}' should have once preLaunchTask")
+            elif "_attach" in config_name:
+                self.assertIn("_attach", task, f"attach configuration '{config_name}' should have attach preLaunchTask")
+                # Verify postDebugTask exists for attach configurations (kill gdbserver)
+                self.assertIn("postDebugTask", config, f"attach configuration '{config_name}' should have postDebugTask")
+
+            # Categorize configurations
+            config_name = config["name"]
+            if "_once" in config_name:
+                once_configs.append(config_name)
+            elif "_attach" in config_name:
+                attach_configs.append(config_name)
+
+        # Verify we have expected configuration types
+        self.assertEqual(len(once_configs), 2, f"Should have two once configuration, found: {once_configs}")
+        self.assertEqual(len(attach_configs), 1, f"Should have one attach configuration, found: {attach_configs}")
+
+    def _verify_launch_json_debugging(self, tempdir, qemu, recipe_name, example_exe):
+        """Verify remote debugging and deployment works using launch.json configurations
+
+        This method tests the VSCode debug configurations by:
+        1. Starting gdbserver on target using tasks.json commands
+        2. Running gdb debugging session with batch commands
+            $BUILDDIR/tmp/work/x86_64-linux/gdb-cross-x86_64/16.3/recipe-sysroot-native/usr/bin/x86_64-poky-linux/x86_64-poky-linux-gdb --batch  \
+            -ex 'set sysroot $BUILDDIR/tmp/work/x86-64-v3-poky-linux/cmake-example/1.0/image'  \
+            -ex 'set substitute-path /usr/include $BUILDDIR/tmp/work/x86-64-v3-poky-linux/cmake-example/1.0/recipe-sysroot/usr/include'  \
+            -ex 'set substitute-path /usr/src/debug/cmake-example/1.0 $BUILDDIR/workspace/sources/cmake-example'  \
+            -ex 'set substitute-path /usr/src/debug $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/src/debug'  \
+            -ex 'set solib-search-path $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/lib/.debug:\
+                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib/.debug:\
+                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib/debug:\
+                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/lib:\
+                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib:\
+                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs/lib:\
+                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs/usr/lib'  \
+            -ex 'file $BUILDDIR/tmp/work/x86-64-v3-poky-linux/cmake-example/1.0/image/usr/bin/cmake-example'  \
+            -ex 'target remote 192.168.7.2:1234'  \
+            -ex 'break main'  \
+            -ex 'continue'  \
+            -ex 'break CppExample::print_json()'  \
+            -ex 'continue'  \
+            -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789")'  \
+            -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")'  \
+            -ex 'list cpp-example-lib.hpp:14,14'  \
+            -ex 'continue'
+        3. Verifying debug output and stopping gdbserver
+        """
+        with open(os.path.join(tempdir, '.vscode', 'launch.json')) as launch_j:
+            launch_d = json.load(launch_j)
+        with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as tasks_j:
+            tasks_d = json.load(tasks_j)
+
+        configurations = launch_d["configurations"]
+        tasks = tasks_d["tasks"]
+
+        # Test one configuration for remote debugging
+        once_config_count = 0
+        for config in configurations:
+            if f"usr-bin-{recipe_name}_once" in config["name"]:
+                once_config_count += 1
+                self._verify_launch_config(tempdir, config, tasks, qemu, example_exe,
+                                           self._gdb_debug_cpp_example, self._gdb_debug_cpp_example_check)
+            # It works but is not 100% reliable in VSCode
+            # This one: https://github.com/microsoft/vscode-cpptools/issues/4243 ?
+            # elif f"usr-bin-{recipe_name}_attach" in config["name"]
+            #     self._verify_launch_config(tempdir, config, tasks, qemu, example_exe)
+            else:
+                continue
+        self.assertEqual(once_config_count, 1, f"Should have one once configuration, found: {once_config_count}")
+
+    def _verify_launch_config(self, tempdir, launch_config, tasks, qemu, example_exe, debug_func=None, debug_check_func=None):
+        self.assertIsNotNone(launch_config, "Should have at least one launch debug configuration")
+
+        # Extract configuration values for launch.json
+        debugger_path = launch_config["miDebuggerPath"]
+        server_addr = launch_config["miDebuggerServerAddress"]
+        prelaunch_task_name = launch_config["preLaunchTask"]
+        program = launch_config["program"]
+        additional_so_lib_search_path = launch_config["additionalSOLibSearchPath"]
+        source_file_map = launch_config["sourceFileMap"]
+        setup_commands = launch_config["setupCommands"]
+
+        # Find the preLaunchTask in tasks.json
+        prelaunch_task = next(
+            (task for task in tasks if task["label"] == prelaunch_task_name), None)
+        self.assertIsNotNone(prelaunch_task, f"PreLaunchTask '{prelaunch_task_name}' not found in tasks.json")
+
+        # Find the dependsOn task if exists (install and deploy-target)
+        if "dependsOn" in prelaunch_task:
+            depends_task_names = prelaunch_task["dependsOn"]
+            for depends_task_name in depends_task_names:
+                depends_task = next(
+                    (task for task in tasks if task["label"] == depends_task_name), None)
+                self.assertIsNotNone(depends_task, f"DependsOn task '{depends_task_name}' not found in tasks.json")
+                # For simplicity, we assume the dependsOn task is a prerequisite and does not affect the main command
+                self.logger.debug(f"PreLaunchTask '{prelaunch_task_name}' depends on '{depends_task_name}'")
+
+                # Extract command details from dependsOn task
+                depends_task_command = depends_task["command"]
+                depends_task_args = depends_task.get("args", [])
+                self.logger.debug(f"Would execute dependsOn task: {depends_task_command} {' '.join(depends_task_args)}")
+                runCmd(f"{depends_task_command} {' '.join(depends_task_args)}", output_log=self._cmd_logger)
+
+        # Verify task structure and extract command details
+        self.assertEqual(prelaunch_task["type"], "shell", f"Task '{prelaunch_task_name}' should be shell type")
+        task_command = prelaunch_task["command"]
+        task_args = prelaunch_task["args"]
+
+        # The command should be ssh for remote execution
+        self.assertEqual(task_command, "ssh", f"Task '{prelaunch_task_name}' should use ssh command")
+        self.assertTrue(len(task_args) >= 2, f"Task '{prelaunch_task_name}' should have at least 2 args (ssh options and remote command)")
+
+        sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+        result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'test -x /usr/bin/gdbserver'),
+                        output_log=self._cmd_logger)
+        self.assertEqual(result.status, 0, "gdbserver should be installed on target")
+        result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'test -x ' + os.path.join('/usr/bin', example_exe)),
+                        output_log=self._cmd_logger)
+        self.assertEqual(result.status, 0, "Example binary should be installed on target")
+
+        # Start gdbserver on target using the task command (keep the ssh connection open while debugging)
+        ssh_gdbserver_cmd = [task_command] + task_args
+        # Fix shell command escaping - remove extra quotes from the last argument
+        # The task_args likely contains a quoted shell command that needs to be unquoted
+        if len(ssh_gdbserver_cmd) > 0 and ssh_gdbserver_cmd[-1].startswith('"') and ssh_gdbserver_cmd[-1].endswith('"'):
+            ssh_gdbserver_cmd[-1] = ssh_gdbserver_cmd[-1][1:-1]  # Remove surrounding quotes
+        self.logger.debug(f"Starting gdbserver with command: {' '.join(ssh_gdbserver_cmd)}")
+        with RunCmdBackground(ssh_gdbserver_cmd, output_log=self._cmd_logger):
+            # Give gdbserver a moment to start
+            time.sleep(1)
+
+            # Verify gdbserver is running on target and listening on expected port
+            result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'), output_log=self._cmd_logger)
+            self.assertEqual(result.status, 0, "Failed to check processes on target")
+            self.assertIn("gdbserver", result.output, "gdbserver should be running on target")
+            _, server_port = server_addr.split(':')
+            self.assertIn(server_port, result.output, f"gdbserver should be listening on port {server_port}")
+
+            if debug_func and debug_check_func:
+                # Do a gdb remote session using the once configuration
+                gdb_batch_cmd = debugger_path + " --batch"
+                for setup_command in setup_commands:
+                    # What VSCode does, for tracing add "logging": {"engineLogging": true } to launch.json
+                    setup_cmd = setup_command["text"].strip()
+                    if setup_cmd.startswith("-"):
+                        # Ignore commands starting with '-' as they are VSCode internal commands?
+                        continue
+                    else:
+                        gdb_batch_cmd += ' -ex ' + shlex.quote(setup_cmd)
+                for k, v in source_file_map.items():
+                    gdb_batch_cmd += " -ex 'set substitute-path %s %s'" % (k, v.replace("${workspaceFolder}", tempdir))
+                gdb_batch_cmd += " -ex 'set solib-search-path %s'" % additional_so_lib_search_path
+                gdb_batch_cmd += " -ex 'file %s'" % program
+                gdb_batch_cmd += " -ex 'target remote %s'" % server_addr
+                # Add a basic set of command performing a simple debugging session
+                gdb_batch_cmd += debug_func(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, "continue")
+                self.logger.debug(f"Starting gdb session with command: {gdb_batch_cmd}")
+                r = runCmd(gdb_batch_cmd, output_log=self._cmd_logger)
+                self.logger.debug("%s %s returned: %s", debugger_path, gdb_batch_cmd, r.output)
+                self.assertEqual(r.status, 0)
+                debug_check_func(r.output, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
+
+    @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"
         build_file = "CMakeLists.txt"
         testimage = "oe-selftest-image"
+        build_file = "CMakeLists.txt"
 
         self._check_workspace()
         self._write_bb_config([recipe_name])
-        tempdir = self._devtool_ide_sdk_recipe(
-            recipe_name, build_file, testimage)
-        bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
-            recipe_name, testimage)
-        runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-        self._verify_cmake_preset(tempdir)
-        self._verify_install_script_code(tempdir,  recipe_name)
-        self._gdb_cross()
+
+        # Verify deployment to Qemu (system mode) works
+        self._check_runqemu_prerequisites()
+        bitbake(testimage)
+        with runqemu(testimage, runqemuparams="nographic") as qemu:
+            tempdir = self._devtool_ide_sdk_recipe(
+                recipe_name, build_file, testimage)
+            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=code' % (
+                recipe_name, testimage, qemu.ip)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+            self._verify_cmake_preset(tempdir)
+            self._verify_install_script_code(tempdir,  recipe_name)
+            self._gdb_cross()
+
+            # Verify the launch.json file created is valid
+            self._verify_launch_json(tempdir)
+
+            # Verify deployment and remote debugging works
+            self._verify_launch_json_debugging(tempdir, qemu, recipe_name, example_exe)
 
     def test_devtool_ide_sdk_code_meson(self):
         """Verify a meson recipe works with ide=code mode"""
-- 
2.52.0



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

* [PATCH v2 10/14] devtool: ide-sdk: evaluate DEBUG_PREFIX_MAP
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (8 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 09/14] oe-selftest: devtool ide-sdk cover vscode remote debugging AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 11/14] cpp-example: Add std::vector example AdrianF
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Improve the reverse mapping for searching the source files for remote
debugging by taking the details from DEBUG_PREFIX_MAP into account when
generating GDB's debug-file-directory mappings. This allows settings
such as DEBUG_PREFIX_MAP = "" for modified recipes to be used to avoid
any path remapping.

Background:
For packaged debug-symbols, the references to the source code need to be
relocated to paths which are valid on the target system. By default,
devtool ide-sdk tries to keep the relocated paths and configures the
debugger to reverse map them back to the original source paths. The goal
is to provide a debug setup which is a close as possible to a regular
build.

Usually this works well, but there are situations where the reverse
mapping is not unambiguous. For example the default DEBUG_PREFIX_MAP

 DEBUG_PREFIX_MAP ?= "\
 -ffile-prefix-map=${S}=${TARGET_DBGSRC_DIR} \
 -ffile-prefix-map=${B}=${TARGET_DBGSRC_DIR} \

adds two different source paths (${S} and ${B}) to the same target path
(${TARGET_DBGSRC_DIR}). If both source paths contain files with the same
name, the debugger cannot determine which source file to use. For this
example it is usually sufficient to only map ${S} to the target path.
The source files in ${B} are probably a few generated files which are
not that interesting for debugging. But depending on the project, the
files in ${B} might also be relevant for debugging.

Also add a hint to the generated local.conf snippet to use
DEBUG_PREFIX_MAP = "" if the user wants to optimize the build for
debugging.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_plugins/ide_code.py | 26 +++++---
 scripts/lib/devtool/ide_plugins/ide_none.py | 28 ++++----
 scripts/lib/devtool/ide_sdk.py              | 73 ++++++++++++++++++++-
 scripts/lib/devtool/standard.py             |  7 +-
 4 files changed, 111 insertions(+), 23 deletions(-)

diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py
index 05d513f160..647a5b8cc6 100644
--- a/scripts/lib/devtool/ide_plugins/ide_code.py
+++ b/scripts/lib/devtool/ide_plugins/ide_code.py
@@ -266,18 +266,26 @@ class IdeVSCode(IdeBase):
             launch_config['additionalSOLibSearchPath'] = modified_recipe.solib_search_path_str(
                 gdb_cross_config.image_recipe)
             # First: Search for sources of this recipe in the workspace folder
-            if modified_recipe.pn in modified_recipe.target_dbgsrc_dir:
-                src_file_map[modified_recipe.target_dbgsrc_dir] = "${workspaceFolder}"
-            else:
-                logger.error(
-                    "TARGET_DBGSRC_DIR must contain the recipe name PN.")
+            # If compiled with DEBUG_PREFIX_MAP = "", no reverse map is is needed. The binaries
+            # contain the full path to the source files. But by default there is a reverse map.
+            for target_path, host_path in modified_recipe.reverse_debug_prefix_map.items():
+                if host_path.startswith(modified_recipe.real_srctree):
+                    src_file_map[target_path] = "${workspaceFolder}" + host_path[len(modified_recipe.real_srctree):]
+                else:
+                    src_file_map[target_path] = host_path
+
             # Second: Search for sources of other recipes in the rootfs-dbg
-            if modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"):
+            # We expect that /usr/src/debug/${PN}/${PV} or no mapping is used for the recipes
+            # own sources and we can use the key "/usr/src/debug" for the rootfs-dbg.
+            if "/usr/src/debug" in src_file_map:
+                logger.error(
+                    'Key "/usr/src/debug" already exists in src_file_map. '
+                    'Something with DEBUG_PREFIX_MAP looks unexpected and finding '
+                    'sources in the rootfs-dbg will not work as expected.'
+                )
+            else:
                 src_file_map["/usr/src/debug"] = os.path.join(
                     gdb_cross_config.image_recipe.rootfs_dbg, "usr", "src", "debug")
-            else:
-                logger.error(
-                    "TARGET_DBGSRC_DIR must start with /usr/src/debug.")
         else:
             logger.warning(
                 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py
index 8284c4e0a5..ba65f6f7da 100644
--- a/scripts/lib/devtool/ide_plugins/ide_none.py
+++ b/scripts/lib/devtool/ide_plugins/ide_none.py
@@ -78,19 +78,25 @@ class GdbCrossConfigNone(GdbCrossConfig):
                              os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"')
         if self.image_recipe.rootfs_dbg:
             # First: Search for sources of this recipe in the workspace folder
-            if self.modified_recipe.pn in self.modified_recipe.target_dbgsrc_dir:
-                gdbinit_lines.append('set substitute-path "%s" "%s"' %
-                                     (self.modified_recipe.target_dbgsrc_dir, self.modified_recipe.real_srctree))
-            else:
-                logger.error(
-                    "TARGET_DBGSRC_DIR must contain the recipe name PN.")
+            # If compiled with DEBUG_PREFIX_MAP = "", no reverse map is is needed. The binaries
+            # contain the full path to the source files. But by default there is a reverse map.
+            src_file_map = dict(self.modified_recipe.reverse_debug_prefix_map)
+
             # Second: Search for sources of other recipes in the rootfs-dbg
-            if self.modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"):
-                gdbinit_lines.append('set substitute-path "/usr/src/debug" "%s"' % os.path.join(
-                    self.image_recipe.rootfs_dbg, "usr", "src", "debug"))
-            else:
+            # We expect that /usr/src/debug/${PN}/${PV} or no mapping is used for the recipes
+            # own sources and we can use the key "/usr/src/debug" for the rootfs-dbg.
+            if "/usr/src/debug" in src_file_map:
                 logger.error(
-                    "TARGET_DBGSRC_DIR must start with /usr/src/debug.")
+                    'Key "/usr/src/debug" already exists in src_file_map. '
+                    'Something with DEBUG_PREFIX_MAP looks unexpected and finding sources '
+                    'in the rootfs-dbg will not work as expected.'
+                )
+            else:
+                src_file_map["/usr/src/debug"] = os.path.join(
+                    self.image_recipe.rootfs_dbg, "usr", "src", "debug")
+
+            for target_path, host_path in src_file_map.items():
+                gdbinit_lines.append('set substitute-path "%s" "%s"' % (target_path, host_path))
         else:
             logger.warning(
                 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 96d60ad4f1..78d7ed78c6 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -14,6 +14,7 @@ import shutil
 import stat
 import subprocess
 import sys
+import shlex
 from argparse import RawTextHelpFormatter
 from enum import Enum
 
@@ -397,6 +398,7 @@ class RecipeModified:
         self.bpn = None
         self.d = None
         self.debug_build = None
+        self.reverse_debug_prefix_map = {}
         self.fakerootcmd = None
         self.fakerootenv = None
         self.libdir = None
@@ -408,10 +410,10 @@ class RecipeModified:
         self.recipe_id = None
         self.recipe_sysroot = None
         self.recipe_sysroot_native = None
+        self.s = None
         self.staging_incdir = None
         self.strip_cmd = None
         self.target_arch = None
-        self.target_dbgsrc_dir = None
         self.topdir = None
         self.workdir = None
         # Service management
@@ -479,13 +481,13 @@ class RecipeModified:
             recipe_d.getVar('RECIPE_SYSROOT'))
         self.recipe_sysroot_native = os.path.realpath(
             recipe_d.getVar('RECIPE_SYSROOT_NATIVE'))
+        self.s = recipe_d.getVar('S')
         self.staging_bindir_toolchain = os.path.realpath(
             recipe_d.getVar('STAGING_BINDIR_TOOLCHAIN'))
         self.staging_incdir = os.path.realpath(
             recipe_d.getVar('STAGING_INCDIR'))
         self.strip_cmd = recipe_d.getVar('STRIP')
         self.target_arch = recipe_d.getVar('TARGET_ARCH')
-        self.target_dbgsrc_dir = recipe_d.getVar('TARGET_DBGSRC_DIR')
         self.topdir = recipe_d.getVar('TOPDIR')
         self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR'))
 
@@ -504,6 +506,9 @@ class RecipeModified:
             self.meson_cross_file = recipe_d.getVar('MESON_CROSS_FILE')
             self.build_tool = BuildTool.MESON
 
+        self.reverse_debug_prefix_map = self._init_reverse_debug_prefix_map(
+            recipe_d.getVar('DEBUG_PREFIX_MAP'))
+
         # Recipe ID is the identifier for IDE config sections
         self.recipe_id = self.bpn + "-" + self.package_arch
         self.recipe_id_pretty = self.bpn + ": " + self.package_arch
@@ -563,6 +568,70 @@ class RecipeModified:
         """Return a : separated list of paths usable by GDB's set solib-search-path"""
         return ':'.join(self.solib_search_path(image))
 
+    def _init_reverse_debug_prefix_map(self, debug_prefix_map):
+        """Parses GCC map options and returns a mapping of target to host paths.
+
+        This function scans a string containing GCC options such as -fdebug-prefix-map,
+        -fmacro-prefix-map, and -ffile-prefix-map (in both '--option value' and '--option=value'
+        forms), extracting all mappings from target paths (used in debug info) to host source
+        paths. If multiple mappings for the same target path are found, the most suitable
+        host path is selected (preferring 'sources' over 'build' directories).
+        """
+        prefixes = ("-fdebug-prefix-map", "-fmacro-prefix-map", "-ffile-prefix-map")
+        all_mappings = {}
+        args = shlex.split(debug_prefix_map)
+        i = 0
+
+        # Collect all mappings, storing potentially multiple host paths per target path
+        while i < len(args):
+            arg = args[i]
+            mapping = None
+            for prefix in prefixes:
+                if arg == prefix:
+                    i += 1
+                    mapping = args[i]
+                    break
+                elif arg.startswith(prefix + '='):
+                    mapping = arg[len(prefix)+1:]
+                    break
+            if mapping:
+                host_path, target_path = mapping.split('=', 1)
+                if target_path:
+                    if target_path not in all_mappings:
+                        all_mappings[target_path] = []
+                    all_mappings[target_path].append(os.path.realpath(host_path))
+            i += 1
+
+        # Select the best host path for each target path (only 1:1 mappings are supported by GDB)
+        mappings = {}
+        unused_host_paths = []
+        for target_path, host_paths in all_mappings.items():
+            if len(host_paths) == 1:
+                mappings[target_path] = host_paths[0]
+            else:
+                # First priority path for sources is the source directory S
+                # Second priority path is any other directory
+                # Least priority is the build directory B, which probably contains only generated source files
+                sources_paths = [path for path in host_paths if os.path.realpath(path).startswith(os.path.realpath(self.s))]
+                if sources_paths:
+                    mappings[target_path] = sources_paths[0]
+                    unused_host_paths.extend([path for path in host_paths if path != sources_paths[0]])
+                else:
+                    # If no 'sources' path, prefer non-'build' paths
+                    non_build_paths = [path for path in host_paths if not os.path.realpath(path).startswith(os.path.realpath(self.b))]
+                    if non_build_paths:
+                        mappings[target_path] = non_build_paths[0]
+                        unused_host_paths.extend([path for path in host_paths if path != non_build_paths[0]])
+                    else:
+                        # Fall back to first path if all are build paths
+                        mappings[target_path] = host_paths[0]
+                        unused_host_paths.extend(host_paths[1:])
+
+        if unused_host_paths:
+            logger.info("Some source directories mapped by -fdebug-prefix-map are not included in the debugger search paths. Ignored host paths: %s", unused_host_paths)
+
+        return mappings
+
     def __init_exported_variables(self, d):
         """Find all variables with export flag set.
 
diff --git a/scripts/lib/devtool/standard.py b/scripts/lib/devtool/standard.py
index 1fd5947c41..f4d5d7cd3f 100644
--- a/scripts/lib/devtool/standard.py
+++ b/scripts/lib/devtool/standard.py
@@ -971,7 +971,12 @@ def modify(args, config, basepath, workspace):
                         continue
                     f.write('# patches_%s: %s\n' % (branch, ','.join(branch_patches[branch])))
             if args.debug_build:
-                f.write('\nDEBUG_BUILD = "1"\n')
+                f.write('\n# Optimize for debugging. Use e.g. -Og instead of -O2\n')
+                f.write('DEBUG_BUILD = "1"\n')
+                f.write('\n# Keep the paths to the source files for remote debugging\n')
+                f.write('# DEBUG_PREFIX_MAP = ""\n')
+                f.write('# WARN_QA:remove = "buildpaths"\n')
+                f.write('# ERROR_QA:remove = "buildpaths"\n')
 
         update_unlockedsigs(basepath, workspace, args.fixed_setup, [pn])
 
-- 
2.52.0



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

* [PATCH v2 11/14] cpp-example: Add std::vector example
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (9 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 10/14] devtool: ide-sdk: evaluate DEBUG_PREFIX_MAP AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 12/14] devtool: ide-sdk: Support GDB pretty-printing for C++ STL types AdrianF
                   ` (3 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Add a standard container (std::vector) to the C++ example program to
demonstrate the debugger's capability to inspect and traverse STL
containers during a debugging session. This requires enabling GDB's
pretty-printing feature, which depends on Python scripts shipped with
the compiler.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 meta-selftest/recipes-test/cpp/files/cpp-example.cpp | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp
index dbf82f15d9..23d7169092 100644
--- a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp
+++ b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp
@@ -9,6 +9,7 @@
 #include <iostream>
 #include <unistd.h>
 #include <string>
+#include <vector>
 
 int main(int argc, char* argv[])
 {
@@ -50,5 +51,12 @@ int main(int argc, char* argv[])
         }
     } while (endless_mode);
 
+    // Example: Demonstrate std::vector traversal for debugger inspection
+    std::vector<int> numbers = {1, 2, 3};
+    std::cout << "Traversing std::vector<int> numbers:" << std::endl;
+    for (size_t i = 0; i < numbers.size(); ++i) {
+        std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl;
+    }
+
     return 0;
 }
-- 
2.52.0



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

* [PATCH v2 12/14] devtool: ide-sdk: Support GDB pretty-printing for C++ STL types
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (10 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 11/14] cpp-example: Add std::vector example AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2025-12-31 11:46 ` [PATCH v2 13/14] oe-selftest: devtool: add test for gdb pretty-printing AdrianF
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

GDB requires Python scripts provided by GCC to properly display C++ STL
types. This commit adds support for configuring GDB to use these
pretty-printers in the ide-sdk, covering both the ide_none and ide_code
plugins.

The implementation locates the GCC Python helper scripts in the sysroot
and injects the necessary commands into the GDB initialization files and
IDE debug configurations. This ensures that when debugging C++
applications, STL containers and other complex types are displayed in a
readable format.

Without this:
  (gdb) print numbers
  $1 = {<std::_Vector_base<int, std::allocator<int> >> = {
    _M_impl = {<std::allocator<int>> = {<std::__new_allocator<int>> =
    {<No data fields>}, <No data fields>}, <std::_Vector_base<int,
    std::allocator<int> >::_Vector_impl_data> =
    {_M_start = 0x55555556c370, _M_finish = 0x55555556c37c,
        _M_end_of_storage = 0x55555556c37c}, <No data fields>}},
        <No data fields>}

With this:
  (gdb) print numbers
  $1 = std::vector of length 3, capacity 3 = {1, 2, 3}

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide_plugins/ide_code.py | 14 +++++++++++
 scripts/lib/devtool/ide_plugins/ide_none.py |  8 +++++++
 scripts/lib/devtool/ide_sdk.py              | 26 +++++++++++++++++++++
 3 files changed, 48 insertions(+)

diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py
index 647a5b8cc6..c2ee9b91c6 100644
--- a/scripts/lib/devtool/ide_plugins/ide_code.py
+++ b/scripts/lib/devtool/ide_plugins/ide_code.py
@@ -290,6 +290,20 @@ class IdeVSCode(IdeBase):
             logger.warning(
                 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
 
+        # Enable pretty-printing for gdb for resolving STL types with help of python scripts
+        pretty_printing_cmd = modified_recipe.gdb_pretty_print_scripts
+        if pretty_printing_cmd:
+            setup_commands += [
+                {
+                    "description": "Enable pretty-printing for gdb",
+                    "text": "python " +";".join(pretty_printing_cmd)
+                },
+                {
+                    "description": "Enable pretty-printing for gdb",
+                    "text": "-enable-pretty-printing"
+                }
+            ]
+
         launch_config['sourceFileMap'] = src_file_map
         launch_config['setupCommands'] = setup_commands
 
diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py
index ba65f6f7da..ed96afa33c 100644
--- a/scripts/lib/devtool/ide_plugins/ide_none.py
+++ b/scripts/lib/devtool/ide_plugins/ide_none.py
@@ -102,6 +102,14 @@ class GdbCrossConfigNone(GdbCrossConfig):
                 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
         # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir.
         gdbinit_lines.append('set debuginfod enabled off')
+
+        # Enable pretty-printing for gdb for resolving STL types with help of python scripts
+        pretty_printing_cmd = self.modified_recipe.gdb_pretty_print_scripts
+        if pretty_printing_cmd:
+            gdbinit_lines.append(os.linesep +"python")
+            gdbinit_lines += pretty_printing_cmd
+            gdbinit_lines.append("end" + os.linesep)
+
         gdbinit_lines.append(
             '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port))
         gdbinit_lines.append('set remote exec-file ' + self.binary.binary_path)
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index 78d7ed78c6..ba225f20b9 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -15,6 +15,7 @@ import stat
 import subprocess
 import sys
 import shlex
+import glob
 from argparse import RawTextHelpFormatter
 from enum import Enum
 
@@ -414,6 +415,7 @@ class RecipeModified:
         self.staging_incdir = None
         self.strip_cmd = None
         self.target_arch = None
+        self.tcoverride = None
         self.topdir = None
         self.workdir = None
         # Service management
@@ -437,6 +439,7 @@ class RecipeModified:
 
         # Populated after bitbake built all the recipes
         self._installed_binaries = None
+        self._gdb_pretty_print_scripts = None
 
     def initialize(self, config, workspace, tinfoil):
         recipe_d = parse_recipe(
@@ -488,6 +491,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.topdir = recipe_d.getVar('TOPDIR')
         self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR'))
 
@@ -632,6 +636,28 @@ class RecipeModified:
 
         return mappings
 
+    @property
+    def gdb_pretty_print_scripts(self):
+        if self._gdb_pretty_print_scripts is None:
+            if self.tcoverride == "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:
+                    gcc_python_helpers = gcc_python_helpers_dirs[0]
+                else:
+                    logger.warning("Could not find gcc python helpers directory matching: %s", gcc_python_helpers_pattern)
+                    gcc_python_helpers = ""
+                pretty_print_scripts = [
+                    "import sys",
+                    "sys.path.insert(0, '" + gcc_python_helpers + "')",
+                    "from libstdcxx.v6.printers import register_libstdcxx_printers",
+                    "register_libstdcxx_printers(None)"
+                ]
+                self._gdb_pretty_print_scripts = pretty_print_scripts
+            else:
+                self._gdb_pretty_print_scripts = ""
+        return self._gdb_pretty_print_scripts
+
     def __init_exported_variables(self, d):
         """Find all variables with export flag set.
 
-- 
2.52.0



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

* [PATCH v2 13/14] oe-selftest: devtool: add test for gdb pretty-printing
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (11 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 12/14] devtool: ide-sdk: Support GDB pretty-printing for C++ STL types AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2026-01-02 14:44   ` [OE-core] " Mathieu Dubois-Briand
  2025-12-31 11:46 ` [PATCH v2 14/14] oe-selftest: devtool: add compile step in ide-sdk tests AdrianF
  2026-01-13  8:17 ` [OE-core] [PATCH v2 00/14] IDE SDK Improvements Antonin Godard
  14 siblings, 1 reply; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

This extends the existing devtool IDE SDK tests to verify that gdb
pretty-printing is working correctly.

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

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index df5c863a85..3f00ce8ffb 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -2725,6 +2725,14 @@ class DevtoolIdeSdkTests(DevtoolBase):
         gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
         gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
         gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
+
+        # check if resolving std::vector works with python scripts
+        # break at line 56, because the line which initializes the vector
+        # may be optimized out by the compiler
+        gdb_batch_cmd += " -ex 'list cpp-example.cpp:55,55'"
+        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'"
         return gdb_batch_cmd
 
@@ -2734,6 +2742,10 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("$2 = -3", gdb_output)  # test.string.compare longer
         self.assertIn(
             'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, gdb_output)
+
+        # check if resolving std::vector works with python scripts
+        self.assertRegex(gdb_output, r"55\s+std::vector<int> numbers = \{1, 2, 3\};")
+        self.assertRegex(gdb_output, r"\$\d+ = std::vector of length 3, capacity 3 = \{1, 2, 3\}")
         self.assertIn("exited normally", gdb_output)
 
     def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string):
-- 
2.52.0



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

* [PATCH v2 14/14] oe-selftest: devtool: add compile step in ide-sdk tests
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (12 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 13/14] oe-selftest: devtool: add test for gdb pretty-printing AdrianF
@ 2025-12-31 11:46 ` AdrianF
  2026-01-13  8:17 ` [OE-core] [PATCH v2 00/14] IDE SDK Improvements Antonin Godard
  14 siblings, 0 replies; 20+ messages in thread
From: AdrianF @ 2025-12-31 11:46 UTC (permalink / raw)
  To: openembedded-core; +Cc: Adrian Freihofer

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

Add explicit compile step to the ide-sdk test workflow. The current
implementation relies on calling bitbake -c install to perform the
install step, which also triggers a build. But this will change when
bitbake will support task execution without handling dependencies.
To make the tests future-proof, add an explicit compile step after
modifying the source code.

This also improves the test coverage for meson based recipes, as the
compile step is now explicitly tested.

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

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 3f00ce8ffb..73282111d5 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -2574,6 +2574,9 @@ class DevtoolIdeSdkTests(DevtoolBase):
     def _sources_scripts_dir(self, src_dir):
         return os.path.realpath(os.path.join(src_dir, 'oe-scripts'))
 
+    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'))
 
@@ -2642,7 +2645,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             self._workspace_scripts_dir(recipe_name), i_and_d_script)
         self.assertExists(i_and_d_script_path)
 
-    def _devtool_ide_sdk_qemu(self, tempdir, qemu, recipe_name, example_exe):
+    def _devtool_ide_sdk_qemu(self, tempdir, qemu, recipe_name, example_exe, compile_cmd):
         """Verify deployment and execution in Qemu system work for one recipe.
 
         This function checks the entire SDK workflow: changing the code, recompiling
@@ -2687,6 +2690,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
             cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
         with open(cpp_example_lib_hpp, 'w') as file:
             file.write(cpp_code)
+        runCmd(compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
         runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger)
 
         # Verify the modified example prints the modified magic string
@@ -2818,6 +2822,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertEqual(len(config_presets), 1)
         cmake_exe = config_presets[0]["cmakeExecutable"]
         preset_name = config_presets[0]["name"]
+        compile_cmd = '%s --build --preset %s' % (cmake_exe, preset_name)
 
         # Verify the wrapper for cmake native is available
         self.assertExists(cmake_exe)
@@ -2827,28 +2832,59 @@ class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn(preset_name, result.output)
 
         # Verify cmake re-uses the o files compiled by bitbake
-        result = runCmd('%s --build --preset %s' %
-                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
+        result = runCmd(compile_cmd, 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)
-        result = runCmd('%s --build --preset %s --target test' %
-                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
+        result = runCmd('%s --target test' % compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("100% tests passed", result.output)
 
         # Verify re-building and testing works again
-        result = runCmd('%s --build --preset %s --target clean' %
-                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
+        result = runCmd('%s --target clean' % compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("Cleaning", result.output)
-        result = runCmd('%s --build --preset %s' %
-                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
+        result = runCmd(compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("Building", result.output)
         self.assertIn("Linking", result.output)
-        result = runCmd('%s --build --preset %s --target test' %
-                        (cmake_exe, preset_name), cwd=tempdir, output_log=self._cmd_logger)
+        result = runCmd('%s --target test' % compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
         self.assertIn("Running tests...", result.output)
         self.assertIn("100% tests passed", result.output)
 
+        return compile_cmd
+
+    def _verify_meson_build(self, tempdir, recipe_name):
+        """Verify meson works as expected
+
+        Check if compiling works
+        Check if unit tests can be executed in qemu (not qemu-system)
+        """
+        meson_exe = os.path.join(self._workspace_scripts_dir(recipe_name), "meson")
+        self.assertExists(meson_exe)
+        build_dir = os.path.join(self._sources_workdir_dir(tempdir), recipe_name + "-1.0")
+        compile_cmd = '%s compile -C %s' % (meson_exe, build_dir)
+
+        # Verify meson re-uses the o files compiled by bitbake
+        result = runCmd(compile_cmd, 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)
+        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)
+
+        # Verify re-building and testing works again
+        result = runCmd('%s compile -C %s --clean' % (meson_exe, build_dir),
+                        cwd=tempdir, output_log=self._cmd_logger)
+        self.assertIn("Cleaning...", result.output)
+        result = runCmd(compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
+        self.assertIn("Linking target", result.output)
+        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)
+
+        return compile_cmd
+
     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)
@@ -2898,8 +2934,8 @@ class DevtoolIdeSdkTests(DevtoolBase):
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
 
             self._gdb_cross()
-            self._verify_cmake_preset(tempdir)
-            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
+            compile_cmd = self._verify_cmake_preset(tempdir)
+            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(
@@ -2930,7 +2966,8 @@ class DevtoolIdeSdkTests(DevtoolBase):
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
 
             self._gdb_cross()
-            self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
+            compile_cmd = self._verify_meson_build(tempdir, recipe_name)
+            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(
-- 
2.52.0



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

* Re: [OE-core] [PATCH v2 13/14] oe-selftest: devtool: add test for gdb pretty-printing
  2025-12-31 11:46 ` [PATCH v2 13/14] oe-selftest: devtool: add test for gdb pretty-printing AdrianF
@ 2026-01-02 14:44   ` Mathieu Dubois-Briand
  0 siblings, 0 replies; 20+ messages in thread
From: Mathieu Dubois-Briand @ 2026-01-02 14:44 UTC (permalink / raw)
  To: adrian.freihofer, openembedded-core

On Wed Dec 31, 2025 at 12:46 PM CET, Adrian Freihofer via lists.openembedded.org wrote:
> From: Adrian Freihofer <adrian.freihofer@siemens.com>
>
> This extends the existing devtool IDE SDK tests to verify that gdb
> pretty-printing is working correctly.
>
> Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
> ---

Hi Adrian,

Thanks for the new version, but it looks like it is still failing:

2026-01-02 10:09:04,822 - oe-selftest - INFO - 11: 2/36 112/655 (365.52s) (0 failed) (devtool.DevtoolIdeSdkTests.test_devtool_ide_sdk_code_cmake)
2026-01-02 10:09:04,822 - oe-selftest - INFO - testtools.testresult.real._StringException: Traceback (most recent call last):
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 3237, in test_devtool_ide_sdk_code_cmake
    self._verify_launch_json_debugging(tempdir, qemu, recipe_name, example_exe)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 3106, in _verify_launch_json_debugging
    self._verify_launch_config(tempdir, config, tasks, qemu, example_exe,
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                               self._gdb_debug_cpp_example, self._gdb_debug_cpp_example_check)
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 3206, in _verify_launch_config
    debug_check_func(r.output, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 2750, in _gdb_debug_cpp_example_check
    self.assertRegex(gdb_output, r"\$\d+ = std::vector of length 3, capacity 3 = \{1, 2, 3\}")
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/unittest/case.py", line 1369, in assertRegex
    raise self.failureException(msg)
AssertionError: Regex didn't match: '\\$\\d+ = std::vector of length 3, capacity 3 = \\{1, 2, 3\\}' not found in '_start () at ../sysdeps/aarch64/dl-start.S:23\n23\tENTRY (_start)\nBreakpoint 1 at 0xaaaaaaaa1400: file /usr/src/debug/cmake-example/1.0/cpp-example.cpp, line 15.\n\nBreakpoint 1, main (argc=1, argv=0xfffffffffd08) at /usr/src/debug/cmake-example/1.0/cpp-example.cpp:15\n15\t{\nBreakpoint 2 at 0xfffff7f91d70: file /usr/src/debug/cmake-example/1.0/cpp-example-lib.cpp, line 28.\n\nBreakpoint 2, CppExample::print_json (this=0xfffffffffac8) at /usr/src/debug/cmake-example/1.0/cpp-example-lib.cpp:28\n28\t    jobj = json_object_new_object();\n$1 = 0\n$2 = -3\n14\t    inline static const std::string test_string = "cpp-example-lib Magic: 123456789";\n55\t    std::vector<int> numbers = {1, 2, 3};\nBreakpoint 3 at 0xaaaaaaaa1780: file /usr/src/debug/cmake-example/1.0/cpp-example.cpp, line 56.\n\nBreakpoint 3, std::uninitialized_copy<int const*, int*> (__first=<optimized out>, __last=<optimized out>, __result=<optimized out>) at /usr/src/debug/cmake-example/1.0/cpp-example.cpp:56\n56\t    std::cout << "Traversing std::vector<int> numbers:" << std::endl;\nAttempt to use a type name as an expression\n[Inferior 1 (process 346) exited normally]'

And a similar one for devtool.DevtoolIdeSdkTests.test_devtool_ide_sdk_none_qemu.

https://autobuilder.yoctoproject.org/valkyrie/#/builders/23/builds/3078

I know debugging this might be a bit tough, as it is arm host only. If
you have doubts about what is failing, I can do some tests next week.

Thanks,
Mathieu

-- 
Mathieu Dubois-Briand, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com



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

* Re: [OE-core] [PATCH v2 00/14] IDE SDK Improvements
  2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
                   ` (13 preceding siblings ...)
  2025-12-31 11:46 ` [PATCH v2 14/14] oe-selftest: devtool: add compile step in ide-sdk tests AdrianF
@ 2026-01-13  8:17 ` Antonin Godard
  2026-01-14 18:37   ` adrian.freihofer
  14 siblings, 1 reply; 20+ messages in thread
From: Antonin Godard @ 2026-01-13  8:17 UTC (permalink / raw)
  To: adrian.freihofer, openembedded-core

Hi Adrian,

I think I hit a different error on the Autobuilder with your series:

2026-01-12 21:40:10,012 - oe-selftest - INFO - FAIL: devtool.DevtoolIdeSdkTests.test_devtool_ide_sdk_code_cmake (subunit.RemotedTestCase)
2026-01-12 21:40:10,012 - oe-selftest - INFO - ----------------------------------------------------------------------
2026-01-12 21:40:10,012 - oe-selftest - INFO - testtools.testresult.real._StringException: Traceback (most recent call last):
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 3238, in test_devtool_ide_sdk_code_cmake
    self._verify_launch_json_debugging(tempdir, qemu, recipe_name, example_exe)
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 3107, in _verify_launch_json_debugging
    self._verify_launch_config(tempdir, config, tasks, qemu, example_exe,
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/selftest/cases/devtool.py", line 3204, in _verify_launch_config
    r = runCmd(gdb_batch_cmd, output_log=self._cmd_logger)
  File "/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/layers/openembedded-core/meta/lib/oeqa/utils/commands.py", line 214, in runCmd
    raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, exc_output))
AssertionError: Command '/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/aarch64-linux/gdb-cross-aarch64/16.3/recipe-sysroot-native/usr/bin/aarch64-poky-linux/aarch64-poky-linux-gdb --batch -ex 'set sysroot /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/cortexa57-poky-linux/cmake-example/1.0/image' -ex 'python import sys;sys.path.insert(0, '"'"'/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/cortexa57-poky-linux/cmake-example/1.0/recipe-sysroot/usr/share/gcc-15.2.0/python'"'"');from libstdcxx.v6.printers import register_libstdcxx_printers;register_libstdcxx_printers(None)' -ex 'set substitute-path /usr/include /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/cortexa57-poky-linux/cmake-example/1.0/recipe-sysroot/usr/include' -ex 'set substitute-path /usr/src/debug/cmake-example/1.0 /tmp/devtoolqa5x_myf_r' -ex 'set substitute-path /usr/src/debug /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/src/debug' -ex 'set solib-search-path /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/lib/.debug:/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib/.debug:/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib/debug:/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/lib:/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib:/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs/lib:/srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs/usr/lib' -ex 'file /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-241514/tmp/work/cortexa57-poky-linux/cmake-example/1.0/image/usr/bin/cmake-example' -ex 'target remote 192.168.7.2:1234' -ex 'break main' -ex 'continue' -ex 'break CppExample::print_json()' -ex 'continue' -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789")' -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")' -ex 'list cpp-example-lib.hpp:14,14' -ex 'list cpp-example.cpp:55,55' -ex 'break cpp-example.cpp:55' -ex 'continue' -ex 'print numbers' -ex 'continue'' returned non-zero exit status 1:
_start () at ../sysdeps/aarch64/dl-start.S:23
23	ENTRY (_start)
Breakpoint 1 at 0xaaaaaaaa1400: file /usr/src/debug/cmake-example/1.0/cpp-example.cpp, line 15.

Breakpoint 1, main (argc=1, argv=0xfffffffffd08) at /usr/src/debug/cmake-example/1.0/cpp-example.cpp:15
15	{
Breakpoint 2 at 0xfffff7f91d70: file /usr/src/debug/cmake-example/1.0/cpp-example-lib.cpp, line 28.

Breakpoint 2, CppExample::print_json (this=0xfffffffffac8) at /usr/src/debug/cmake-example/1.0/cpp-example-lib.cpp:28
28	    jobj = json_object_new_object();
$1 = 0
$2 = -3
14	    inline static const std::string test_string = "cpp-example-lib Magic: 123456789";
55	    std::vector<int> numbers = {1, 2, 3};
No compiled code for line 55 in file "cpp-example.cpp".
Make breakpoint pending on future shared library load? (y or [n]) [answered N; input not from terminal]
[Inferior 1 (process 333) exited normally]
A syntax error in expression, near the end of `numbers'.
The program is not being run.

It's also on an Arm host as Mathieu also noticed previously.

Can you have a look?

https://autobuilder.yoctoproject.org/valkyrie/#/builders/23/builds/3145
https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/4913998/raw_inline

Thanks,
Antonin

-- 
Antonin Godard, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com



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

* Re: [OE-core] [PATCH v2 00/14] IDE SDK Improvements
  2026-01-13  8:17 ` [OE-core] [PATCH v2 00/14] IDE SDK Improvements Antonin Godard
@ 2026-01-14 18:37   ` adrian.freihofer
  2026-01-20 11:59     ` Antonin Godard
  0 siblings, 1 reply; 20+ messages in thread
From: adrian.freihofer @ 2026-01-14 18:37 UTC (permalink / raw)
  To: antonin.godard, adrian.freihofer, openembedded-core

Hi Antonin

My proposal for proceeding here is to drop one patch from my series:
  [OE-core] [PATCH v2 13/14]
  oe-selftest: devtool: add test for gdb pretty-printing

This one get killed by patches which are currently on master-next too.
This makes it almost impossible to fix at the moment.
When things will settle down, I will test and send this one again.

It is just one more test step which would be nice to have but not
essential.
It also fails only on a ARM host, which I cannot reproduce.

All in all just too many floating parts for getting this to final state
right now.

Could we please proceed with 13 out of 14 patches from my series as a
next step? Mathieu already hat such a branch for a while.

Thank you.
Adrian



On Tue, 2026-01-13 at 09:17 +0100, Antonin Godard via
lists.openembedded.org wrote:
> Hi Adrian,
> 
> I think I hit a different error on the Autobuilder with your series:
> 
> 2026-01-12 21:40:10,012 - oe-selftest - INFO - FAIL:
> devtool.DevtoolIdeSdkTests.test_devtool_ide_sdk_code_cmake
> (subunit.RemotedTestCase)
> 2026-01-12 21:40:10,012 - oe-selftest - INFO - ----------------------
> ------------------------------------------------
> 2026-01-12 21:40:10,012 - oe-selftest - INFO -
> testtools.testresult.real._StringException: Traceback (most recent
> call last):
>   File "/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/layers/openembedded-
> core/meta/lib/oeqa/selftest/cases/devtool.py", line 3238, in
> test_devtool_ide_sdk_code_cmake
>     self._verify_launch_json_debugging(tempdir, qemu, recipe_name,
> example_exe)
>   File "/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/layers/openembedded-
> core/meta/lib/oeqa/selftest/cases/devtool.py", line 3107, in
> _verify_launch_json_debugging
>     self._verify_launch_config(tempdir, config, tasks, qemu,
> example_exe,
>   File "/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/layers/openembedded-
> core/meta/lib/oeqa/selftest/cases/devtool.py", line 3204, in
> _verify_launch_config
>     r = runCmd(gdb_batch_cmd, output_log=self._cmd_logger)
>   File "/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/layers/openembedded-
> core/meta/lib/oeqa/utils/commands.py", line 214, in runCmd
>     raise AssertionError("Command '%s' returned non-zero exit status
> %d:\n%s" % (command, result.status, exc_output))
> AssertionError: Command '/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/build-st-241514/tmp/work/aarch64-linux/gdb-cross-
> aarch64/16.3/recipe-sysroot-native/usr/bin/aarch64-poky-
> linux/aarch64-poky-linux-gdb --batch -ex 'set sysroot
> /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-
> 241514/tmp/work/cortexa57-poky-linux/cmake-example/1.0/image' -ex
> 'python import sys;sys.path.insert(0, '"'"'/srv/pokybuild/yocto-
> worker/oe-selftest-armhost/build/build-st-241514/tmp/work/cortexa57-
> poky-linux/cmake-example/1.0/recipe-sysroot/usr/share/gcc-
> 15.2.0/python'"'"');from libstdcxx.v6.printers import
> register_libstdcxx_printers;register_libstdcxx_printers(None)' -ex
> 'set substitute-path /usr/include /srv/pokybuild/yocto-worker/oe-
> selftest-armhost/build/build-st-241514/tmp/work/cortexa57-poky-
> linux/cmake-example/1.0/recipe-sysroot/usr/include' -ex 'set
> substitute-path /usr/src/debug/cmake-example/1.0
> /tmp/devtoolqa5x_myf_r' -ex 'set substitute-path /usr/src/debug
> /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-
> 241514/tmp/work/qemuarm64-poky-linux/oe-selftest-image/1.0/rootfs-
> dbg/usr/src/debug' -ex 'set solib-search-path /srv/pokybuild/yocto-
> worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-
> poky-linux/oe-selftest-image/1.0/rootfs-
> dbg/lib/.debug:/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-
> selftest-image/1.0/rootfs-dbg/usr/lib/.debug:/srv/pokybuild/yocto-
> worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-
> poky-linux/oe-selftest-image/1.0/rootfs-
> dbg/usr/lib/debug:/srv/pokybuild/yocto-worker/oe-selftest-
> armhost/build/build-st-241514/tmp/work/qemuarm64-poky-linux/oe-
> selftest-image/1.0/rootfs-dbg/lib:/srv/pokybuild/yocto-worker/oe-
> selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-poky-
> linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib:/srv/pokybuild/yocto-
> worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-
> poky-linux/oe-selftest-image/1.0/rootfs/lib:/srv/pokybuild/yocto-
> worker/oe-selftest-armhost/build/build-st-241514/tmp/work/qemuarm64-
> poky-linux/oe-selftest-image/1.0/rootfs/usr/lib' -ex 'file
> /srv/pokybuild/yocto-worker/oe-selftest-armhost/build/build-st-
> 241514/tmp/work/cortexa57-poky-linux/cmake-
> example/1.0/image/usr/bin/cmake-example' -ex 'target remote
> 192.168.7.2:1234' -ex 'break main' -ex 'continue' -ex 'break
> CppExample::print_json()' -ex 'continue' -ex 'print
> CppExample::test_string.compare("cpp-example-lib Magic: 123456789")'
> -ex 'print CppExample::test_string.compare("cpp-example-lib Magic:
> 123456789aaa")' -ex 'list cpp-example-lib.hpp:14,14' -ex 'list cpp-
> example.cpp:55,55' -ex 'break cpp-example.cpp:55' -ex 'continue' -ex
> 'print numbers' -ex 'continue'' returned non-zero exit status 1:
> _start () at ../sysdeps/aarch64/dl-start.S:23
> 23	ENTRY (_start)
> Breakpoint 1 at 0xaaaaaaaa1400: file /usr/src/debug/cmake-
> example/1.0/cpp-example.cpp, line 15.
> 
> Breakpoint 1, main (argc=1, argv=0xfffffffffd08) at
> /usr/src/debug/cmake-example/1.0/cpp-example.cpp:15
> 15	{
> Breakpoint 2 at 0xfffff7f91d70: file /usr/src/debug/cmake-
> example/1.0/cpp-example-lib.cpp, line 28.
> 
> Breakpoint 2, CppExample::print_json (this=0xfffffffffac8) at
> /usr/src/debug/cmake-example/1.0/cpp-example-lib.cpp:28
> 28	    jobj = json_object_new_object();
> $1 = 0
> $2 = -3
> 14	    inline static const std::string test_string = "cpp-
> example-lib Magic: 123456789";
> 55	    std::vector<int> numbers = {1, 2, 3};
> No compiled code for line 55 in file "cpp-example.cpp".
> Make breakpoint pending on future shared library load? (y or [n])
> [answered N; input not from terminal]
> [Inferior 1 (process 333) exited normally]
> A syntax error in expression, near the end of `numbers'.
> The program is not being run.
> 
> It's also on an Arm host as Mathieu also noticed previously.
> 
> Can you have a look?
> 
> https://autobuilder.yoctoproject.org/valkyrie/#/builders/23/builds/3145
> https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/4913998/raw_inline
> 
> Thanks,
> Antonin
> 
> 
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#229231):
> https://lists.openembedded.org/g/openembedded-core/message/229231
> Mute This Topic: https://lists.openembedded.org/mt/117010483/4454582
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe:
> https://lists.openembedded.org/g/openembedded-core/unsub [
> adrian.freihofer@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-


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

* Re: [OE-core] [PATCH v2 00/14] IDE SDK Improvements
  2026-01-14 18:37   ` adrian.freihofer
@ 2026-01-20 11:59     ` Antonin Godard
  2026-01-26  7:47       ` adrian.freihofer
  0 siblings, 1 reply; 20+ messages in thread
From: Antonin Godard @ 2026-01-20 11:59 UTC (permalink / raw)
  To: adrian.freihofer, adrian.freihofer, openembedded-core

Hi,

On Wed Jan 14, 2026 at 7:37 PM CET, adrian.freihofer wrote:
> Hi Antonin
>
> My proposal for proceeding here is to drop one patch from my series:
>   [OE-core] [PATCH v2 13/14]
>   oe-selftest: devtool: add test for gdb pretty-printing
>
> This one get killed by patches which are currently on master-next too.
> This makes it almost impossible to fix at the moment.
> When things will settle down, I will test and send this one again.
>
> It is just one more test step which would be nice to have but not
> essential.
> It also fails only on a ARM host, which I cannot reproduce.
>
> All in all just too many floating parts for getting this to final state
> right now.
>
> Could we please proceed with 13 out of 14 patches from my series as a
> next step? Mathieu already hat such a branch for a while.

Sorry for messing up with what Mathieu had on his branch, probably a miss on my
side as I've picked up his work.

I picked your patches again, and while some oe-selftest runs did succeed, I hit
this error, which seem related to your series:

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

From this build:
https://autobuilder.yoctoproject.org/valkyrie/#/builders/35/builds/3035

It was run with the following commits:
https://git.yoctoproject.org/poky-ci-archive/log/?h=oecore/autobuilder.yoctoproject.org/valkyrie/a-full-3073

I don't recall hitting the issue before having your series in my branch, so to
be sure I ran another build with only your series applied on master, and I still
have the issue:
https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/5038061/raw_inline

Can you have a look?

Antonin

-- 
Antonin Godard, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com



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

* Re: [OE-core] [PATCH v2 00/14] IDE SDK Improvements
  2026-01-20 11:59     ` Antonin Godard
@ 2026-01-26  7:47       ` adrian.freihofer
  0 siblings, 0 replies; 20+ messages in thread
From: adrian.freihofer @ 2026-01-26  7:47 UTC (permalink / raw)
  To: Antonin Godard; +Cc: openembedded-core

On Tue, 2026-01-20 at 12:59 +0100, Antonin Godard wrote:
> Hi,
> 
> On Wed Jan 14, 2026 at 7:37 PM CET, adrian.freihofer wrote:
> > Hi Antonin
> > 
> > My proposal for proceeding here is to drop one patch from my
> > series:
> >   [OE-core] [PATCH v2 13/14]
> >   oe-selftest: devtool: add test for gdb pretty-printing
> > 
> > This one get killed by patches which are currently on master-next
> > too.
> > This makes it almost impossible to fix at the moment.
> > When things will settle down, I will test and send this one again.
> > 
> > It is just one more test step which would be nice to have but not
> > essential.
> > It also fails only on a ARM host, which I cannot reproduce.
> > 
> > All in all just too many floating parts for getting this to final
> > state
> > right now.
> > 
> > Could we please proceed with 13 out of 14 patches from my series as
> > a
> > next step? Mathieu already hat such a branch for a while.
> 
> Sorry for messing up with what Mathieu had on his branch, probably a
> miss on my
> side as I've picked up his work.

The situation was very confusing.
> 
> I picked your patches again, and while some oe-selftest runs did
> succeed, I hit
> this error, which seem related to your series:
> 
> https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/4983396/raw_inline
> 
> From this build:
> https://autobuilder.yoctoproject.org/valkyrie/#/builders/35/builds/3035
> 
> It was run with the following commits:
> https://git.yoctoproject.org/poky-ci-archive/log/?h=oecore/autobuilder.yoctoproject.org/valkyrie/a-full-3073
> 
> I don't recall hitting the issue before having your series in my
> branch, so to
> be sure I ran another build with only your series applied on master,
> and I still
> have the issue:
> https://autobuilder.yoctoproject.org/valkyrie/api/v2/logs/5038061/raw_inline
> 
> Can you have a look?

Yes, it seams that my patches can trigger this race issue. I sent a v3
with a fix.

Thank you for the extra effort. It's very helpful.

Adrian

> 
> Antonin


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

end of thread, other threads:[~2026-01-26  7:47 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-31 11:46 [PATCH v2 00/14] IDE SDK Improvements AdrianF
2025-12-31 11:46 ` [PATCH v2 01/14] devtool: ide-sdk find bitbake-setup init-build-env AdrianF
2025-12-31 11:46 ` [PATCH v2 02/14] oe-selftest: devtool: DevtoolIdeSdkTests debug logging AdrianF
2025-12-31 11:46 ` [PATCH v2 03/14] cpp-example: run as a service AdrianF
2025-12-31 11:46 ` [PATCH v2 04/14] oe-selftest: devtool: check example services are running AdrianF
2025-12-31 11:46 ` [PATCH v2 05/14] devtool: ide-sdk: add gdbserver attach mode support AdrianF
2025-12-31 11:46 ` [PATCH v2 06/14] devtool: ide-sdk: move code to ide_none AdrianF
2025-12-31 11:46 ` [PATCH v2 07/14] devtool: ide-sdk: make install_and_deploy script pass target arg AdrianF
2025-12-31 11:46 ` [PATCH v2 08/14] devtool: ide-sdk: vscode replace scripts AdrianF
2025-12-31 11:46 ` [PATCH v2 09/14] oe-selftest: devtool ide-sdk cover vscode remote debugging AdrianF
2025-12-31 11:46 ` [PATCH v2 10/14] devtool: ide-sdk: evaluate DEBUG_PREFIX_MAP AdrianF
2025-12-31 11:46 ` [PATCH v2 11/14] cpp-example: Add std::vector example AdrianF
2025-12-31 11:46 ` [PATCH v2 12/14] devtool: ide-sdk: Support GDB pretty-printing for C++ STL types AdrianF
2025-12-31 11:46 ` [PATCH v2 13/14] oe-selftest: devtool: add test for gdb pretty-printing AdrianF
2026-01-02 14:44   ` [OE-core] " Mathieu Dubois-Briand
2025-12-31 11:46 ` [PATCH v2 14/14] oe-selftest: devtool: add compile step in ide-sdk tests AdrianF
2026-01-13  8:17 ` [OE-core] [PATCH v2 00/14] IDE SDK Improvements Antonin Godard
2026-01-14 18:37   ` adrian.freihofer
2026-01-20 11:59     ` Antonin Godard
2026-01-26  7:47       ` adrian.freihofer

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