From patchwork Wed Sep 9 21:53:01 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97787 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id C1F7BC88E4D for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-225.siemens.flowmailer.net (mta-65-225.siemens.flowmailer.net [185.136.65.225]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.76.1788990824405280225 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=Nj+a5AYA; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.225, mailfrom: fm-1329275-202609092153415d402edd8100020779-65sx_e@rts-flowmailer.siemens.com) Received: by mta-65-225.siemens.flowmailer.net with ESMTPSA id 202609092153415d402edd8100020779 for ; Wed, 09 Sep 2026 23:53:41 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=ANLY7FwUIgeZ9HunGG6NOREtTYymk0rGtsfjGsAG4So=; b=Nj+a5AYADQYfcNhrmLx1kSqfcoofke9/UJfKQF3SpZ/VOMTE5Y8h8Xq5/+GZxpY4vEeqoQ B0Ldk0DGvHwH1X+N/lv9Igbd1spQ9/eWmBXWWzMvsYqg2aNSu4+am3zT4RzHq84/QV0d1b7a F5T2s66b3cIZ2/Ya/Vk2DFInmQ1zaFaYX3A97ZAQCOSfwHAV0isJromirrvzyjmpSC+tVMYO KJUl2GL58ohyFgF8fweoCpV2fdrh3eKNp2/ADEKiWdQagQpPWdRbJ7OrRMcZ2CrZfcgqOZb7 6NGExGV8O+a4rWDFSfWeQaRJdZydvIvrThkwhCggUPV5kV7/i2ImSIWQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 01/15] devtool: deploy: split ssh deployment into a separate function Date: Wed, 9 Sep 2026 23:53:01 +0200 Message-ID: <20260909215337.89106-2-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245509 From: Adrian Freihofer This is a refactoring preparation for adding a new local deployment method that will be used for NFS rootfs deployments. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/deploy.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/lib/devtool/deploy.py b/scripts/lib/devtool/deploy.py index 51a1944f38..ffca1269ef 100644 --- a/scripts/lib/devtool/deploy.py +++ b/scripts/lib/devtool/deploy.py @@ -353,6 +353,12 @@ def deploy_no_d(srcdir, workdir, path, strip_cmd, libdir, base_libdir, max_proce print(' %s' % item) return 0 + return _deploy_ssh(args, destdir, filelist, ftotalsize, tar_relpaths, allowed_files, + fakerootcmd, fakerootenv, path, recipe_outdir) + +def _deploy_ssh(args, destdir, filelist, ftotalsize, tar_relpaths, allowed_files, + fakerootcmd, fakerootenv, path, recipe_outdir): + """Copy files to target_dir over ssh/scp (user@hostname[:destdir]).""" extraoptions = '' if args.no_host_check: extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' @@ -440,6 +446,10 @@ def undeploy(args, config, basepath, workspace): elif not args.recipename and not args.all: raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target') + return _undeploy_ssh(args) + +def _undeploy_ssh(args): + """Run the undeploy script on the target over ssh/scp (user@hostname[:destdir]).""" extraoptions = '' if args.no_host_check: extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' From patchwork Wed Sep 9 21:53:02 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97790 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1C7B1C88E51 for ; Wed, 9 Sep 2026 21:53:53 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.78.1788990824405656244 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=SrgsV1Sd; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-202609092153416949377d8000020721-ciia4o@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 202609092153416949377d8000020721 for ; Wed, 09 Sep 2026 23:53:41 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=jCVXzLMjKJd8BHi/VGM577Uy2/hebzjy4CNHyxBgO6s=; b=SrgsV1Sd+AkbCZEAD2bFRpIK3Ehj/xDti/lu130b5TPzl2fReuP3d/kx4WzX7wK2aXAYo5 f/LQzENl0kwhiv/cQReYOvp6YLzSK3m5MRgN5k4IhtYsA5OKHaonMEEwX4briddxjrVRt+bO GgXttXLf+a13s8S/CK2/rHCqXLGvmJDzK6f7E4i9P6upzVNVKC+WbjRnCWNwQ01indBYa1nt dm68d8GaL7PZU3stMlL2/bIdm7Ru1zoobjyo/DL4dGJWg/wSeSVV8rxcurBgYWg4oXxcInNK nSSAnxL7LvTX8D7x9KsAGhhdBZf1DoskV9CIw+iSzT259v+zr5xcDLzQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 02/15] devtool: deploy: allow deploying directly into a local rootfs directory Date: Wed, 9 Sep 2026 23:53:02 +0200 Message-ID: <20260909215337.89106-3-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:53 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245503 From: Adrian Freihofer Add support for deploy-target/undeploy-target to write into a local pseudo-managed rootfs directory (e.g. one extracted by runqemu-extract-sdk for NFS booting) instead of requiring an ssh connection to a live target. If the 'target' argument is an absolute path, _deploy_local and _undeploy_local run the same tar/manifest based copy/removal as the ssh path, but locally: the file transfer is piped between two pseudo instances (one scoped to the recipe's own PSEUDO_INCLUDE_PATHS, one scoped to the target rootfs's own PSEUDO_LOCALSTATEDIR/PSEUDO_INCLUDE_PATHS) instead of over ssh/scp. exec_fakeroot_no_d()'s environment-building code is factored out into build_fakeroot_env_no_d() so _deploy_local can assemble its own pseudo environment without exec_fakeroot_no_d's implicit single-command prefixing, which doesn't compose with the dual-pseudo pipeline. This is a prerequisite for adding NFS export support to devtool ide-sdk. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/__init__.py | 22 ++-- scripts/lib/devtool/deploy.py | 210 ++++++++++++++++++++++++++++---- 2 files changed, 201 insertions(+), 31 deletions(-) diff --git a/scripts/lib/devtool/__init__.py b/scripts/lib/devtool/__init__.py index 32a7bc0098..9d227e2032 100644 --- a/scripts/lib/devtool/__init__.py +++ b/scripts/lib/devtool/__init__.py @@ -77,6 +77,18 @@ def exec_watch(cmd, **options): return buf, None +def build_fakeroot_env_no_d(fakerootenv, path, env_overrides=None): + """Build the environment for running a command under pseudo (see exec_fakeroot_no_d).""" + newenv = dict(os.environ) + newenv['PATH'] = path + for varvalue in fakerootenv.split(): + if '=' in varvalue: + splitval = varvalue.split('=', 1) + newenv[splitval[0]] = splitval[1] + if env_overrides: + newenv.update(env_overrides) + return newenv + def exec_fakeroot_no_d(fakerootcmd, fakerootenv, path, cmd, env_overrides=None, **kwargs): """Run cmd under pseudo using a recipe's own FAKEROOTCMD/FAKEROOTENV. @@ -88,15 +100,7 @@ def exec_fakeroot_no_d(fakerootcmd, fakerootenv, path, cmd, env_overrides=None, if not os.path.exists(fakerootcmd): logger.error('pseudo executable %s could not be found - have you run a build yet? pseudo-native should install this and if you have run any build then that should have been built') return 2 - # Set up the appropriate environment - newenv = dict(os.environ) - newenv['PATH'] = path - for varvalue in fakerootenv.split(): - if '=' in varvalue: - splitval = varvalue.split('=', 1) - newenv[splitval[0]] = splitval[1] - if env_overrides: - newenv.update(env_overrides) + newenv = build_fakeroot_env_no_d(fakerootenv, path, env_overrides) return subprocess.call("%s %s" % (fakerootcmd, cmd), env=newenv, **kwargs) def setup_tinfoil(config_only=False, basepath=None, tracking=False): diff --git a/scripts/lib/devtool/deploy.py b/scripts/lib/devtool/deploy.py index ffca1269ef..e3890a0113 100644 --- a/scripts/lib/devtool/deploy.py +++ b/scripts/lib/devtool/deploy.py @@ -19,7 +19,7 @@ import argparse_oe import oe.types import oe.package -from devtool import exec_fakeroot_no_d, setup_tinfoil, check_workspace_recipe, DevtoolError +from devtool import exec_fakeroot_no_d, build_fakeroot_env_no_d, setup_tinfoil, check_workspace_recipe, DevtoolError logger = logging.getLogger('devtool') @@ -257,14 +257,18 @@ def deploy(args, config, basepath, workspace): def deploy_no_d(srcdir, workdir, path, strip_cmd, libdir, base_libdir, max_process, fakerootcmd, fakerootenv, args, file_globs=None, packages_files=None): import math - try: - host, destdir = args.target.split(':') - except ValueError: - destdir = '/' + if os.path.isabs(args.target): + # A local pseudo-managed rootfs directory (e.g. NFS-exported) + destdir = os.path.realpath(args.target) else: - args.target = host - if not destdir.endswith('/'): - destdir += '/' + try: + host, destdir = args.target.split(':') + except ValueError: + destdir = '/' + else: + args.target = host + # Canonical form used throughout: no trailing slash (except root itself). + destdir = destdir.rstrip('/') or '/' recipe_outdir = srcdir if not os.path.exists(recipe_outdir) or not os.listdir(recipe_outdir): @@ -353,9 +357,107 @@ def deploy_no_d(srcdir, workdir, path, strip_cmd, libdir, base_libdir, max_proce print(' %s' % item) return 0 + if os.path.isabs(args.target): + # A local directory (e.g. an NFS-exported rootfs) rather than a + # user@host ssh target: copy the files in directly, no network needed. + return _deploy_local(args, destdir, filelist, ftotalsize, tar_relpaths, + allowed_files, fakerootcmd, fakerootenv, path, recipe_outdir) + return _deploy_ssh(args, destdir, filelist, ftotalsize, tar_relpaths, allowed_files, fakerootcmd, fakerootenv, path, recipe_outdir) +def _deploy_local(args, destdir, filelist, ftotalsize, tar_relpaths, allowed_files, + fakerootcmd, fakerootenv, path, recipe_outdir): + """Copy files directly into destdir instead of over ssh/scp. + + destdir is the local pseudo-managed rootfs directory itself (no trailing + slash), not the real filesystem root. + """ + if not os.path.isdir(destdir): + raise DevtoolError('Target directory %s does not exist' % destdir) + state_dir = destdir + '.pseudo_state' + if not os.path.isdir(state_dir): + raise DevtoolError( + '%s does not exist - %s does not look like a pseudo-managed rootfs ' + '(e.g. one extracted by runqemu-extract-sdk).' % (state_dir, destdir)) + + if not args.no_check_space: + freespace = shutil.disk_usage(destdir).free // 1024 + if ftotalsize > freespace: + raise DevtoolError('Deploy failed - insufficient space on target ' + '(available %d, needed %d)' % (freespace, ftotalsize)) + + shellscript = _prepare_remote_script(deploy=True, + destdir=destdir, + verbose=args.show_status, + nopreserve=args.no_preserve, + nocheckspace=True) + + tmpdir = tempfile.mkdtemp(prefix='devtool') + tar_send_filelist_path = None + try: + script_path = os.path.join(tmpdir, 'devtool_deploy.sh') + with open(script_path, 'w') as f: + f.write(shellscript) + filelist_path = os.path.join(tmpdir, 'devtool_deploy.list') + with open(filelist_path, 'w') as f: + f.write('%d\n' % ftotalsize) + for fpath, fsize in filelist: + f.write('%s %d\n' % (fpath, fsize)) + + # tar_send_* builds up the sending side of the pipe: the plain tar + # invocation, then wrapped to capture its own exit status (only the + # last stage of a shell pipeline is visible to subprocess), then + # wrapped again to run under its own pseudo instance. + if allowed_files is not None: + tar_send_fd, tar_send_filelist_path = tempfile.mkstemp(prefix='devtool-deploy-filelist-') + with os.fdopen(tar_send_fd, 'w') as f: + for relpath in tar_relpaths: + f.write('./' + relpath + '\n') + tar_send_argv = 'tar cf - -T %s' % shlex.quote(tar_send_filelist_path) + else: + tar_send_argv = 'tar cf - .' + + tar_send_status_path = os.path.join(tmpdir, 'devtool_deploy.tar_status') + tar_send_script = 'sh -c %s' % shlex.quote( + '%s; echo $? > %s' % (tar_send_argv, shlex.quote(tar_send_status_path))) + tar_send_cmd = 'PSEUDO_INCLUDE_PATHS=%s %s %s' % ( + shlex.quote(recipe_outdir), shlex.quote(fakerootcmd), tar_send_script) + + # tar_receive_cmd is the other side of the pipe: extracts into destdir + # under the target rootfs's own pseudo database (state_dir/destdir, + # not the recipe's). + # $2 needs a trailing slash: the script's manifest substitution + # (sed 's!^./!$2!') turns tar's './relative' entries into absolute paths. + tar_receive_cmd = 'PSEUDO_LOCALSTATEDIR=%s PSEUDO_INCLUDE_PATHS=%s %s sh %s %s %s %s' % ( + shlex.quote(state_dir), shlex.quote(destdir), shlex.quote(fakerootcmd), + shlex.quote(script_path), shlex.quote(args.recipename), + shlex.quote(destdir.rstrip('/') + '/'), shlex.quote(filelist_path)) + + if not os.path.exists(fakerootcmd): + logger.error('pseudo executable %s could not be found - have you run a build ' + 'yet? pseudo-native should install this and if you have run any ' + 'build then that should have been built' % fakerootcmd) + ret = 2 + else: + shell_env = build_fakeroot_env_no_d(fakerootenv, path) + ret = subprocess.call('%s | %s' % (tar_send_cmd, tar_receive_cmd), env=shell_env, + cwd=recipe_outdir, shell=True) + if ret == 0 and os.path.exists(tar_send_status_path): + with open(tar_send_status_path) as f: + ret = int(f.read().strip() or 0) + finally: + if tar_send_filelist_path: + os.remove(tar_send_filelist_path) + shutil.rmtree(tmpdir) + + if ret != 0: + raise DevtoolError('Deploy failed - rerun with -s to get a complete ' + 'error message') + + logger.info('Successfully deployed %s to %s' % (recipe_outdir, destdir)) + return 0 + def _deploy_ssh(args, destdir, filelist, ftotalsize, tar_relpaths, allowed_files, fakerootcmd, fakerootenv, path, recipe_outdir): """Copy files to target_dir over ssh/scp (user@hostname[:destdir]).""" @@ -423,9 +525,11 @@ def _deploy_ssh(args, destdir, filelist, ftotalsize, tar_relpaths, allowed_files tar_cmd = 'tar cf - -T %s' % shlex.quote(tar_filelist_path) else: tar_cmd = 'tar cf - .' + # $2 needs a trailing slash: the script's manifest substitution + # (sed 's!^./!$2!') turns tar's './relative' entries into absolute paths. remote_cmd = '%s | %s %s %s %s \'sh %s %s %s %s\'' % ( tar_cmd, ssh_sshexec, ssh_port, extraoptions, args.target, - tmpscript, args.recipename, destdir, tmpfilelist) + tmpscript, args.recipename, destdir.rstrip('/') + '/', tmpfilelist) ret = exec_fakeroot_no_d(fakerootcmd, fakerootenv, path, remote_cmd, cwd=recipe_outdir, env_overrides={'PSEUDO_INCLUDE_PATHS': recipe_outdir}, shell=True) finally: @@ -446,8 +550,64 @@ def undeploy(args, config, basepath, workspace): elif not args.recipename and not args.all: raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target') + if os.path.isabs(args.target): + # A local directory (e.g. an NFS-exported rootfs) rather than a + # user@host ssh target: remove the files in directly, no network needed. + tinfoil = setup_tinfoil(config_only=True, basepath=basepath) + try: + fakerootcmd = tinfoil.config_data.getVar('FAKEROOTCMD') + fakerootenv = tinfoil.config_data.getVar('FAKEROOTENV') + path = tinfoil.config_data.getVar('PATH') + finally: + tinfoil.shutdown() + return _undeploy_local(args, os.path.realpath(args.target), fakerootcmd, fakerootenv, path) + return _undeploy_ssh(args) +def _undeploy_local(args, target_dir, fakerootcmd, fakerootenv, path): + """Remove files directly from target_dir instead of over ssh.""" + if not os.path.isdir(target_dir): + raise DevtoolError('Target directory %s does not exist' % target_dir) + state_dir = target_dir + '.pseudo_state' + if not os.path.isdir(state_dir): + raise DevtoolError( + '%s does not exist - %s does not look like a pseudo-managed rootfs ' + '(e.g. one extracted by runqemu-extract-sdk).' % (state_dir, target_dir)) + + # deploy=False here: the generated script never touches $2, so target_dir + # doesn't need the trailing slash _with_trailing_slash() adds for deploy. + shellscript = _prepare_remote_script(deploy=False, destdir=target_dir, dryrun=args.dry_run, undeployall=args.all) + + tmpdir = tempfile.mkdtemp(prefix='devtool') + try: + script_path = os.path.join(tmpdir, 'devtool_undeploy.sh') + with open(script_path, 'w') as f: + f.write(shellscript) + + environment = dict(os.environ) + environment['PATH'] = path + for varvalue in (fakerootenv or '').split(): + if '=' in varvalue: + key, value = varvalue.split('=', 1) + environment[key] = value + # Use target_dir's own pseudo database, not the ambient one from FAKEROOTENV, + # so file removals stay consistent with what was recorded on deploy/extract. + environment['PSEUDO_LOCALSTATEDIR'] = state_dir + environment['PSEUDO_INCLUDE_PATHS'] = target_dir + command = [fakerootcmd, 'sh', script_path, args.recipename or ''] + ret = subprocess.call(command, env=environment) + finally: + shutil.rmtree(tmpdir) + + if ret != 0: + # Unlike the ssh case there is nothing -s could add here, the script + # runs locally and its output is already on the console. + raise DevtoolError('Undeploy failed - see the output above for details') + + if not args.all and not args.dry_run: + logger.info('Successfully undeployed %s' % args.recipename) + return 0 + def _undeploy_ssh(args): """Run the undeploy script on the target over ssh/scp (user@hostname[:destdir]).""" extraoptions = '' @@ -473,8 +633,6 @@ def _undeploy_ssh(args): destdir = '/' else: args.target = host - if not destdir.endswith('/'): - destdir += '/' tmpdir = tempfile.mkdtemp(prefix='devtool') try: @@ -508,17 +666,20 @@ def register_commands(subparsers, context): parser_deploy = subparsers.add_parser('deploy-target', help='Deploy recipe output files to live target machine', description='Deploys a recipe\'s build output (i.e. the output of ' - 'the do_install task) to a live target machine over ssh. ' - 'By default, any existing files will be preserved instead ' - 'of being overwritten and will be restored if you run ' - 'devtool undeploy-target. Note: this only deploys the ' - 'recipe itself and not any runtime dependencies, so it is ' - 'assumed that those have been installed on the target ' - 'beforehand. Use --package/--file-glob to deploy only a ' - 'subset of the recipe\'s installed files.', + 'the do_install task) to a live target machine over ssh, ' + 'or directly into a local pseudo-managed rootfs directory ' + '(e.g. one extracted for NFS booting). Existing files are ' + 'preserved by default and restored by devtool ' + 'undeploy-target. Only the recipe itself is deployed, not ' + 'its runtime dependencies. Use --package/--file-glob to ' + 'deploy only a subset of the recipe\'s installed files.', group='testbuild') parser_deploy.add_argument('recipename', help='Recipe to deploy') - parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]') + parser_deploy.add_argument('target', + help='Either a live target machine running an ssh server: ' + 'user@hostname[:destdir]; or an absolute path to a local ' + 'pseudo-managed rootfs directory (e.g. one extracted by ' + 'runqemu-extract-sdk) to copy the files into directly, without ssh.') parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true') parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true') parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true') @@ -552,10 +713,15 @@ def register_commands(subparsers, context): parser_undeploy = subparsers.add_parser('undeploy-target', help='Undeploy recipe output files in live target machine', - description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.', + description='Un-deploys recipe output files previously deployed to a live target machine or local ' + 'pseudo-managed rootfs directory by devtool deploy-target.', group='testbuild') parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?') - parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname') + parser_undeploy.add_argument('target', + help='Either a live target machine running an ssh server: ' + 'user@hostname; or an absolute path to the local pseudo-managed ' + 'rootfs directory previously used with deploy-target, to remove ' + 'the files directly, without ssh.') parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true') parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true') parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true') From patchwork Wed Sep 9 21:53:03 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97781 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 684E0C88E48 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.77.1788990824405615363 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=DEm1404M; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-2026090921534175da5e0a8700020714-znjxbf@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 2026090921534175da5e0a8700020714 for ; Wed, 09 Sep 2026 23:53:41 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=Ba97bilShWFgM64DS1I2GA+j79afIgNFQiHxCpqSTCk=; b=DEm1404MSqa2YRC8EQeNv6/0ojlG70v4buA6h9PA8x+sPc6Lpw5SVq2nqzUEov4Uro2DGu ztnvP/W5HuG7QELkqPrwPDI7vMCURtIXZAcBfdjtoVym8BixTw4TLB3h3xAVOsi4Ov9H/VX8 3FJtNSEjtCweLQMJ1Ck1A7orTxEHp8VE9+OUFe8eoCGOKofG5h6Jshjy2fMUFxIuiNCNez56 G0YsIk3D/Dp5Pb1FCp4nhRUt9ClTeZb7rt1AUtB4N+rZu0qPjuazsHzjth11aUXbQOrnGwaH eV/y1dlK/qaf1ZceLSGTn3ZQeGgi0pC0bqS77Zs0ExUV5gyuMw2rGW5g==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 03/15] oe-selftest: devtool deploy-target: test deploying into a local rootfs path Date: Wed, 9 Sep 2026 23:53:03 +0200 Message-ID: <20260909215337.89106-4-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245506 From: Adrian Freihofer Extract an image's rootfs tarball with runqemu-extract-sdk, boot it via NFS with runqemu, then deploy a devtool-modified recipe directly into that directory with plain 'devtool deploy-target ' (no ssh) while the target has it mounted live, and confirm the target immediately sees the change. Deploying a second time covers replacing an existing deployment, a --file-glob deploy covers the filtered variant which packs an explicit file list, and 'devtool undeploy-target ' covers removing the files again, all while the rootfs stays mounted. Also deploy/undeploy the same recipe over ssh into the very same live directory and compare the resulting file ownership/permissions (captured via the same pseudo find/ls trick test_devtool_deploy_target uses) against the local-path deploy's result, to confirm the two independent code paths (_deploy_local's pseudo pipe vs _deploy_ssh's tar-over-ssh) produce identical output. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 161 ++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index cf7b97fdda..8bfdd15552 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -2171,6 +2171,167 @@ class DevtoolDeployTargetTests(DevtoolBase): extra_args = ' '.join(a for a in (strip_opt, filter_args) if a) _deploy_and_check(extra_args, check_full_filelist=not filter_args, expected_files=expected_files) + @OETestTag("runqemu") + def test_devtool_deploy_target_path(self): + """Verify 'devtool deploy-target/undeploy-target ' deploy/remove straight into + a local pseudo-managed rootfs directory (no ssh), and that a target + booting that same directory via NFS immediately sees the change. + """ + self._check_runqemu_prerequisites() + self.assertTrue(not os.path.exists(self.workspacedir), 'This test cannot be run with a workspace directory under the build directory') + testrecipe = 'mdadm' + testfile = '/sbin/mdadm' + # mdmon is installed by the same do_install, used to check --file-glob filtering + otherfile = '/sbin/mdmon' + if "usrmerge" in get_bb_var('DISTRO_FEATURES'): + testfile = '/usr/sbin/mdadm' + otherfile = '/usr/sbin/mdmon' + testcommand = '/sbin/mdadm --help' + testimage = 'oe-selftest-image' + # Use the mdadm-doc package to check --package filtering excludes it + mandir = get_bb_var('mandir', testrecipe) + docfile = os.path.join(mandir, 'man8', '%s.8' % testrecipe) + + # A tar rootfs is needed both to extract a local copy of it (below) + # and for runqemu to NFS-boot straight from that extracted directory. + self.append_config('IMAGE_FSTYPES:append = " tar"\n') + bitbake("%s qemu-native qemu-helper-native" % testimage) + bb_vars = get_bb_vars(['DEPLOY_DIR_IMAGE', 'IMAGE_LINK_NAME'], testimage) + deploy_dir_image = bb_vars['DEPLOY_DIR_IMAGE'] + image_link_name = bb_vars['IMAGE_LINK_NAME'] + self.add_command_to_tearDown('bitbake -c clean %s' % testimage) + self.add_command_to_tearDown('rm -f %s/%s*' % (deploy_dir_image, testimage)) + + tempdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(tempdir) + self.track_for_cleanup(self.workspacedir) + self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe) + self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') + runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) + runCmd('devtool build %s' % testrecipe) + + # Extract a local pseudo-managed rootfs the same way + rootfs_tarball = os.path.join(deploy_dir_image, image_link_name + '.tar') + self.assertExists(rootfs_tarball) + extractdir = tempfile.mkdtemp(prefix='devtoolqa') + self.track_for_cleanup(extractdir) + nfs_rootfs = os.path.join(extractdir, 'rootfs') + runCmd('runqemu-extract-sdk %s %s' % (rootfs_tarball, nfs_rootfs)) + self.assertExists(nfs_rootfs) + self.assertExists(nfs_rootfs + '.pseudo_state') + + # oe-selftest-image does not install mdadm by default, so the target must not see it yet. + self.assertNotExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + + qemuboot = os.path.join(deploy_dir_image, image_link_name + '.qemuboot.conf') + self.assertExists(qemuboot) + launch_cmd = 'runqemu %s %s nographic' % (shlex.quote(qemuboot), shlex.quote(nfs_rootfs)) + with runqemu(testimage, launch_cmd=launch_cmd) as qemu: + status, output = qemu.run("awk '$2 == \"/\" {print $3}' /proc/mounts") + self.assertEqual(status, 0) + self.assertEqual(output.strip(), 'nfs') + + status, _ = qemu.run(testcommand) + self.assertNotEqual(status, 0, '%s should not be deployed yet' % testfile) + + # Deploy directly into the local rootfs path (no ssh) while the target has it NFS-mounted live + deploy_cmd = 'devtool deploy-target %s %s' % (testrecipe, nfs_rootfs) + if self.logger.isEnabledFor(logging.DEBUG): + deploy_cmd += ' -s' + result = runCmd(deploy_cmd) + self.assertEqual(result.status, 0) + self.assertExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + self.assertExists(os.path.join(nfs_rootfs, otherfile.lstrip('/'))) + + status, _ = qemu.run(testcommand) + self.assertEqual(status, 0, '%s was not deployed' % testfile) + + # Deploying again while the target still has this directory NFS-mounted live must still succeed. + result = runCmd(deploy_cmd) + self.assertEqual(result.status, 0) + + # Undeploy directly from the local rootfs path (no ssh) while the target still has it NFS-mounted live + undeploy_cmd = 'devtool undeploy-target %s %s' % (testrecipe, nfs_rootfs) + if self.logger.isEnabledFor(logging.DEBUG): + undeploy_cmd += ' -s' + result = runCmd(undeploy_cmd) + self.assertEqual(result.status, 0) + self.assertNotExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + + status, _ = qemu.run(testcommand) + self.assertNotEqual(status, 0, 'undeploy-target did not remove %s as it should have' % testfile) + + # Confirm the local-path deploy (dual-pseudo pipe) and the ssh deploy + # (tar over ssh) land identical files with identical ownership/perms + # into the very same live NFS-exported directory, for both an + # unfiltered deploy and a --package filtered one. + bb_vars = get_bb_vars(['FAKEROOTENV', 'FAKEROOTCMD', 'PATH'], testrecipe) + fakerootenv = bb_vars['FAKEROOTENV'] + fakerootcmd = bb_vars['FAKEROOTCMD'] + path = bb_vars['PATH'] + state_dir = nfs_rootfs + '.pseudo_state' + + def _get_rootfs_filelist(): + # nfs_rootfs has its own pseudo database, distinct from the one + # FAKEROOTENV points at, so override it for this inspection. + cmd = 'PATH="%s" %s PSEUDO_LOCALSTATEDIR=%s PSEUDO_INCLUDE_PATHS=%s %s find . -type f -exec ls -l {} \\;' % ( + path, fakerootenv, shlex.quote(state_dir), shlex.quote(nfs_rootfs), fakerootcmd) + result = runCmd(cmd, cwd=nfs_rootfs) + filelist = self._process_ls_output(result.output) + filelist.sort(key=lambda item: item.split()[-1]) + return filelist + + def _compare_local_and_ssh_deploy(extra_args, expected_files): + """Deploy extra_args once locally and once over ssh into the same + live directory, and assert both leave identical files with + identical ownership/permissions (expected_files checks each path + was/wasn't deployed, on both sides).""" + local_cmd = ('%s %s' % (deploy_cmd, extra_args)).strip() + result = runCmd(local_cmd) + self.assertEqual(result.status, 0) + for filepath, expected in expected_files.items(): + exists = os.path.exists(os.path.join(nfs_rootfs, filepath.lstrip('/'))) + self.assertEqual(exists, expected, '%s: %s exists=%s after local deploy' % (extra_args, filepath, exists)) + local_filelist = _get_rootfs_filelist() + + result = runCmd(undeploy_cmd) + self.assertEqual(result.status, 0) + self.assertNotExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + + ssh_cmd = ('devtool deploy-target -c %s root@%s %s' % (testrecipe, qemu.ip, extra_args)).strip() + result = runCmd(ssh_cmd) + self.assertEqual(result.status, 0) + for filepath, expected in expected_files.items(): + exists = os.path.exists(os.path.join(nfs_rootfs, filepath.lstrip('/'))) + self.assertEqual(exists, expected, '%s: %s exists=%s after ssh deploy' % (extra_args, filepath, exists)) + ssh_filelist = _get_rootfs_filelist() + + self.assertEqual(local_filelist, ssh_filelist, + '%s: local-path deploy and ssh deploy produced different file ownership/permissions' % extra_args) + + result = runCmd('devtool undeploy-target -c %s root@%s' % (testrecipe, qemu.ip)) + self.assertEqual(result.status, 0) + self.assertNotExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + + _compare_local_and_ssh_deploy('', {testfile: True, otherfile: True, docfile: True}) + _compare_local_and_ssh_deploy('--package %s' % testrecipe, {testfile: True, otherfile: True, docfile: False}) + + # A --package/--file-glob filtered deploy hands tar an explicit file + # list instead of packing the whole tree, so cover that path as well. + filtered_deploy_cmd = deploy_cmd + ' --file-glob %s' % testfile + result = runCmd(filtered_deploy_cmd) + self.assertEqual(result.status, 0) + self.assertExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + self.assertNotExists(os.path.join(nfs_rootfs, otherfile.lstrip('/'))) + + status, _ = qemu.run(testcommand) + self.assertEqual(status, 0, '%s was not deployed' % testfile) + + result = runCmd(undeploy_cmd) + self.assertEqual(result.status, 0) + self.assertNotExists(os.path.join(nfs_rootfs, testfile.lstrip('/'))) + + class DevtoolBuildImageTests(DevtoolBase): def test_devtool_build_image(self): From patchwork Wed Sep 9 21:53:04 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97783 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 40CCCC88E46 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-228.siemens.flowmailer.net (mta-65-228.siemens.flowmailer.net [185.136.65.228]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.79.1788990824462444181 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=QjgiDtsu; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.228, mailfrom: fm-1329275-202609092153427eb73e0b2d00020794-rsroi2@rts-flowmailer.siemens.com) Received: by mta-65-228.siemens.flowmailer.net with ESMTPSA id 202609092153427eb73e0b2d00020794 for ; Wed, 09 Sep 2026 23:53:42 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=WJ5LeN9mGHrprBWr2oUhdqLWd+UKKBc7g9uZhOMMg1M=; b=QjgiDtsuGuwYoHK3A2ulBF8G42yFD93pnv2g6xe0+kH0lHdBRD+Tc2c2mM9uWA7QmnSev7 EB5QioTBO3MVmY4YYEmLPHPq6WfHoidZDkJQQXOXtRVEPaprvE2vDjkF8QFC4k/0I9aS5MzS /AhzjyF/rRN93cQDufxqj7CEWAGlKy3e0MH8QB9OINFhNao1eRNHbq3kEakatawi6NgVN1ok BpO4Dhav7Tx5MXmRz0HP2puDp/KuDSQIqp5pUBd+YWLzNZm8GFfPveCj/oN9H/NJ+yBwCr5l W1GtvADiPGWFM8PUbSjrJ5GgB/FQpsNYfrGTh3FTbTzSvBIwVm2dTveQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 04/15] devtool: ide-sdk: support NFS rootfs Date: Wed, 9 Sep 2026 23:53:04 +0200 Message-ID: <20260909215337.89106-5-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245505 From: Adrian Freihofer Add a new command line option --nfs to devtool ide-sdk. This allows running the full ide-sdk workflow against a target device booted with an NFS rootfs, instead of deploying over ssh. devtool ide-sdk extracts the image's tar archive, a bootable rootfs, below /nfs-exports// (optionally relocatable via --nfs-extract-dir), and generates a small helper script next to it that starts QEMU booting from that rootfs, which in turn serves it via NFS using runqemu-export-rootfs. Deploying the modified recipe writes directly into this directory instead of ssh, using devtool deploy-target's local rootfs support. --nfs=rootfs boots the plain rootfs, same as a non-NFS ssh setup, so debugging still relies on the locally built rootfs-dbg/D debug symbols. --nfs=rootfs-dbg instead extracts and boots the combined debug rootfs. The generated debugging configurations still read those debug symbols on the host side, but from the extracted directory, not as usual from the image's WORKDIR. Since the target also boots that same directory over NFS, the symbols are available target-side too, e.g. for perf or gcov. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/__init__.py | 19 +- scripts/lib/devtool/ide_plugins/ide_code.py | 29 +-- scripts/lib/devtool/ide_plugins/ide_none.py | 2 +- scripts/lib/devtool/ide_sdk.py | 217 +++++++++++++++++++- 4 files changed, 240 insertions(+), 27 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index 91063c8d12..bb9f100652 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -411,11 +411,22 @@ class IdeBase: raise err -def get_devtool_deploy_opts(args): +def resolve_deploy_target(args, target_override=None): + """Resolve the effective devtool deploy-target destination. + + target_override, e.g. the local NFS rootfs directory when --nfs was used + (see devtool.deploy for how a directory target is handled without ssh), + always takes precedence over the ssh target from -t/--target. + """ + target = target_override or args.target + if not target: + raise DevtoolError('No deploy target, pass -t/--target or --nfs') + return target + + +def get_devtool_deploy_opts(args, target_override=None): """Filter args for devtool deploy-target args""" - if not args.target: - return None - devtool_deploy_opts = [args.target] + devtool_deploy_opts = [resolve_deploy_target(args, target_override)] if args.no_host_check: devtool_deploy_opts += ["-c"] if args.show_status: diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 6effbac654..7c700f258e 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -9,7 +9,7 @@ import json import logging import os import shutil -from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, DebuggerServerModes, LldbServerConfig, get_devtool_deploy_opts +from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, DebuggerServerModes, LldbServerConfig, get_devtool_deploy_opts, resolve_deploy_target logger = logging.getLogger('devtool') @@ -594,10 +594,10 @@ class IdeVSCode(IdeBase): IdeBase.update_json_file( self.dot_code_dir(modified_recipe), launch_file, launch_dict) - def vscode_tasks_cpp(self, args, modified_recipe): - run_install_deploy = modified_recipe.gen_install_deploy_script(args) + def vscode_tasks_cpp(self, args, image_recipe, modified_recipe): + run_install_deploy = modified_recipe.gen_install_deploy_script(args, image_recipe.nfs_deploy_dir) install_task_name = "install && deploy-target %s" % modified_recipe.recipe_id_pretty - deploy_args = ["--target", args.target] + deploy_args = ["--target", resolve_deploy_target(args, image_recipe.nfs_deploy_dir)] if args.port: deploy_args += ["--port", args.port] for package in args.package or []: @@ -694,7 +694,7 @@ class IdeVSCode(IdeBase): args += [target_device.target, remote_cmd] return args - def vscode_tasks_kernel_module(self, args, modified_recipe): + def vscode_tasks_kernel_module(self, args, image_recipe, modified_recipe): """Generate tasks.json for kernel module recipes. Three tasks are generated and chained in sequence: @@ -717,8 +717,9 @@ class IdeVSCode(IdeBase): install_task_name = "install && deploy-target %s" % modified_recipe.recipe_id_pretty reload_task_name = "reload module %s" % modified_recipe.recipe_id_pretty verify_task_name = "verify module %s" % modified_recipe.recipe_id_pretty - run_install_deploy = modified_recipe.gen_install_deploy_script(args) - deploy_args = ["--target", args.target] + run_install_deploy = modified_recipe.gen_install_deploy_script(args, image_recipe.nfs_deploy_dir) + # Redundant with the default baked into the script, but keeps the task self-contained. + deploy_args = ["--target", resolve_deploy_target(args, image_recipe.nfs_deploy_dir)] if args.port: deploy_args += ["--port", args.port] for package in args.package or []: @@ -760,7 +761,7 @@ class IdeVSCode(IdeBase): IdeBase.update_json_file( self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) - def vscode_tasks_fallback(self, args, modified_recipe): + def vscode_tasks_fallback(self, args, image_recipe, modified_recipe): oe_init_dir = modified_recipe.oe_init_dir oe_init = ". %s %s > /dev/null && " % (modified_recipe.oe_init_build_env, modified_recipe.topdir) dt_build = "devtool build " @@ -773,7 +774,7 @@ class IdeVSCode(IdeBase): dt_deploy_label = dt_deploy + modified_recipe.recipe_id_pretty dt_deploy_cmd = dt_deploy + modified_recipe.bpn dt_build_deploy_label = "devtool build & deploy-target %s" % modified_recipe.recipe_id_pretty - deploy_opts = ' '.join(get_devtool_deploy_opts(args)) + deploy_opts = ' '.join(get_devtool_deploy_opts(args, image_recipe.nfs_deploy_dir)) tasks_dict = { "version": "2.0.0", "tasks": [ @@ -893,13 +894,13 @@ class IdeVSCode(IdeBase): IdeBase.update_json_file( self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) - def vscode_tasks(self, args, modified_recipe): + def vscode_tasks(self, args, image_recipe, modified_recipe): if modified_recipe.build_tool.is_c_cpp: - self.vscode_tasks_cpp(args, modified_recipe) + self.vscode_tasks_cpp(args, image_recipe, modified_recipe) elif modified_recipe.build_tool == BuildTool.KERNEL_MODULE: - self.vscode_tasks_kernel_module(args, modified_recipe) + self.vscode_tasks_kernel_module(args, image_recipe, modified_recipe) else: - self.vscode_tasks_fallback(args, modified_recipe) + self.vscode_tasks_fallback(args, image_recipe, modified_recipe) def setup_modified_recipe(self, args, image_recipe, modified_recipe): self.vscode_settings(modified_recipe, image_recipe) @@ -913,7 +914,7 @@ class IdeVSCode(IdeBase): self.initialize_cross_debug_configs( image_recipe, modified_recipe, GdbCrossConfigVSCode) self.vscode_launch(args, modified_recipe) - self.vscode_tasks(args, modified_recipe) + self.vscode_tasks(args, image_recipe, modified_recipe) def register_ide_plugin(ide_plugins): diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py index e4b255f2fa..559940fda2 100644 --- a/scripts/lib/devtool/ide_plugins/ide_none.py +++ b/scripts/lib/devtool/ide_plugins/ide_none.py @@ -312,7 +312,7 @@ class IdeNone(IdeBase): - Generate a gdbinit file per executable - Generate the oe-scripts sym-link """ - script_path = modified_recipe.gen_install_deploy_script(args) + script_path = modified_recipe.gen_install_deploy_script(args, image_recipe.nfs_deploy_dir) logger.info("Created: %s" % script_path) if modified_recipe.toolchain == 'clang': diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 0b76aa448b..2e37f20894 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -18,6 +18,7 @@ import shlex import glob from argparse import RawTextHelpFormatter from enum import Enum +from pathlib import Path import scriptutils import bb @@ -212,15 +213,24 @@ class RecipeImage: def __init__(self, name, orig_bbappend_content=None): self.name = name - self.rootfs = None + self.pn = None + self.__rootfs = None self.__rootfs_dbg = None + self.__nfs_rootfs = None + self.__nfs_rootfs_dbg = None + self.nfs_deploy_dir = None + self.deploy_dir_image = None + self.image_link_name = None self.qb_slirp_opt = '' + self.fakerootcmd = None + self.fakerootenv = None self.bootstrap_tasks = [self.name + ':do_build'] # Debug settings already provided by the base configuration (e.g. # local.conf, MACHINE, DISTRO, the recipe itself) plus any bbappend # content other than devtool ide-sdk's own sections (see # strip_bbappend_sections()). Populated by initialize(). self.base_image_gen_debugfs = False + self.base_image_fstypes = set() self.base_image_fstypes_debugfs = '' self.base_has_combined_dbg = False self.base_image_install = set() @@ -271,8 +281,11 @@ class RecipeImage: raise DevtoolError( "Parsing image recipe %s failed" % self.name) + self.pn = image_d.getVar('PN') self.base_image_gen_debugfs = image_d.getVar( 'IMAGE_GEN_DEBUGFS') == '1' + self.base_image_fstypes = set( + (image_d.getVar('IMAGE_FSTYPES') or '').split()) self.base_image_fstypes_debugfs = image_d.getVar( 'IMAGE_FSTYPES_DEBUGFS') or '' self.base_has_combined_dbg = bb.data.inherits_class( @@ -281,22 +294,72 @@ class RecipeImage: (image_d.getVar('IMAGE_INSTALL') or '').split()) workdir = image_d.getVar('WORKDIR') - self.rootfs = os.path.join(workdir, 'rootfs') + self.__rootfs = os.path.join(workdir, 'rootfs') self.__rootfs_dbg = os.path.join(workdir, 'rootfs-dbg') + self.deploy_dir_image = image_d.getVar('DEPLOY_DIR_IMAGE') + self.image_link_name = image_d.getVar('IMAGE_LINK_NAME') self.qb_slirp_opt = image_d.getVar('QB_SLIRP_OPT') or '' + self.fakerootcmd = image_d.getVar('FAKEROOTCMD') + self.fakerootenv = image_d.getVar('FAKEROOTENV') @property def debug_support(self): return bool(self.rootfs_dbg) + @property + def rootfs(self): + """Prefer the live NFS-exported rootfs (if --nfs=rootfs is used) over the + static WORKDIR/rootfs left over from the image build, so solib_search_path() + finds files as devtool deploy-target actually updates them.""" + if self.__nfs_rootfs: + return self.__nfs_rootfs + return self.__rootfs + @property def rootfs_dbg(self): + if self.__nfs_rootfs_dbg: + return self.__nfs_rootfs_dbg if self.__rootfs_dbg and os.path.isdir(self.__rootfs_dbg): return self.__rootfs_dbg return None - def update_image_bbappend(self, recipes_modified): + def set_nfs_rootfs(self, nfs_export_base_dir, nfs): + """Select the NFS rootfs for generated debugger paths and deploys.""" + if not nfs: + return + self.nfs_deploy_dir = self.nfs_rootfs_dir(nfs_export_base_dir, nfs) + if nfs == 'rootfs-dbg': + self.__nfs_rootfs_dbg = self.nfs_deploy_dir + elif nfs == 'rootfs': + self.__nfs_rootfs = self.nfs_deploy_dir + + def nfs_rootfs_dir(self, nfs_export_base_dir, nfs): + """Return the directory for the selected NFS rootfs, below nfs_export_base_dir.""" + return os.path.join(nfs_export_base_dir, self.pn, nfs) + + def nfs_runqemu_helper(self, nfs_export_base_dir, nfs): + """Create a helper that boots the selected rootfs through runqemu.""" + export_dir = os.path.join(nfs_export_base_dir, self.pn) + rootfs_dir = self.nfs_rootfs_dir(nfs_export_base_dir, nfs) + qemuboot = os.path.join( + self.deploy_dir_image, self.image_link_name + '.qemuboot.conf') + if not os.path.exists(qemuboot): + logger.info( + 'No qemuboot configuration was generated for %s; ' + 'not creating a runqemu helper.', self.name) + return None + + helper = os.path.join(export_dir, 'runqemu-' + nfs) + with open(helper, 'w') as helper_file: + helper_file.write('#!/bin/sh\n') + helper_file.write( + 'exec runqemu %s %s "$@"\n' % ( + shlex.quote(qemuboot), shlex.quote(rootfs_dir))) + os.chmod(helper, os.stat(helper).st_mode | stat.S_IEXEC) + return helper + + def update_image_bbappend(self, recipes_modified, nfs=None): """Write debug settings for modified-mode recipes into the image bbappend. Writes IMAGE_GEN_DEBUGFS, IMAGE_FSTYPES_DEBUGFS, IMAGE_CLASSES for @@ -323,7 +386,17 @@ class RecipeImage: lines = [] if not self.base_image_gen_debugfs: lines.append('IMAGE_GEN_DEBUGFS = "1"') - if self.base_image_fstypes_debugfs != '': + if nfs == 'rootfs': + if 'tar' not in self.base_image_fstypes: + lines.append('IMAGE_FSTYPES:append = " tar"') + elif nfs == 'rootfs-dbg': + if self.base_image_fstypes_debugfs: + if 'tar' not in self.base_image_fstypes_debugfs.split(): + lines.append('IMAGE_FSTYPES_DEBUGFS:append = " tar"') + else: + lines.append('IMAGE_FSTYPES_DEBUGFS = "tar"') + elif self.base_image_fstypes_debugfs != '': + # Without --nfs no debug filesystem image is needed at all. lines.append('IMAGE_FSTYPES_DEBUGFS = ""') if not self.base_has_combined_dbg: lines.append('IMAGE_CLASSES += "image-combined-dbg"') @@ -368,6 +441,98 @@ class RecipeImage: slirp_changed = self.update_qb_slirp_opt() return image_changed or slirp_changed + @staticmethod + def _tar_options(rootfs_tarball): + tar_extract_options = { + '.tar.xz': '-xJf', + '.tar.bz2': '-xjf', + '.tar.gz': '-xzf', + '.tar.zst': '--zstd -xf', + '.tar': '-xf', + } + for extension, option in tar_extract_options.items(): + if rootfs_tarball.endswith(extension): + return ['--numeric-owner', *option.split()] + raise DevtoolError( + 'Unable to determine sdk tarball format\n' + 'Accepted types: .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst') + + @staticmethod + def pseudo_state_dir(rootfs_dir): + """Return the pseudo database location associated with an extracted rootfs.""" + return os.path.realpath(rootfs_dir) + '.pseudo_state' + + def extract_sdk_rootfs(self, rootfs_tarball, rootfs_dir): + """Extract a rootfs tarball under pseudo and return its absolute directory.""" + if not os.path.exists(rootfs_tarball): + raise DevtoolError("sdk tarball '%s' does not exist" % rootfs_tarball) + if not os.path.exists(self.fakerootcmd): + raise DevtoolError('%s does not exist' % self.fakerootcmd) + + rootfs_tarball = os.path.realpath(rootfs_tarball) + rootfs_dir = os.path.realpath(rootfs_dir) + tar_options = self._tar_options(rootfs_tarball) + state_dir = self.pseudo_state_dir(rootfs_dir) + + os.makedirs(rootfs_dir, exist_ok=True) + os.makedirs(state_dir, exist_ok=True) + Path(state_dir, 'pseudo.pid').touch() + + environment = dict(os.environ) + for varvalue in (self.fakerootenv or '').split(): + if '=' in varvalue: + key, value = varvalue.split('=', 1) + environment[key] = value + command = [self.fakerootcmd, 'tar', '-C', rootfs_dir] + + environment['PSEUDO_LOCALSTATEDIR'] = state_dir + environment['PSEUDO_INCLUDE_PATHS'] = rootfs_dir + command.extend(tar_options) + command.append(rootfs_tarball) + logger.info('Extracting rootfs tarball using pseudo: %s', ' '.join(command)) + try: + subprocess.run(command, env=environment, check=True) + except subprocess.CalledProcessError as exc: + raise DevtoolError('Failed to extract %s' % rootfs_tarball) from exc + + if len(os.listdir(rootfs_dir)) < 4: + logger.warning( + "Only few files in %s, please double-check the extraction " + "worked as intended", rootfs_dir) + return rootfs_dir + + def extract_nfs_rootfs(self, nfs_export_base_dir, nfs, target): + """Refresh the selected rootfs below nfs_export_base_dir.""" + if not self.image_link_name: + raise DevtoolError( + 'IMAGE_LINK_NAME is empty for %s, --nfs cannot locate the ' + 'rootfs tarball without it.' % self.name) + suffix = '-dbg' if nfs == 'rootfs-dbg' else '' + rootfs_tarball = os.path.join( + self.deploy_dir_image, self.image_link_name + suffix + '.tar') + rootfs_dir = self.nfs_rootfs_dir(nfs_export_base_dir, nfs) + state_dir = self.pseudo_state_dir(rootfs_dir) + + if os.path.exists(rootfs_dir): + logger.warning( + 'Re-extracting %s: files deployed into it are lost and a ' + 'target currently booted from it will break.', rootfs_dir) + for stale_dir in (rootfs_dir, state_dir): + if os.path.exists(stale_dir): + shutil.rmtree(stale_dir) + + self.extract_sdk_rootfs(rootfs_tarball, rootfs_dir) + + logger.info('NFS rootfs extracted to %s', rootfs_dir) + helper = self.nfs_runqemu_helper(nfs_export_base_dir, nfs) + if helper: + opts = 'slirp' if is_loopback_target(target) else '' + logger.info( + 'With the build environment sourced, start QEMU with NFS rootfs:\n' + ' %s %s\n' + 'Pass any additional runqemu options to this helper.', + helper, opts) + def update_qb_slirp_opt(self): """Update QB_SLIRP_OPT in the image bbappend @@ -1363,12 +1528,17 @@ class RecipeModified: 'by the %s recipe (PACKAGES: %s)' % (package, self.pn, ' '.join(self.packages_files.keys()))) - def gen_deploy_target_script(self, args): + def gen_deploy_target_script(self, args, deploy_target=None): """Generate a script which does what devtool deploy-target does This script is much quicker than devtool target-deploy. Because it does not need to start a bitbake server. All information from tinfoil is hard-coded in the generated script. + + deploy_target overrides args.target as the baked-in default, e.g. with + the local NFS rootfs directory when --nfs was used (see devtool.deploy + for how a directory target is handled without ssh). A runtime -t/--target + can still override this default, same as without --nfs. """ self._validate_requested_packages(args) cmd_lines = ['#!%s' % str(sys.executable)] @@ -1383,6 +1553,8 @@ class RecipeModified: 'no_preserve', 'port', 'show_status', 'ssh_exec', 'strip', 'target'] filtered_args_dict = {key: value for key, value in vars( args).items() if key in args_filter} + if deploy_target: + filtered_args_dict['target'] = deploy_target if is_loopback_target(filtered_args_dict['target']): filtered_args_dict['no_host_check'] = True cmd_lines.append('filtered_args_dict = %s' % str(filtered_args_dict)) @@ -1442,11 +1614,11 @@ class RecipeModified: ' tinfoil.shutdown()'] return self.write_script(cmd_lines, 'bb_run_do_install') - def gen_install_deploy_script(self, args): + def gen_install_deploy_script(self, args, deploy_target=None): """Generate a script which does install and deploy""" cmd_lines = ['#!/bin/sh -e'] cmd_lines.append(self.gen_install_task_script()) - cmd_lines.append(self.gen_deploy_target_script(args) + ' "$@"') + cmd_lines.append(self.gen_deploy_target_script(args, deploy_target) + ' "$@"') return self.write_script(cmd_lines, 'install_and_deploy') @@ -1549,6 +1721,12 @@ def ide_setup(args, config, basepath, workspace): logger.error("In shared sysroots mode modified recipes %s cannot be handled." % str( recipes_modified_names)) invalid_params = True + if args.nfs: + logger.error("--nfs is only supported in modified mode.") + invalid_params = True + if args.nfs_extract_dir and not args.nfs: + logger.error("--nfs-extract-dir requires --nfs.") + invalid_params = True if args.mode == DevtoolIdeMode.modified: if not recipes_modified_names: appends_dir = os.path.join(config.workspace_path, 'appends') @@ -1584,6 +1762,8 @@ def ide_setup(args, config, basepath, workspace): # For the shared sysroots mode, add all dependencies of all the images to the sysroots # For the modified mode provide one rootfs and the corresponding debug symbols via rootfs-dbg + nfs_export_base_dir = args.nfs_extract_dir or os.path.join( + config.workspace_path, 'nfs-exports') recipes_images = [] for recipes_image_name in recipes_image_names: logger.info("Using image: %s" % recipes_image_name) @@ -1591,6 +1771,13 @@ def ide_setup(args, config, basepath, workspace): recipes_image_name, orig_bbappend_contents.get(recipes_image_name)) recipe_image.initialize(config, tinfoil) + recipe_image.set_nfs_rootfs(nfs_export_base_dir, args.nfs) + # With --skip-bitbake nothing is extracted, so the generated IDE + # configuration would point at a directory that never appears. + if args.nfs and args.skip_bitbake and not os.path.isdir(recipe_image.nfs_deploy_dir): + raise DevtoolError( + "%s does not exist. Run devtool ide-sdk --nfs=%s without " + "--skip-bitbake first." % (recipe_image.nfs_deploy_dir, args.nfs)) if args.mode == DevtoolIdeMode.modified: # Keep the image build separate so that the complete bbappend # (IMAGE_ vars + QB_SLIRP_OPT) can be written in one step @@ -1696,7 +1883,7 @@ def ide_setup(args, config, basepath, workspace): # removed by strip_bbappend_sections() would be lost. bbappend_changed = False for ri in recipes_images: - if ri.update_image_bbappend(recipes_modified): + if ri.update_image_bbappend(recipes_modified, args.nfs): bbappend_changed = True if not args.skip_bitbake: @@ -1722,6 +1909,10 @@ def ide_setup(args, config, basepath, workspace): exec_build_env_command( config.init_path, basepath, bb_cmd + ' '.join(image_bootstrap_tasks), watch=True) + + if args.nfs and not args.skip_bitbake: + for ri in recipes_images: + ri.extract_nfs_rootfs(nfs_export_base_dir, args.nfs, args.target) else: raise DevtoolError("Must not end up here.") @@ -1796,6 +1987,16 @@ def register_commands(subparsers, context): '-P', '--port', help='Specify ssh port to use for connection to the target') parser_ide_sdk.add_argument( '-I', '--key', help='Specify ssh private key for connection to the target') + parser_ide_sdk.add_argument( + '--nfs', choices=('rootfs', 'rootfs-dbg'), + help='Build and extract the selected image rootfs below ' + '/nfs-exports// ' + 'for NFS booting.') + parser_ide_sdk.add_argument( + '--nfs-extract-dir', metavar='DIR', + help='Extract the --nfs rootfs below DIR// instead of the default ' + '/nfs-exports//, regardless of whether --nfs=rootfs ' + 'or --nfs=rootfs-dbg is selected. Requires --nfs.') parser_ide_sdk.add_argument( '--skip-bitbake', help='Skip the bitbake builds which update the SDK. The recipes are still parsed, ' 'the IDE configuration is generated from their metadata', action='store_true') From patchwork Wed Sep 9 21:53:05 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97788 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id E093FC88E50 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-228.siemens.flowmailer.net (mta-65-228.siemens.flowmailer.net [185.136.65.228]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.80.1788990824462594145 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=kTk8AUOP; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.228, mailfrom: fm-1329275-20260909215342060f9fa35e0002071f-3xesus@rts-flowmailer.siemens.com) Received: by mta-65-228.siemens.flowmailer.net with ESMTPSA id 20260909215342060f9fa35e0002071f for ; Wed, 09 Sep 2026 23:53:42 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=vHo1uzZZG51LglazYHcR0louWyMS0ZxAN36s7QE1BZw=; b=kTk8AUOP9Ney46033bIBbKMTMS3SCt7zV6Klt9PnJCbWsk82SCkfHtZALoi/X9h6xYTbN9 YsY3di0aQSkiuKmtRvLqJJN89TF5XIX+dt4II3sU2/GRmkjivQhWUoVfXrYKgqACKoEpAwCS PrlC2RLlqgQ89fpzaQQNwC7AI7lbnAfW/gCh1/Q4poqbcAaAPWLX3fi2uJawv9H6L26flAFZ UVJ50GgOlyQLtRblFNJBKmh+8xXH+FbkKb+HirWhKMqyA7Ny6zPBf6nrRi55dfNJZ1pKozeX JMNifWRr13QLak1+eeN0T4QzYVNI4UBpvlaL/IvHm1EK16RFQm9ZzX4A==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 05/15] oe-selftest: devtool ide-sdk: test NFS debug rootfs Date: Wed, 9 Sep 2026 23:53:05 +0200 Message-ID: <20260909215337.89106-6-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245511 From: Adrian Freihofer Exercise --nfs=rootfs-dbg with a real image build and verify the extracted rootfs, pseudo state, runqemu helper, image bbappend and the generated VS Code debug paths. Two further tests boot that rootfs over NFS with runqemu, once with tap and once with slirp networking, re-run ide-sdk against the actual target address and drive a full remote GDB debug session. Booting from NFS also exposed that the gdbserver pid file written by the start script can become readable over the next SSH connection only a moment later, so poll for it instead of reading it once. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 120 ++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 8bfdd15552..38eb9b3905 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -3627,8 +3627,17 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests): r = runCmd(gdbserver_script, output_log=self._cmd_logger) self.assertEqual(r.status, 0) + # The start script waits for gdbserver's port before returning, but on + # an NFS-root target (--nfs=rootfs-dbg) the pid file it wrote has been + # observed to become readable over the next SSH connection only a + # moment later, so poll for it. pid_file = '/tmp/gdbserver_1234_usr-bin-%s_multi/gdbserver.pid' % example_exe status, output = qemu.run('cat %s' % pid_file) + for _ in range(10): + if status == 0: + break + time.sleep(1) + status, output = qemu.run('cat %s' % pid_file) self.assertEqual(status, 0) gdbserver_pid = output.strip() self.assertRegex(gdbserver_pid, r'^\d+$') @@ -4068,6 +4077,117 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests): self.assertEqual(r.status, 0) debug_check_func(r.output, DevtoolIdeSdkTests.MAGIC_STRING_ORIG) + def test_devtool_ide_sdk_code_nfs_debug_rootfs(self): + """Verify ide-sdk extracts an NFS debug rootfs for VS Code debugging.""" + recipe_name = "cmake-example" + build_file = "CMakeLists.txt" + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config() + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + runCmd('devtool ide-sdk %s %s -c --ide=code --nfs=rootfs-dbg' % + (recipe_name, testimage), output_log=self._cmd_logger) + + nfs_rootfs = os.path.join( + self.workspacedir, 'nfs-exports', testimage, 'rootfs-dbg') + self.assertExists(nfs_rootfs) + self.assertExists(nfs_rootfs + '.pseudo_state') + self.assertExists(os.path.join(nfs_rootfs, 'usr', 'bin', recipe_name)) + runqemu_helper = os.path.join( + self.workspacedir, 'nfs-exports', testimage, + 'runqemu-rootfs-dbg') + self.assertExists(runqemu_helper) + self.assertTrue(os.access(runqemu_helper, os.X_OK)) + with open(runqemu_helper) as helper_file: + helper = helper_file.read() + self.assertIn('exec runqemu ', helper) + self.assertIn(nfs_rootfs, helper) + + bbappend = os.path.join( + self.workspacedir, 'appends', testimage + '.bbappend') + with open(bbappend) as append_file: + self.assertRegex( + append_file.read(), + r'IMAGE_FSTYPES_DEBUGFS(?::append)? = " ?tar"') + + with open(os.path.join(tempdir, '.vscode', 'launch.json')) as launch_file: + launch_configurations = json.load(launch_file)['configurations'] + for configuration in launch_configurations: + self.assertIn( + nfs_rootfs, configuration['additionalSOLibSearchPath']) + self.assertEqual( + os.path.join(nfs_rootfs, 'usr', 'src', 'debug'), + configuration['sourceFileMap']['/usr/src/debug']) + + def _test_devtool_ide_sdk_nfs_debug_rootfs_qemu(self, slirp=False): + """Boot the extracted NFS debug rootfs and exercise remote GDB.""" + recipe_name = "cmake-example" + build_file = "CMakeLists.txt" + testimage = "oe-selftest-image" + + self._check_workspace() + self._write_bb_config() + if not slirp: + self._check_runqemu_prerequisites() + + tempdir = self._devtool_ide_sdk_recipe( + recipe_name, build_file, testimage) + runCmd('devtool ide-sdk %s %s -c --ide=none --nfs=rootfs-dbg' % + (recipe_name, testimage), + output_log=self._cmd_logger) + + nfs_rootfs = os.path.join( + self.workspacedir, 'nfs-exports', testimage, 'rootfs-dbg') + runqemu_helper = os.path.join( + self.workspacedir, 'nfs-exports', testimage, + 'runqemu-rootfs-dbg') + self.assertExists(nfs_rootfs) + self.assertExists(runqemu_helper) + + launch_cmd = '%s nographic' % shlex.quote(runqemu_helper) + runqemuparams = '' + if slirp: + launch_cmd += ' slirp' + # QemuTarget uses this only to select its localhost SSH endpoint; + # launch_cmd supplies the actual runqemu option. + runqemuparams = 'slirp' + + with runqemu(testimage, runqemuparams=runqemuparams, + launch_cmd=launch_cmd) as qemu: + status, output = qemu.run( + "awk '$2 == \"/\" {print $3}' /proc/mounts") + self.assertEqual(status, 0) + self.assertEqual(output.strip(), 'nfs') + + # Re-run with the real QEMU target address, instead of 192.168.7.2 IP. + # --skip-bitbake also skips the rootfs extraction, which would + # otherwise wipe the directory the target has mounted right now. + if slirp: + self.assertIsNotNone(qemu.port, 'No SSH port for the slirp target') + target_options = '-t root@%s -P %s ' % (qemu.ip, qemu.port) + else: + target_options = '-t root@%s -c ' % qemu.ip + runCmd('devtool ide-sdk %s %s %s--skip-bitbake --ide=none --nfs=rootfs-dbg' % + (recipe_name, testimage, target_options), + output_log=self._cmd_logger) + + self._gdb_cross() + compile_cmd = self._verify_cmake_preset(tempdir) + self._devtool_ide_sdk_qemu( + tempdir, qemu, recipe_name, recipe_name, compile_cmd) + + @OETestTag("runqemu") + def test_devtool_ide_sdk_none_nfs_qemu(self): + """Verify remote GDB debugging through an NFS-root QEMU target.""" + self._test_devtool_ide_sdk_nfs_debug_rootfs_qemu() + + @OETestTag("runqemu") + def test_devtool_ide_sdk_none_nfs_qemu_slirp(self): + """Verify remote GDB debugging through an NFS-root slirp target.""" + self._test_devtool_ide_sdk_nfs_debug_rootfs_qemu(slirp=True) + @OETestTag("runqemu") def test_devtool_ide_sdk_code_cmake(self): """Verify a cmake recipe works with ide=code mode""" From patchwork Wed Sep 9 21:53:06 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97785 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 5C04EC88E47 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.83.1788990824619041170 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=q1GHB/5A; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-20260909215342d70c2075ae0002072b-sygomq@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 20260909215342d70c2075ae0002072b for ; Wed, 09 Sep 2026 23:53:42 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=6kEmu9tBHjNFywent13dwN8coUjgLxWrzm4ipLCBlJQ=; b=q1GHB/5AO9F3bKBbz34I0Prik/hiG/k/IUHICOPRcJDHrwdHTs+uwFW1iAolw8RAxUrfTT nMZcBtv9zJlKDolmav3JHMAqZY3towWrEg7seycxgLD8MViVjqP7SKRO9PJ2voBWMbWUjueA KX0nfYJQn0qHQfiBDVWDoS/U2uIjfRH8Jtm3142S1o0DZX6LCbCKQIyjFGjNDmbQsurZoAvk 7k6LZRQhW8jmqqfN1bfC43oBXTFxKuyxOFTCqTcRE6upsKTPNaIdHp4s4xVtbs4VQo4TXpbd s7/UxqjQ4ufKuKVVv8jDDd9JUPZv+pF4PcmUhmFupT1wtwBa+UAlARsw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 06/15] runqemu-extract-sdk: refactor in Python Date: Wed, 9 Sep 2026 23:53:06 +0200 Message-ID: <20260909215337.89106-7-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245510 From: Adrian Freihofer Move the pseudo-backed rootfs extraction logic into a reusable Python module while preserving the existing command interface. Install the module with qemu-helper so the wrapper also works from an SDK. Signed-off-by: Adrian Freihofer --- .../qemu/nativesdk-qemu-helper_1.0.bb | 3 + scripts/lib/pseudo_rootfs_utils.py | 113 +++++++++++++++++ scripts/runqemu-extract-sdk | 116 ++++-------------- 3 files changed, 137 insertions(+), 95 deletions(-) create mode 100644 scripts/lib/pseudo_rootfs_utils.py diff --git a/meta/recipes-devtools/qemu/nativesdk-qemu-helper_1.0.bb b/meta/recipes-devtools/qemu/nativesdk-qemu-helper_1.0.bb index e1f343989a..0558a564bb 100644 --- a/meta/recipes-devtools/qemu/nativesdk-qemu-helper_1.0.bb +++ b/meta/recipes-devtools/qemu/nativesdk-qemu-helper_1.0.bb @@ -9,6 +9,7 @@ RDEPENDS:${PN} = "nativesdk-qemu nativesdk-unfs3 nativesdk-pseudo \ LIC_FILES_CHKSUM = "file://${COREBASE}/scripts/runqemu;beginline=5;endline=10;md5=ac2b489a58739c7628a2604698db5e7f" SRC_URI = "file://${COREBASE}/scripts/runqemu \ + file://${COREBASE}/scripts/lib/pseudo_rootfs_utils.py \ file://${COREBASE}/scripts/runqemu-addptable2image \ file://${COREBASE}/scripts/runqemu-gen-tapdevs \ file://${COREBASE}/scripts/runqemu-ifup \ @@ -30,4 +31,6 @@ do_install() { install -d ${D}${bindir} install -m 0755 ${S}${COREBASE}/scripts/oe-* ${D}${bindir}/ install -m 0755 ${S}${COREBASE}/scripts/runqemu* ${D}${bindir}/ + # The runqemu-* wrappers import this module from their own directory. + install -m 0644 ${S}${COREBASE}/scripts/lib/pseudo_rootfs_utils.py ${D}${bindir}/ } diff --git a/scripts/lib/pseudo_rootfs_utils.py b/scripts/lib/pseudo_rootfs_utils.py new file mode 100644 index 0000000000..89d082f672 --- /dev/null +++ b/scripts/lib/pseudo_rootfs_utils.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# +# Helpers for preparing pseudo-managed rootfs trees (NFS booting, SDK/rootfs +# extraction, target deploy). +# +# SPDX-License-Identifier: GPL-2.0-only + +"""Extract rootfs tarballs and locate their pseudo state for NFS booting.""" + +import os +import subprocess +from pathlib import Path + + +class PseudoRootfsError(Exception): + """Raised when a pseudo-managed rootfs cannot be prepared or exported.""" + + +def pseudo_native_environment(): + """Return the pseudo native environment (PSEUDO and OECORE_NATIVE_SYSROOT), from the + qemu-helper-native recipe which provides a pseudo binary usable outside a recipe sysroot.""" + native_sysroot = os.environ.get('OECORE_NATIVE_SYSROOT') + if not native_sysroot: + try: + import bb.tinfoil + except ImportError as exc: + raise PseudoRootfsError( + 'Unable to import bitbake.\n' + 'Did you forget to source your build system environment setup script?') from exc + try: + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(quiet=2) + native_sysroot = tinfoil.parse_recipe('qemu-helper-native').getVar('STAGING_DIR_NATIVE') + except Exception as exc: + raise PseudoRootfsError('Unable to set up the qemu-helper-native sysroot') from exc + + if not native_sysroot or not os.path.exists(native_sysroot): + raise PseudoRootfsError("%s doesn't exist" % native_sysroot) + + environment = {'OECORE_NATIVE_SYSROOT': native_sysroot} + environment['PSEUDO'] = os.path.join(native_sysroot, 'usr', 'bin', 'pseudo') + return environment + + +def _tar_options(rootfs_tarball): + tar_extract_options = { + '.tar.xz': '-xJf', + '.tar.bz2': '-xjf', + '.tar.gz': '-xzf', + '.tar.zst': '--zstd -xf', + '.tar': '-xf', + } + for extension, option in tar_extract_options.items(): + if rootfs_tarball.endswith(extension): + return ['--numeric-owner', *option.split()] + raise PseudoRootfsError( + 'Unable to determine sdk tarball format\n' + 'Accepted types: .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst') + + +def pseudo_state_dir(rootfs_dir): + """Return the pseudo database location associated with an extracted rootfs.""" + return os.path.realpath(rootfs_dir) + '.pseudo_state' + + +def extract_sdk_rootfs(rootfs_tarball, rootfs_dir, pseudo_cmd, environment): + """Extract a rootfs tarball under pseudo and return its absolute directory. + + pseudo_cmd is the pseudo invocation prefix, e.g. a recipe's own + [FAKEROOTCMD] or [pseudo, '-P', native_sysroot_usr] from + pseudo_native_environment(). environment supplies the matching pseudo + database/env vars (e.g. a parsed FAKEROOTENV, or pseudo_native_environment() + itself); PSEUDO_LOCALSTATEDIR/PSEUDO_INCLUDE_PATHS are always overridden + here to point at rootfs_dir's own pseudo state, regardless of what's + already set in environment. + """ + if not os.path.exists(rootfs_tarball): + raise PseudoRootfsError("sdk tarball '%s' does not exist" % rootfs_tarball) + + rootfs_tarball = os.path.realpath(rootfs_tarball) + rootfs_dir = os.path.realpath(rootfs_dir) + tar_options = _tar_options(rootfs_tarball) + state_dir = pseudo_state_dir(rootfs_dir) + debug_image = '-dbg' in os.path.basename(rootfs_tarball) + + if os.path.exists(state_dir) and not debug_image: + raise PseudoRootfsError( + '%s already exists!\n' + 'Please delete the rootfs tree and pseudo directory manually\n' + 'if this is really what you want.' % state_dir) + + os.makedirs(rootfs_dir, exist_ok=True) + os.makedirs(state_dir, exist_ok=True) + Path(state_dir, 'pseudo.pid').touch() + + environment = dict(environment) + environment['PSEUDO_LOCALSTATEDIR'] = state_dir + environment['PSEUDO_INCLUDE_PATHS'] = rootfs_dir + + command = list(pseudo_cmd) + ['tar', '-C', rootfs_dir] + tar_options + [rootfs_tarball] + print('Extracting rootfs tarball using pseudo...') + print(' '.join(command)) + try: + subprocess.run(command, env=environment, check=True) + except subprocess.CalledProcessError as exc: + raise PseudoRootfsError('Failed to extract rootfs tarball') from exc + + if len(os.listdir(rootfs_dir)) < 4: + print("Warning: I don't see many files in %s" % rootfs_dir) + print('Please double-check the extraction worked as intended') + else: + print('SDK image successfully extracted to %s' % rootfs_dir) + return rootfs_dir diff --git a/scripts/runqemu-extract-sdk b/scripts/runqemu-extract-sdk index db9813e1dc..87b5f89916 100755 --- a/scripts/runqemu-extract-sdk +++ b/scripts/runqemu-extract-sdk @@ -1,104 +1,30 @@ -#!/bin/bash -# -# This utility extracts an SDK image tarball using pseudo, and stores -# the pseudo database in var/pseudo within the rootfs. If you want to -# boot QEMU using an nfsroot, you *must* use this script to create the -# rootfs to ensure it is done correctly with pseudo. -# -# Copyright (c) 2010 Intel Corp. +#!/usr/bin/env python3 # # SPDX-License-Identifier: GPL-2.0-only -# -function usage() { - echo "Usage: $0 " -} +import os +import sys -if [ $# -ne 2 ]; then - usage - exit 1 -fi +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib')) -SYSROOT_SETUP_SCRIPT=`which oe-find-native-sysroot 2> /dev/null` -if [ -z "$SYSROOT_SETUP_SCRIPT" ]; then - echo "Error: Unable to find the oe-find-native-sysroot script" - echo "Did you forget to source your build system environment setup script?" - exit 1 -fi -. $SYSROOT_SETUP_SCRIPT qemu-helper-native -PSEUDO_OPTS="-P $OECORE_NATIVE_SYSROOT/usr" +from pseudo_rootfs_utils import PseudoRootfsError, extract_sdk_rootfs, pseudo_native_environment -ROOTFS_TARBALL=$1 -SDK_ROOTFS_DIR=$2 -if [ ! -e "$ROOTFS_TARBALL" ]; then - echo "Error: sdk tarball '$ROOTFS_TARBALL' does not exist" - usage - exit 1 -fi +def main(): + argv = sys.argv[1:] + if len(argv) != 2: + print('Usage: %s ' % sys.argv[0]) + return 1 + try: + environment = pseudo_native_environment() + pseudo_cmd = [environment['PSEUDO'], '-P', + os.path.join(environment['OECORE_NATIVE_SYSROOT'], 'usr')] + extract_sdk_rootfs(*argv, pseudo_cmd, environment) + except PseudoRootfsError as exc: + print('Error: %s' % exc) + return 1 + return 0 -# Convert SDK_ROOTFS_DIR to a full pathname -if [[ ${SDK_ROOTFS_DIR:0:1} != "/" ]]; then - SDK_ROOTFS_DIR=$(readlink -f $(pwd)/$SDK_ROOTFS_DIR) -fi -TAR_OPTS="" -if [[ "$ROOTFS_TARBALL" =~ tar\.xz$ ]]; then - TAR_OPTS="--numeric-owner -xJf" -fi -if [[ "$ROOTFS_TARBALL" =~ tar\.bz2$ ]]; then - TAR_OPTS="--numeric-owner -xjf" -fi -if [[ "$ROOTFS_TARBALL" =~ tar\.gz$ ]]; then - TAR_OPTS="--numeric-owner -xzf" -fi -if [[ "$ROOTFS_TARBALL" =~ tar\.zst$ ]]; then - TAR_OPTS="--numeric-owner --zstd -xf" -fi -if [[ "$ROOTFS_TARBALL" =~ \.tar$ ]]; then - TAR_OPTS="--numeric-owner -xf" -fi -if [ -z "$TAR_OPTS" ]; then - echo "Error: Unable to determine sdk tarball format" - echo "Accepted types: .tar / .tar.gz / .tar.bz2 / .tar.xz / tar.zst" - exit 1 -fi - -if [ ! -d "$SDK_ROOTFS_DIR" ]; then - echo "Creating directory $SDK_ROOTFS_DIR" - mkdir -p "$SDK_ROOTFS_DIR" -fi - -pseudo_state_dir="$SDK_ROOTFS_DIR/../$(basename "$SDK_ROOTFS_DIR").pseudo_state" -pseudo_state_dir="$(readlink -f $pseudo_state_dir)" - -debug_image="`echo $ROOTFS_TARBALL | grep '\-dbg\.rootfs\.tar'`" - -if [ -e "$pseudo_state_dir" -a -z "$debug_image" ]; then - echo "Error: $pseudo_state_dir already exists!" - echo "Please delete the rootfs tree and pseudo directory manually" - echo "if this is really what you want." - exit 1 -fi - -mkdir -p "$pseudo_state_dir" -touch "$pseudo_state_dir/pseudo.pid" -PSEUDO_LOCALSTATEDIR="$pseudo_state_dir" -export PSEUDO_LOCALSTATEDIR -PSEUDO_INCLUDE_PATHS="$SDK_ROOTFS_DIR" -export PSEUDO_INCLUDE_PATHS - -echo "Extracting rootfs tarball using pseudo..." -echo "$PSEUDO $PSEUDO_OPTS tar -C \"$SDK_ROOTFS_DIR\" $TAR_OPTS \"$ROOTFS_TARBALL\"" -$PSEUDO $PSEUDO_OPTS tar -C "$SDK_ROOTFS_DIR" $TAR_OPTS "$ROOTFS_TARBALL" - -DIRCHECK=`ls -l "$SDK_ROOTFS_DIR" | wc -l` -if [ "$DIRCHECK" -lt 5 ]; then - echo "Warning: I don't see many files in $SDK_ROOTFS_DIR" - echo "Please double-check the extraction worked as intended" - exit 0 -fi - -echo "SDK image successfully extracted to $SDK_ROOTFS_DIR" - -exit 0 +if __name__ == "__main__": + sys.exit(main()) From patchwork Wed Sep 9 21:53:07 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97779 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 25284C88E41 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-225.siemens.flowmailer.net (mta-65-225.siemens.flowmailer.net [185.136.65.225]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.92.1788990824532674637 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=fcRlPjOQ; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.225, mailfrom: fm-1329275-202609092153426a312e77f00002071a-mkblch@rts-flowmailer.siemens.com) Received: by mta-65-225.siemens.flowmailer.net with ESMTPSA id 202609092153426a312e77f00002071a for ; Wed, 09 Sep 2026 23:53:42 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=EfAjU2tYwjcdPsWXyL5nwVjUBHOb/ADccvqPCTZZXTk=; b=fcRlPjOQ9R9u2YHxZ2epOMuypv9aDiFK0ZS71yvhEsUb5WXXaIHSdv2xlyifePO1/B1+ta sE/0t/4mRCEqVR6E+g4h1pnNTru/ou0zCQsaStqG9yHO5Wkslo0m9dv0mgzsSKVNRIbbO29Y txUPpX4q58GmZGngz818ugi3VAxzgPO9AZEOf4HQXX+wGWqCZyp49Xw8NX43GkVmqutSRfz6 3nrx8cednFRVu7YryODisSJn4c/rZxIe4u7j/pJpUd1eaaHVkhMzfMK6ZNxdxZacS/OPwuf4 8WdkLtPjrU+YYD3h0DexH2uAt3YfTRMwe3KX6ucalomPc70hZ5jOtJwg==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 07/15] devtool: ide-sdk: reuse pseudo_rootfs_utils for SDK rootfs extraction Date: Wed, 9 Sep 2026 23:53:07 +0200 Message-ID: <20260909215337.89106-8-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245512 From: Adrian Freihofer RecipeImage duplicated the tar/pseudo extraction logic now available in scripts/lib/pseudo_rootfs_utils.py (shared with runqemu-extract-sdk). Drop the local copy in favor of the shared extract_sdk_rootfs()/ pseudo_state_dir(). extract_sdk_rootfs() now takes the pseudo invocation and environment as parameters instead of resolving them itself via qemu-helper-native: devtool ide-sdk keeps using the image recipe's own FAKEROOTCMD/FAKEROOTENV, which is already guaranteed to be built as part of the image, rather than gaining a new, otherwise unused build dependency on qemu-helper-native. runqemu-extract-sdk resolves pseudo via pseudo_native_environment() itself and passes it in, as it has no recipe of its own to take FAKEROOTCMD from. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_sdk.py | 78 +++++++--------------------------- 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index 2e37f20894..ba9d4adff1 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -27,6 +27,7 @@ from devtool.standard import get_real_srctree from devtool.deploy import parse_packages_arg from devtool.ide_plugins import BuildTool, DebuggerCrossConfig from oe.kernel_module import kernel_module_os_env +from pseudo_rootfs_utils import PseudoRootfsError, extract_sdk_rootfs, pseudo_state_dir logger = logging.getLogger('devtool') @@ -441,66 +442,6 @@ class RecipeImage: slirp_changed = self.update_qb_slirp_opt() return image_changed or slirp_changed - @staticmethod - def _tar_options(rootfs_tarball): - tar_extract_options = { - '.tar.xz': '-xJf', - '.tar.bz2': '-xjf', - '.tar.gz': '-xzf', - '.tar.zst': '--zstd -xf', - '.tar': '-xf', - } - for extension, option in tar_extract_options.items(): - if rootfs_tarball.endswith(extension): - return ['--numeric-owner', *option.split()] - raise DevtoolError( - 'Unable to determine sdk tarball format\n' - 'Accepted types: .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst') - - @staticmethod - def pseudo_state_dir(rootfs_dir): - """Return the pseudo database location associated with an extracted rootfs.""" - return os.path.realpath(rootfs_dir) + '.pseudo_state' - - def extract_sdk_rootfs(self, rootfs_tarball, rootfs_dir): - """Extract a rootfs tarball under pseudo and return its absolute directory.""" - if not os.path.exists(rootfs_tarball): - raise DevtoolError("sdk tarball '%s' does not exist" % rootfs_tarball) - if not os.path.exists(self.fakerootcmd): - raise DevtoolError('%s does not exist' % self.fakerootcmd) - - rootfs_tarball = os.path.realpath(rootfs_tarball) - rootfs_dir = os.path.realpath(rootfs_dir) - tar_options = self._tar_options(rootfs_tarball) - state_dir = self.pseudo_state_dir(rootfs_dir) - - os.makedirs(rootfs_dir, exist_ok=True) - os.makedirs(state_dir, exist_ok=True) - Path(state_dir, 'pseudo.pid').touch() - - environment = dict(os.environ) - for varvalue in (self.fakerootenv or '').split(): - if '=' in varvalue: - key, value = varvalue.split('=', 1) - environment[key] = value - command = [self.fakerootcmd, 'tar', '-C', rootfs_dir] - - environment['PSEUDO_LOCALSTATEDIR'] = state_dir - environment['PSEUDO_INCLUDE_PATHS'] = rootfs_dir - command.extend(tar_options) - command.append(rootfs_tarball) - logger.info('Extracting rootfs tarball using pseudo: %s', ' '.join(command)) - try: - subprocess.run(command, env=environment, check=True) - except subprocess.CalledProcessError as exc: - raise DevtoolError('Failed to extract %s' % rootfs_tarball) from exc - - if len(os.listdir(rootfs_dir)) < 4: - logger.warning( - "Only few files in %s, please double-check the extraction " - "worked as intended", rootfs_dir) - return rootfs_dir - def extract_nfs_rootfs(self, nfs_export_base_dir, nfs, target): """Refresh the selected rootfs below nfs_export_base_dir.""" if not self.image_link_name: @@ -511,7 +452,7 @@ class RecipeImage: rootfs_tarball = os.path.join( self.deploy_dir_image, self.image_link_name + suffix + '.tar') rootfs_dir = self.nfs_rootfs_dir(nfs_export_base_dir, nfs) - state_dir = self.pseudo_state_dir(rootfs_dir) + state_dir = pseudo_state_dir(rootfs_dir) if os.path.exists(rootfs_dir): logger.warning( @@ -521,7 +462,20 @@ class RecipeImage: if os.path.exists(stale_dir): shutil.rmtree(stale_dir) - self.extract_sdk_rootfs(rootfs_tarball, rootfs_dir) + if not os.path.exists(self.fakerootcmd): + raise DevtoolError('%s does not exist' % self.fakerootcmd) + # Reuse the image's own pseudo instead of qemu-helper-native's, so + # extraction does not depend on a recipe devtool ide-sdk never builds. + pseudo_cmd = [self.fakerootcmd] + environment = dict(os.environ) + for varvalue in (self.fakerootenv or '').split(): + if '=' in varvalue: + key, value = varvalue.split('=', 1) + environment[key] = value + try: + extract_sdk_rootfs(rootfs_tarball, rootfs_dir, pseudo_cmd, environment) + except PseudoRootfsError as exc: + raise DevtoolError('Unable to prepare NFS rootfs: %s' % exc) from exc logger.info('NFS rootfs extracted to %s', rootfs_dir) helper = self.nfs_runqemu_helper(nfs_export_base_dir, nfs) From patchwork Wed Sep 9 21:53:08 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97786 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 8855CC88E4A for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-227.siemens.flowmailer.net (mta-65-227.siemens.flowmailer.net [185.136.65.227]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.82.1788990824563150120 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=W2OxYiKk; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.227, mailfrom: fm-1329275-2026090921534207870998580002077a-c98jru@rts-flowmailer.siemens.com) Received: by mta-65-227.siemens.flowmailer.net with ESMTPSA id 2026090921534207870998580002077a for ; Wed, 09 Sep 2026 23:53:42 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=3Yx3x2umf9CH+kpY6oCVjwemIhuPShIXJIfErmH3AwE=; b=W2OxYiKk18wMrBNU10kTN4pmZO7DOcEpQZwqAHt+tvsTKjl8x8BqXu24Ix84F+E61vE/J8 61tn29o5ldhi4IZ4tGj15o1kvOID8w3fjokBrrjPbLIF83jiYZ+hxExkOhlvSUotCwKou0T8 APoCVLDtgj+5XtP/NRzA4tkEH+sojPpu1iAFKidVkLhBQc+eLwKEdsrLj4XnRFr/UcEZc39b DyyshAtCWXxUkKI5UfeMMJiTnQCEoNBtbcsOvCWkPkdkRAs4ws/JVzxC1SxaMH2g8rShRH63 v0AeFer/uDtz6EdXm8TSj4tUUO//2de/DZB22S8C5oYi/78q+97098jw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 08/15] cpp-example: update formatting and code style Date: Wed, 9 Sep 2026 23:53:08 +0200 Message-ID: <20260909215337.89106-9-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245507 From: Adrian Freihofer Just run clang-format on the source files and add the .clang-format configuration file. Signed-off-by: Adrian Freihofer --- .../recipes-test/cpp/cpp-example.inc | 1 + .../recipes-test/cpp/files/.clang-format | 42 +++++++++++++++++++ .../recipes-test/cpp/files/cpp-example.cpp | 4 +- .../recipes-test/cpp/files/daemonize.cpp | 2 +- .../recipes-test/cpp/files/daemonize.hpp | 2 +- .../cpp/files/test-cpp-example.cpp | 13 +++--- 6 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 meta-selftest/recipes-test/cpp/files/.clang-format diff --git a/meta-selftest/recipes-test/cpp/cpp-example.inc b/meta-selftest/recipes-test/cpp/cpp-example.inc index 3934345f17..603040d123 100644 --- a/meta-selftest/recipes-test/cpp/cpp-example.inc +++ b/meta-selftest/recipes-test/cpp/cpp-example.inc @@ -22,6 +22,7 @@ SRC_URI = "\ file://config.h.in \ file://cpp-example.service \ file://cpp-example.init \ + file://.clang-format \ file://run-ptest \ " diff --git a/meta-selftest/recipes-test/cpp/files/.clang-format b/meta-selftest/recipes-test/cpp/files/.clang-format new file mode 100644 index 0000000000..e22fadaee9 --- /dev/null +++ b/meta-selftest/recipes-test/cpp/files/.clang-format @@ -0,0 +1,42 @@ +--- +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +BasedOnStyle: LLVM +Language: Cpp +Standard: c++17 + +IndentWidth: 4 +UseTab: Never +ColumnLimit: 100 + +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true + AfterClass: true + AfterStruct: true + AfterEnum: true + AfterUnion: true + AfterNamespace: false + AfterControlStatement: Never + BeforeElse: false + BeforeCatch: false + SplitEmptyFunction: false + SplitEmptyRecord: false +AlwaysBreakAfterReturnType: None +IndentCaseLabels: true +NamespaceIndentation: None +AccessModifierOffset: -4 + +PointerAlignment: Right +ReferenceAlignment: Right +SpaceAfterCStyleCast: false +SpaceBeforeParens: ControlStatements + +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never + +SortIncludes: false +IncludeBlocks: Preserve diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp index f18911554c..af7a8c2d73 100644 --- a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp +++ b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp @@ -28,7 +28,7 @@ bool g_use_syslog = false; // Prints an informational message. Under systemd, stdout is already captured // by the journal. Otherwise, use stdout until daemonized (stdio is then // redirected to /dev/null), and syslog afterwards so messages aren't lost. -void log_info(const std::string& msg) +void log_info(const std::string &msg) { #ifndef WITH_SYSTEMD if (g_use_syslog) { @@ -41,7 +41,7 @@ void log_info(const std::string& msg) } // namespace -int main(int argc, char* argv[]) +int main(int argc, char *argv[]) { bool endless_mode = false; #ifndef WITH_SYSTEMD diff --git a/meta-selftest/recipes-test/cpp/files/daemonize.cpp b/meta-selftest/recipes-test/cpp/files/daemonize.cpp index 7d91259bfa..e4752124f6 100644 --- a/meta-selftest/recipes-test/cpp/files/daemonize.cpp +++ b/meta-selftest/recipes-test/cpp/files/daemonize.cpp @@ -38,7 +38,7 @@ void daemonize() } } -void write_pidfile(const std::string& path) +void write_pidfile(const std::string &path) { std::ofstream pidfile(path, std::ios::trunc); pidfile << getpid() << std::endl; diff --git a/meta-selftest/recipes-test/cpp/files/daemonize.hpp b/meta-selftest/recipes-test/cpp/files/daemonize.hpp index 17d85d690a..d837836e02 100644 --- a/meta-selftest/recipes-test/cpp/files/daemonize.hpp +++ b/meta-selftest/recipes-test/cpp/files/daemonize.hpp @@ -20,7 +20,7 @@ void daemonize(); // Writes the current process's pid to the given path. -void write_pidfile(const std::string& path); +void write_pidfile(const std::string &path); // Permanently drops from root to the given group/user. Must run gid before // uid: once uid is dropped, the process no longer has permission to setgid. 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 e1909c3168..d8f2547e88 100644 --- a/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp +++ b/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp @@ -1,8 +1,8 @@ /* -* Copyright OpenEmbedded Contributors -* -* SPDX-License-Identifier: MIT -*/ + * Copyright OpenEmbedded Contributors + * + * SPDX-License-Identifier: MIT + */ #include "cpp-example-lib.hpp" @@ -13,10 +13,11 @@ #define FAIL_COMPARISON_STR "" #endif -int main() { +int main() +{ auto cpp_example = CppExample(); auto ret_string = cpp_example.get_string(); - if(0 == ret_string.compare(CppExample::test_string + FAIL_COMPARISON_STR)) { + if (0 == ret_string.compare(CppExample::test_string + FAIL_COMPARISON_STR)) { std::cout << "PASS: " << ret_string << " = " << CppExample::test_string << std::endl; } else { std::cout << "FAIL: " << ret_string << " != " << CppExample::test_string << std::endl; From patchwork Wed Sep 9 21:53:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97789 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id F40D6C88E4F for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.94.1788990824703066965 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=fVfm2/xm; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-202609092153425b0f44b7890002079c-wccjll@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 202609092153425b0f44b7890002079c for ; Wed, 09 Sep 2026 23:53:42 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=swIIop4HE5gLWTFBh/A5sjz/Y/KPx3eNHSMKyyvqnXU=; b=fVfm2/xmPWTnSziFamxGTB6CovLnldDiWmTZvEd4QgZDbwRGK3m+kfE9VUQBdWcelxAtr+ 6fue2QMYuZpHoO3azXT2702lyegcbGfKgnWknzQD2JWwsqWAgV/8RQ+YLNZMwDiBioLkitFV mYKQP3DHw3PFdEtaIKBW9gGhsvDXaxfyH5D6DSRxISiZganSBed2RahGpTT8HqjI/tcxFaEL 3E8Ncd94hugNyeKeeaFUHLTR0aP6WqmN+3XPrmNdMxU3MX80dB/dPasA1W0dKAK0PVMZiq5g pr+O99VNSlmPtjc9jJjUhultoV32B7BiIraeWi59rjuwTxgb4vLfOgQw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 09/15] cpp-example: fix clang-tidy warnings Date: Wed, 9 Sep 2026 23:53:09 +0200 Message-ID: <20260909215337.89106-10-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245513 From: Adrian Freihofer Just run clang-tidy, fixing warnings and applying the configured checks. Add also the .clang-tidy configuration file. Signed-off-by: Adrian Freihofer cmake-example: add .clang-tidy configuration file Signed-off-by: Adrian Freihofer --- .../recipes-test/cpp/cpp-example.inc | 1 + .../recipes-test/cpp/files/.clang-tidy | 33 +++++++++++++++++++ .../cpp/files/cpp-example-lib.hpp | 3 +- .../recipes-test/cpp/files/cpp-example.cpp | 1 + .../cpp/files/test-cpp-example.cpp | 2 +- meta/lib/oeqa/selftest/cases/devtool.py | 20 +++++------ 6 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 meta-selftest/recipes-test/cpp/files/.clang-tidy diff --git a/meta-selftest/recipes-test/cpp/cpp-example.inc b/meta-selftest/recipes-test/cpp/cpp-example.inc index 603040d123..5800f804e1 100644 --- a/meta-selftest/recipes-test/cpp/cpp-example.inc +++ b/meta-selftest/recipes-test/cpp/cpp-example.inc @@ -23,6 +23,7 @@ SRC_URI = "\ file://cpp-example.service \ file://cpp-example.init \ file://.clang-format \ + file://.clang-tidy \ file://run-ptest \ " diff --git a/meta-selftest/recipes-test/cpp/files/.clang-tidy b/meta-selftest/recipes-test/cpp/files/.clang-tidy new file mode 100644 index 0000000000..d1cafb1fe6 --- /dev/null +++ b/meta-selftest/recipes-test/cpp/files/.clang-tidy @@ -0,0 +1,33 @@ +--- +# Copyright OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + clang-analyzer-*, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + -performance-avoid-endl, + readability-*, + -readability-magic-numbers, + -readability-identifier-length, + -readability-implicit-bool-conversion, + -readability-convert-member-functions-to-static, + -readability-isolate-declaration + +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file + +CheckOptions: + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.ClassCase + value: CamelCase 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 5af30e2a79..7318d4db84 100644 --- a/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp +++ b/meta-selftest/recipes-test/cpp/files/cpp-example-lib.hpp @@ -12,13 +12,14 @@ struct CppExample { + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) -- exercises debugger handling of a lazy-init static member, see devtool ide-sdk tests inline static const std::string test_string = "cpp-example-lib Magic: 123456789"; /* Header-only function, to exercise breakpoint resolution against * header-only debug info. volatile prevents compiler optimization from * eliminating the function body, ensuring a concrete code location exists * for debugger breakpoints. */ - inline static int scale_number(int n) + static int scale_number(int n) { volatile int scaled = n * 7; std::cout << "scale_number(" << n << ") = " << scaled << std::endl; diff --git a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp index af7a8c2d73..c14def288c 100644 --- a/meta-selftest/recipes-test/cpp/files/cpp-example.cpp +++ b/meta-selftest/recipes-test/cpp/files/cpp-example.cpp @@ -113,6 +113,7 @@ int main(int argc, char *argv[]) log_info(std::string("Linking json-c version ") + cpp_example.get_json_c_version()); cpp_example.print_json(); + // NOLINTNEXTLINE(bugprone-infinite-loop) -- intentionally endless when requested; stopped externally (e.g. systemd stop/SIGTERM) do { // Read and print message from config file std::string config_message = cpp_example.read_config_message(); 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 d8f2547e88..69dc2fe6d5 100644 --- a/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp +++ b/meta-selftest/recipes-test/cpp/files/test-cpp-example.cpp @@ -17,7 +17,7 @@ int main() { auto cpp_example = CppExample(); auto ret_string = cpp_example.get_string(); - if (0 == ret_string.compare(CppExample::test_string + FAIL_COMPARISON_STR)) { + if (ret_string == CppExample::test_string + FAIL_COMPARISON_STR) { std::cout << "PASS: " << ret_string << " = " << CppExample::test_string << std::endl; } else { std::cout << "FAIL: " << ret_string << " != " << CppExample::test_string << std::endl; diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 38eb9b3905..1cc9843a72 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -3312,8 +3312,8 @@ class DevtoolIdeSdkTests(DevtoolBase): cpp_code = file.read() cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, MAGIC_STRING_NEW) cpp_code = cpp_code.replace( - " inline static int scale_number(int n)", - extra_lines + " inline static int scale_number(int n)") + " static int scale_number(int n)", + extra_lines + " static int scale_number(int n)") with open(cpp_example_lib_hpp, 'w') as file: file.write(cpp_code) @@ -3362,8 +3362,8 @@ class DevtoolIdeSdkTests(DevtoolBase): # the first _gdb_cross_debugging_multi call above. self._gdb_cross_debugging_multi( qemu, recipe_name, example_exe, MAGIC_STRING_NEW, - exe_break_line=136 + LINE_SHIFT, exe_list_line=128 + LINE_SHIFT, - hpp_break_line=21 + LINE_SHIFT, lib_break_line=31 + LINE_SHIFT) + exe_break_line=136 + LINE_SHIFT, exe_list_line=129 + LINE_SHIFT, + hpp_break_line=24 + LINE_SHIFT, lib_break_line=31 + LINE_SHIFT) def _verify_cmake_preset(self, tempdir): """Verify the generated cmake preset works as expected @@ -3532,14 +3532,14 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests): self.assertIn("GNU gdb", r.output) def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run", - exe_break_line=136, exe_list_line=128, hpp_break_line=21, + exe_break_line=136, exe_list_line=129, hpp_break_line=24, lib_break_line=31): """Get a series of gdb commands to debug the cpp-example-lib example""" gdb_batch_cmd = " -ex 'break main' -ex '%s'" % gdb_start_cmd gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'" gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string - gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:15,15'" + gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:16,16'" # Break inside the library's own .cpp file by file:line (not by # symbol), while still inside the print_json() call reached above. @@ -3578,7 +3578,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests): gdb_batch_cmd += " -ex 'continue'" return gdb_batch_cmd - def _gdb_debug_cpp_example_check(self, gdb_output, magic_string, exe_list_line=128, lib_break_line=31): + def _gdb_debug_cpp_example_check(self, gdb_output, magic_string, exe_list_line=129, lib_break_line=31): self.assertIn("Breakpoint 1, main", gdb_output) self.assertIn("$1 = 0", gdb_output) # test.string.compare equal self.assertIn("$2 = -3", gdb_output) # test.string.compare longer @@ -3606,7 +3606,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests): self.assertIn("exited normally", gdb_output) def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string, - exe_break_line=136, exe_list_line=128, hpp_break_line=21, + exe_break_line=136, exe_list_line=129, hpp_break_line=24, lib_break_line=31): """Verify gdb-cross is working @@ -4814,8 +4814,8 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests): cpp_code = file.read() cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, magic_string_new) cpp_code = cpp_code.replace( - " inline static int scale_number(int n)", - extra_lines + " inline static int scale_number(int n)") + " static int scale_number(int n)", + extra_lines + " static int scale_number(int n)") with open(cpp_example_lib_hpp, 'w') as file: file.write(cpp_code) From patchwork Wed Sep 9 21:53:10 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97778 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id B9F76C79FB6 for ; Wed, 9 Sep 2026 21:53:51 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.93.1788990824645394523 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=gViS7AtT; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-2026090921534394fac6763c000207fb-mtckcg@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 2026090921534394fac6763c000207fb for ; Wed, 09 Sep 2026 23:53:43 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=7dU4UyIkJXQ1zrNWoHas/+YMtyw743JLUSt3fvfpXQM=; b=gViS7AtTACuDYATSO43zQ43lFephuKUK8oy8ECwe+b5LDdavqOzn/E1NRSOBDje6no5Bni hfReItPLUYgErJ6UfMZ5klD9ij+bq1gzn0N1XuBq6SMvZOyuCuW9L9HrAHeRHcx0WorHFl4V KY8EomiefV+q1b7WC0GO8LfatS2Y4EAO7V/rHFosct+FvzOwiSnrg8N4ovr58UkAqG4cs7Rt qwQaPrrvkkmm4VqtYoJVb7l+UWRgYfz6zHZLEd+qwdu7GHHIa8XbqqjrDeVBjelUXCg3cuJS og1lvf274oqxy43+kpaQP2q/pnT0jYYTSd+NxMErwvI1EqEwYLqcCafw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 10/15] devtool: ide-sdk: add clangd support for VSCode IntelliSense Date: Wed, 9 Sep 2026 23:53:10 +0200 Message-ID: <20260909215337.89106-11-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245515 From: Adrian Freihofer Add an llvm-vs-code-extensions.vscode-clangd based IntelliSense provider as an alternative to cpptools for ide=code. clangd is matched to the recipe's real cross compiler via compile_commands.json and --query-driver, which works for gcc cross-compilers as well as clang. As a fisrst step clangd is supported if TOOLCHAIN is clang. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/ide_code.py | 41 ++++++++++++++++++--- scripts/lib/devtool/ide_sdk.py | 2 + 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 7c700f258e..acf3db7494 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -228,6 +228,26 @@ class IdeVSCode(IdeBase): } ] + def __vscode_settings_clangd(self, settings_dict, modified_recipe): + if not (modified_recipe.ide_sdk_intellisense == 'clangd' and modified_recipe.build_tool.is_c_cpp): + return + # Use the recipe's real cross compiler (gcc or clang, whichever TOOLCHAIN + # actually builds it), not a hardcoded 'clang'/'clang++' name. + cross_cc = os.path.join(modified_recipe.staging_bindir_toolchain, modified_recipe.cc.split()[0]) + cross_cxx = os.path.join(modified_recipe.staging_bindir_toolchain, modified_recipe.cxx.split()[0]) + settings_dict["clangd.path"] = os.path.join( + modified_recipe.recipe_sysroot_native, 'usr', 'bin', 'clangd') + settings_dict["clangd.arguments"] = [ + # Allow-list the cross compiler so clangd queries it (not its own + # bundled clang) for target-specific builtin includes/defines. + # Works for gcc cross-compilers too, not just clang. + "--query-driver=%s,%s" % (cross_cc, cross_cxx), + "--compile-commands-dir=%s" % modified_recipe.b, + "--background-index" + ] + # Avoid cpptools (if also installed) fighting clangd over IntelliSense. + settings_dict["C_Cpp.intelliSenseEngine"] = "disabled" + def vscode_settings(self, modified_recipe, image_recipe): files_hide = { "**/.git/**": True, @@ -261,6 +281,7 @@ class IdeVSCode(IdeBase): self.__vscode_settings_cmake(settings_dict, modified_recipe) self.__vscode_settings_meson(settings_dict, modified_recipe) self.__vscode_settings_kernel_module(settings_dict, modified_recipe) + self.__vscode_settings_clangd(settings_dict, modified_recipe) settings_file = 'settings.json' IdeBase.update_json_file( @@ -268,16 +289,22 @@ class IdeVSCode(IdeBase): def vscode_extensions(self, modified_recipe): recommendations = [] - if modified_recipe.build_tool.is_c_cpp_kernel: + # clangd (matched to the recipe's real cross compiler via + # compile_commands.json) replaces cpptools as the IntelliSense provider + # if toolchain is clang. CodeLLDB/GDB debugging is unaffected either way. + use_clangd = (modified_recipe.ide_sdk_intellisense == 'clangd' + and modified_recipe.build_tool.is_c_cpp) + if use_clangd: + recommendations += [ + "llvm-vs-code-extensions.vscode-clangd", + "vadimcn.vscode-lldb" + ] + elif modified_recipe.build_tool.is_c_cpp_kernel: recommendations += [ "ms-vscode.cpptools", "ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-themes" ] - # For clang toolchain, CodeLLDB provides native LLDB debugging in VSCode - if (modified_recipe.toolchain == 'clang' - and modified_recipe.build_tool.is_c_cpp): - recommendations.append("vadimcn.vscode-lldb") if modified_recipe.build_tool is BuildTool.CMAKE: recommendations.append("ms-vscode.cmake-tools") if modified_recipe.build_tool is BuildTool.MESON: @@ -290,6 +317,10 @@ class IdeVSCode(IdeBase): self.dot_code_dir(modified_recipe), extensions_file, {"recommendations": recommendations}) def vscode_c_cpp_properties(self, modified_recipe, image_recipe): + # cpptools' IntelliSense is disabled in settings.json when clangd is + # the active provider, so this file would be inert clutter. + if modified_recipe.build_tool.is_c_cpp and modified_recipe.ide_sdk_intellisense == 'clangd': + return properties_dict = { "name": modified_recipe.recipe_id_pretty, } diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index ba9d4adff1..a3fbe7cdcb 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -789,6 +789,7 @@ class RecipeModified: self.target_arch = None self.tmpdir = None self.toolchain = None + self.ide_sdk_intellisense = None self.topdir = None self.workdir = None # Maps each package name (from PACKAGES) to the glob patterns from its FILES variable @@ -884,6 +885,7 @@ class RecipeModified: self.target_arch = recipe_d.getVar('TARGET_ARCH') self.tmpdir = os.path.realpath(recipe_d.getVar('TMPDIR')) self.toolchain = recipe_d.getVar('TOOLCHAIN') + self.ide_sdk_intellisense = 'clangd' if self.toolchain == 'clang' else 'cpptools' self.topdir = recipe_d.getVar('TOPDIR') self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR')) From patchwork Wed Sep 9 21:53:11 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97780 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id D827DC88E40 for ; Wed, 9 Sep 2026 21:53:51 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.95.1788990824826707821 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=ePaI9cBc; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-20260909215343d525040fa000020753-_lxefr@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 20260909215343d525040fa000020753 for ; Wed, 09 Sep 2026 23:53:43 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=tWBgFYnnCkCw0nF7ViejA15m8nweKEV0YCuM5njyl+E=; b=ePaI9cBc38rcck7UathuvvsdK18tTh9OByvZqBlNtpJNZkvvqBrgOr622pmiOJi6LuMJrD 8oEvOZ24T2It98FKDXPeAbYSAkGhfzYfol0cKS76FBzSgQJnvl3P3vBR+6D1FrrsRp2DONUq 5i2r93tKkItGPV/DoFk5k33t0fUx5Naoqtjeu5YKxnKjvTXs45Ip8P251Z5NQeyE9l87j7vz CpAwJH9VlBTRfmWEhRpKZVrYMiw/uMMsGzFM4K/pwa8lHxEf8JcZa2T6Ujume00l9lDkn15D yOXAZb6c9kPZfokUeCrjzu2n3W/XlOxUov4FdaqQA8pqZXELJFIEkTNQ==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 11/15] devtool: ide-sdk: support clangd for non-clang toolchain recipes Date: Wed, 9 Sep 2026 23:53:11 +0200 Message-ID: <20260909215337.89106-12-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245514 From: Adrian Freihofer Make the IntelliSense provider choice independent of TOOLCHAIN. clangd ships in the clang recipe, whose "clang-native" is only pulled into RECIPE_SYSROOT_NATIVE via clang-cross-* when TOOLCHAIN == "clang". Introduce a new bitbake variable IDE_SDK_INTELLISENSE which can be set to "clangd" on a gcc (or other non-clang) recipe. This requires adding the clang-native recipe to the sysroots in addition to the recipes TOOLCHAIN. Add RecipeModified.update_bbappend(), mirroring RecipeImage's marker-section bbappend management: which adds DEPENDS:append = " clang-native" to the recipe's own workspace bbappend. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/ide_code.py | 3 +- scripts/lib/devtool/ide_sdk.py | 99 ++++++++++++++++++++- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index acf3db7494..8a8ab8bf3d 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -291,7 +291,8 @@ class IdeVSCode(IdeBase): recommendations = [] # clangd (matched to the recipe's real cross compiler via # compile_commands.json) replaces cpptools as the IntelliSense provider - # if toolchain is clang. CodeLLDB/GDB debugging is unaffected either way. + # when selected via IDE_SDK_INTELLISENSE (defaults to clangd for the + # clang toolchain). CodeLLDB/GDB debugging is unaffected either way. use_clangd = (modified_recipe.ide_sdk_intellisense == 'clangd' and modified_recipe.build_tool.is_c_cpp) if use_clangd: diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index a3fbe7cdcb..f9a92ac5ca 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -752,9 +752,12 @@ class RecipeModified: VALID_BASH_ENV_NAME_CHARS = re.compile(r"^[a-zA-Z0-9_]*$") - def __init__(self, name): + MARKER = '# devtool ide-sdk: clangd toolchain support' + + def __init__(self, name, orig_bbappend_content=None): self.name = name self.bootstrap_tasks = [name + ':do_install'] + self._orig_bbappend_content = orig_bbappend_content self.debugger_cross = None # workspace self.real_srctree = None @@ -829,6 +832,73 @@ class RecipeModified: self._installed_binaries = None self._gdb_pretty_print_scripts = None + @staticmethod + def _strip_marker_section(content, marker): + """Remove one devtool ide-sdk marker section, if present""" + return re.sub( + r'^' + re.escape(marker) + r'\n(?:[^\n]+\n)*', + '', content, flags=re.MULTILINE) + + @classmethod + def strip_bbappend_sections(cls, config, recipe_names): + """Remove devtool ide-sdk's own bbappend section from earlier runs + + Runs before setup_tinfoil() so that every parse in this session sees the + original recipe content and only non standard flags can be bbappended later. + """ + originals = {} + appends_dir = os.path.join(config.workspace_path, 'appends') + for name in recipe_names: + bbappend = os.path.join(appends_dir, name + '.bbappend') + if not os.path.exists(bbappend): + continue + with open(bbappend, 'r') as f: + content = f.read() + originals[name] = content + stripped = cls._strip_marker_section(content, cls.MARKER) + if stripped != content: + with open(bbappend, 'w') as f: + f.write(stripped) + return originals + + def update_bbappend(self): + """Add DEPENDS on clang-native when clangd is used as only the IntelliSense engine""" + wants_clang_native = ( + self.build_tool.is_c_cpp + and self.ide_sdk_intellisense == 'clangd' + and self.toolchain != 'clang') + + original_content = self._orig_bbappend_content or '' + if os.path.exists(self.bbappend): + with open(self.bbappend, 'r') as f: + parsed_content = f.read() + else: + parsed_content = '' + + if not wants_clang_native: + if self.MARKER in original_content: + logger.info( + "Removed clangd toolchain support from %s: no longer needed", self.bbappend) + return False + + new_section = self.MARKER + '\nDEPENDS:append = " clang-native"\n' + new_content = parsed_content + if new_content and not new_content.endswith('\n'): + new_content += '\n' + new_content += new_section + + if new_content == original_content: + return False + + appends_dir = os.path.dirname(self.bbappend) + os.makedirs(appends_dir, exist_ok=True) + with open(self.bbappend, 'w') as f: + f.write(new_content) + logger.info( + "Updated %s: added DEPENDS on clang-native, needed to stage clangd " + "for IDE_SDK_INTELLISENSE=\"clangd\"", self.bbappend) + return True + def initialize(self, config, workspace, tinfoil): recipe_d = parse_recipe( config, tinfoil, self.name, appends=True, filter_workspace=False) @@ -885,7 +955,8 @@ class RecipeModified: self.target_arch = recipe_d.getVar('TARGET_ARCH') self.tmpdir = os.path.realpath(recipe_d.getVar('TMPDIR')) self.toolchain = recipe_d.getVar('TOOLCHAIN') - self.ide_sdk_intellisense = 'clangd' if self.toolchain == 'clang' else 'cpptools' + self.ide_sdk_intellisense = recipe_d.getVar('IDE_SDK_INTELLISENSE') or ( + 'clangd' if self.toolchain == 'clang' else 'cpptools') self.topdir = recipe_d.getVar('TOPDIR') self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR')) @@ -1650,6 +1721,8 @@ def ide_setup(args, config, basepath, workspace): # ide-sdk section it is a no-op. orig_bbappend_contents = RecipeImage.strip_bbappend_sections( config, args.recipenames) + orig_recipe_bbappend_contents = RecipeModified.strip_bbappend_sections( + config, args.recipenames) tinfoil = setup_tinfoil(config_only=False, basepath=basepath) try: # define mode depending on recipes which need to be processed @@ -1769,7 +1842,8 @@ def ide_setup(args, config, basepath, workspace): str(recipes_modified_names)) debuggers = {} for recipe_name in recipes_modified_names: - recipe_modified = RecipeModified(recipe_name) + recipe_modified = RecipeModified( + recipe_name, orig_recipe_bbappend_contents.get(recipe_name)) recipe_modified.initialize(config, workspace, tinfoil) bootstrap_tasks += recipe_modified.bootstrap_tasks recipes_modified.append(recipe_modified) @@ -1797,7 +1871,26 @@ def ide_setup(args, config, basepath, workspace): if args.bitbake_k: bb_cmd += "-k " + # Add back the clangd toolchain support section stripped above, if still + # needed. Runs even with --skip-bitbake, otherwise it would be lost (see + # RecipeModified.strip_bbappend_sections()). + recipe_bbappend_changed = False + for recipe_modified in recipes_modified: + if recipe_modified.update_bbappend(): + recipe_bbappend_changed = True + if not args.skip_bitbake: + if recipe_bbappend_changed: + # The bbappend content just written differs from the one bitbake + # parsed during the tinfoil session above. See update_image_bbappend()'s + # matching reset for phase 2 for why this is needed. + reparse_tinfoil = setup_tinfoil(config_only=True, basepath=basepath) + try: + reparse_tinfoil.run_command('resetCooker') + reparse_tinfoil.parse_recipes() + finally: + reparse_tinfoil.shutdown() + # Phase 1: build modified recipes and debug tools so that # installed_binaries is populated and port numbers can be assigned. # The image is built in phase 2, after the complete bbappend is written. From patchwork Wed Sep 9 21:53:12 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97776 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 9B361C79FBB for ; Wed, 9 Sep 2026 21:53:51 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.84.1788990825113338710 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=PPvm9otI; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-20260909215343654ceae7580002070c-tlkoty@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 20260909215343654ceae7580002070c for ; Wed, 09 Sep 2026 23:53:43 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=IJLW+xAN4FHkjAQUK44OVLT5tWuXDo4u5gXnKQ8PVIk=; b=PPvm9otIMjNabkturIEkkt+XFkXhF++8G9/Tn9p2J71SJXibKIg5vFUuRRGosIu0FI6pdS DMTx5l2mRPnyny95RXS5i+dfGdwcZwqm7swEbi7nT88ri6WCn8tmVYY3UbSDuYoBALHLbMi/ 4X+K89GzUbM0sBFXdMD98Hzd1ueppiL66M6qPfjicajDZ16iCrv4an2KXOSRHS9+mSxZ5/A9 rWAQ3SLcouA7/+VuawpvifjEEPUxj25GQpbNHVzjLXNRp8QuhVBRgANr8g2J54quU49vrqnD GE8GWkeJd+5AR9BYGDHZAn/PnvwC23ekaUfW7mQv68VUeFfQPhKsHOUw==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 12/15] devtool: ide-sdk: format C/C++ with clangd when .clang-format is present Date: Wed, 9 Sep 2026 23:53:12 +0200 Message-ID: <20260909215337.89106-13-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245504 From: Adrian Freihofer Add "[cpp]"/"[c]" defaultFormatter and editor.formatOnSave settings pointing at the clangd VSCode extension, so devtool ide-sdk-generated workspaces auto-format on save using the recipe's own .clang-format style. Only enabled when clangd is the active IntelliSense engine (IDE_SDK_INTELLISENSE == "clangd", which also gates whether the clangd extension is recommended/installed) and a .clang-format file exists in the recipe source tree, so gcc/cpptools recipes and recipes without a .clang-format are unaffected. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_plugins/ide_code.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index 8a8ab8bf3d..f8036f15de 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -248,6 +248,19 @@ class IdeVSCode(IdeBase): # Avoid cpptools (if also installed) fighting clangd over IntelliSense. settings_dict["C_Cpp.intelliSenseEngine"] = "disabled" + def __vscode_settings_format(self, settings_dict, modified_recipe): + # clangd is the only formatter wired up here, so only enable it when + # clangd is actually the active IntelliSense engine (its extension is + # recommended/available). + if not (modified_recipe.ide_sdk_intellisense == 'clangd' and modified_recipe.build_tool.is_c_cpp): + return + # Respect the project's own formatting style only if it opted in. + if not os.path.isfile(os.path.join(modified_recipe.real_srctree, '.clang-format')): + return + settings_dict["[cpp]"] = {"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"} + settings_dict["[c]"] = {"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"} + settings_dict["editor.formatOnSave"] = True + def vscode_settings(self, modified_recipe, image_recipe): files_hide = { "**/.git/**": True, @@ -282,6 +295,7 @@ class IdeVSCode(IdeBase): self.__vscode_settings_meson(settings_dict, modified_recipe) self.__vscode_settings_kernel_module(settings_dict, modified_recipe) self.__vscode_settings_clangd(settings_dict, modified_recipe) + self.__vscode_settings_format(settings_dict, modified_recipe) settings_file = 'settings.json' IdeBase.update_json_file( From patchwork Wed Sep 9 21:53:13 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97777 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 9A541C79FB7 for ; Wed, 9 Sep 2026 21:53:51 +0000 (UTC) Received: from mta-64-225.siemens.flowmailer.net (mta-64-225.siemens.flowmailer.net [185.136.64.225]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.97.1788990825719636477 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=bSbspDtv; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.225, mailfrom: fm-1329275-2026090921534333679adddb00020771-amydjm@rts-flowmailer.siemens.com) Received: by mta-64-225.siemens.flowmailer.net with ESMTPSA id 2026090921534333679adddb00020771 for ; Wed, 09 Sep 2026 23:53:43 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=mSN9J9d3II87Tssj/0I0soOiAt74SryyoPtZ1oQ0X4w=; b=bSbspDtv5x37MFe4eF0OIfoLGgg5Ni6wUQMi45qWyR5tDe1UZ8rY7aF615W45e2dam6BZz P0BT4M66FabyekErsSL17sUfF7d06AxscIu8Gs5/7dFElHO2T34WpV33bQ4o11uhgDbOIWcn pHHHh8h6ZyfEkzWnqm3su3CAerEi4t59GW2npBa39XF1UOZsNUVouKzGR7WbFRCqJ7nDh48R nOoWPXSEpNnnWrxkeIOYkKnTOMzzyzsGNeV99cdFmKUhEQ0QxW2IJrQzC4gdZEWYcauHCPfi FGC1wpIuQSDCO4tc3b6TahhfzWsR7nsxsPmQR83HJxjgVmCSj36u8fkA==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 13/15] devtool: ide-sdk: support LLDB ATTACH mode Date: Wed, 9 Sep 2026 23:53:13 +0200 Message-ID: <20260909215337.89106-14-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245516 From: Adrian Freihofer Bring support for LLDB ATTACH mode to the IDE SDK as it is already supported for GDB. lldb-server platform mode has no launch/attach distinction on the target: it just offers a platform connection, and the CodeLLDB client decides client-side whether to launch or attach. ATTACH was previously rejected outright because gdbserver's PID-based attach model doesn't apply, but lldb-server can serve both modes identically. Give each server_modes() entry (MULTI and ATTACH, for service binaries) its own gdbserver_ports entry instead of the single gdbserver_port, so both get their own slirp-forwarded port. _vscode_launch_bin_dbg_lldb() now emits "request": "attach" with a postDebugTask to stop the lldb-server instance afterwards, and drops the launch-only stopOnEntry/cwd keys, which are invalid in the attach schema. ide_none.py's lldbinit generation and LldbServerConfigNone.initialize() are updated the same way to also generate the ATTACH-mode start script for service binaries. Also update the tests accordingly to verify the new ATTACH mode configurations. Signed-off-by: Adrian Freihofer --- meta/lib/oeqa/selftest/cases/devtool.py | 19 +++++-- scripts/lib/devtool/ide_plugins/__init__.py | 60 ++++++++++----------- scripts/lib/devtool/ide_plugins/ide_code.py | 23 +++++--- scripts/lib/devtool/ide_plugins/ide_none.py | 15 +++--- 4 files changed, 66 insertions(+), 51 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index 1cc9843a72..ec257f9859 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -4640,10 +4640,21 @@ class DevtoolIdeSdkClangTests(DevtoolIdeSdkTests): f"Configuration '{config_name}' should not have MIMode (CodeLLDB)") self.assertNotIn("miDebuggerPath", config, f"Configuration '{config_name}' should not have miDebuggerPath") - self.assertEqual(config["request"], "launch", - f"Configuration '{config_name}' should be launch type") - self.assertEqual(config["cwd"], "/tmp", - f"Configuration '{config_name}' cwd should be /tmp (writable on target)") + + is_attach = "_attach" in config_name + if is_attach: + self.assertEqual(config["request"], "attach", + f"Configuration '{config_name}' should be attach type") + self.assertNotIn("cwd", config, + f"Configuration '{config_name}' should not set cwd in attach mode") + self.assertIn("postDebugTask", config, + f"attach configuration '{config_name}' should have postDebugTask " + "to stop the lldb-server platform instance afterwards") + else: + self.assertEqual(config["request"], "launch", + f"Configuration '{config_name}' should be launch type") + self.assertEqual(config["cwd"], "/tmp", + f"Configuration '{config_name}' cwd should be /tmp (writable on target)") # Verify initCommands contain the platform connect sequence init_commands = config.get("initCommands", []) diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py index bb9f100652..bc0b2ca9b7 100644 --- a/scripts/lib/devtool/ide_plugins/__init__.py +++ b/scripts/lib/devtool/ide_plugins/__init__.py @@ -267,9 +267,6 @@ class LldbServerConfig(DebuggerCrossConfig): Unlike gdbserver, lldb-server platform mode is architecture-agnostic on the host side: a single lldb-native binary handles all target architectures via the LLDB platform protocol that CodeLLDB speaks natively. - - The ATTACH mode is not supported because lldb-server platform does not take a - PID argument; attaching is done client-side via 'process attach'. """ DEBUG_SERVER_NAME = "lldb-server" TARGET_START_RETRIES = 600 @@ -281,10 +278,12 @@ class LldbServerConfig(DebuggerCrossConfig): # lldb-server platform spawns a separate gdb-remote-protocol # "gdbserver" instance per debug session; without --gdbserver-port it # picks a random port, which cannot be forwarded through slirp NAT. - # Pin it to a fixed, dedicated port that gets slirp-forwarded too. - self.gdbserver_port = DebuggerCrossConfig._port_next - DebuggerCrossConfig._port_next += 1 - self.extra_ports.append(self.gdbserver_port) + # Pin a fixed, dedicated port per mode, each gets slirp-forwarded too. + self.gdbserver_ports = {} + for mode in self.server_modes(): + self.gdbserver_ports[mode] = DebuggerCrossConfig._port_next + DebuggerCrossConfig._port_next += 1 + self.extra_ports.append(self.gdbserver_ports[mode]) def _lldb_server_tmp_dir(self, mode): return os.path.join('/tmp', 'lldb_server_%s' % self.id_pretty_mode(mode)) @@ -296,35 +295,38 @@ class LldbServerConfig(DebuggerCrossConfig): return os.path.join(self._lldb_server_tmp_dir(mode), 'lldb_server.log') def _target_start_cmd(self, mode): - """SSH command to start lldb-server in platform mode on the target.""" + """SSH command to start lldb-server in platform mode on the target. + + Used identically for MULTI and ATTACH: in both cases lldb-server just + offers a platform connection, it does not care whether the client that + connects to it goes on to launch a new process or attach to an + existing one. + """ + if mode not in (DebuggerServerModes.MULTI, DebuggerServerModes.ATTACH): + raise DevtoolError("Unsupported lldb-server mode: %s" % mode) lldb_server = self.debugger_cross.debug_server_path # Use '*:' so lldb-server binds on all interfaces (0.0.0.0), not # just loopback. The bare ':' form only binds to 127.0.0.1 in # lldb-server 21.x and the remote lldb client connects from the host. # Start from /tmp because lldb-server creates temp files in its cwd and # the SSH default cwd (/home/root) may not exist on a minimal image. - if mode == DebuggerServerModes.MULTI: - pid_file = self._lldb_server_pid_file(mode) - tmp_dir = self._lldb_server_tmp_dir(mode) - log_file = self._lldb_server_log_file(mode) - cmd = self._target_tcp_port_check_cmd() + " && exit 0; " - cmd += "mkdir -p %s; " % tmp_dir - cmd += "cd %s; " % tmp_dir - cmd += "%s platform --server --listen *:%s --gdbserver-port %s > %s 2>&1 & _lldb_server_pid=\\$!; " % ( - lldb_server, self.debug_server_port, self.gdbserver_port, log_file) - cmd += "echo \\$_lldb_server_pid > %s; " % pid_file - cmd += self._target_wait_for_tcp_port_cmd( - "lldb_server_pid", log_file) - else: - raise DevtoolError( - "lldb-server only supports MULTI mode; " - "ATTACH is handled client-side with 'process attach': %s" % mode) + pid_file = self._lldb_server_pid_file(mode) + tmp_dir = self._lldb_server_tmp_dir(mode) + log_file = self._lldb_server_log_file(mode) + cmd = self._target_tcp_port_check_cmd(mode) + " && exit 0; " + cmd += "mkdir -p %s; " % tmp_dir + cmd += "cd %s; " % tmp_dir + cmd += "%s platform --server --listen *:%s --gdbserver-port %s > %s 2>&1 & _lldb_server_pid=\\$!; " % ( + lldb_server, self.port(mode), self.gdbserver_ports[mode], log_file) + cmd += "echo \\$_lldb_server_pid > %s; " % pid_file + cmd += self._target_wait_for_tcp_port_cmd( + "lldb_server_pid", log_file, mode) return "\"/bin/sh -c '" + cmd + "'\"" def _target_stop_cmd(self, server_mode): - """SSH command to stop a MULTI-mode lldb-server on the target.""" - pid_file = self._lldb_server_pid_file(DebuggerServerModes.MULTI) - tmp_dir = self._lldb_server_tmp_dir(DebuggerServerModes.MULTI) + """SSH command to stop the lldb-server platform instance for the given mode.""" + pid_file = self._lldb_server_pid_file(server_mode) + tmp_dir = self._lldb_server_tmp_dir(server_mode) cmd = ("test -f %(pf)s && kill \\$(cat %(pf)s) 2>/dev/null; rm -rf %(td)s" % {'pf': pid_file, 'td': tmp_dir}) return "\"/bin/sh -c '" + cmd + "'\"" @@ -335,10 +337,6 @@ class LldbServerConfig(DebuggerCrossConfig): def server_script(self, mode): return os.path.join(self.script_dir, self.server_script_file(mode)) - def server_modes(self): - """ATTACH mode is not applicable for lldb-server platform.""" - return [self.default_mode] - class IdeBase: """Base class defining the interface for IDE plugins""" diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py index f8036f15de..081dc08657 100644 --- a/scripts/lib/devtool/ide_plugins/ide_code.py +++ b/scripts/lib/devtool/ide_plugins/ide_code.py @@ -508,11 +508,12 @@ class IdeVSCode(IdeBase): return launch_config def _vscode_launch_bin_dbg_lldb(self, lldb_config, server_mode): - """Generate a CodeLLDB (type: lldb) launch configuration entry for launch.json. + """Generate a CodeLLDB (type: lldb) launch/attach configuration entry for launch.json. CodeLLDB connects to lldb-server via the LLDB platform protocol. The initCommands select the remote platform and open the connection before - the process is launched, so CodeLLDB can inspect and control it. + the process is launched or attached to, so CodeLLDB can inspect and + control it. Using targetCreateCommands instead of "program" so we can pass both the local host binary (for debug symbols) and the remote target path (where @@ -520,14 +521,16 @@ class IdeVSCode(IdeBase): "target create --remote-file". This prevents LLDB from uploading the binary from its module cache to a temporary directory and ensures the process starts from its installed location where the dynamic linker can - find shared libraries via the standard search paths. + find shared libraries via the standard search paths. In ATTACH mode the + same lldb-server platform connection is used. """ modified_recipe = lldb_config.modified_recipe debugger_cross = modified_recipe.debugger_cross + is_attach = server_mode == DebuggerServerModes.ATTACH init_commands = [ "platform select remote-linux", - "platform connect connect://%s:%d" % (debugger_cross.host, lldb_config.debug_server_port), + "platform connect connect://%s:%d" % (debugger_cross.host, lldb_config.port(server_mode)), # Clear the default step-avoid-regexp so std:: and other library # namespaces are not silently skipped on step-in. (default is "std::" in LLDB 15+) "settings set target.process.thread.step-avoid-regexp \"\"", @@ -592,15 +595,21 @@ class IdeVSCode(IdeBase): launch_config = { "name": lldb_config.id_pretty_mode(server_mode), "type": "lldb", - "request": "launch", + "request": "attach" if is_attach else "launch", # Use targetCreateCommands instead of "program" to control both # the local binary (for debug symbols) and the remote path. "targetCreateCommands": [target_create_cmd], - "stopOnEntry": False, - "cwd": "/tmp", "preLaunchTask": lldb_config.id_pretty_mode(server_mode), "initCommands": init_commands, } + if is_attach: + launch_config["postDebugTask"] = self._stop_task_label( + lldb_config, server_mode) + else: + # cwd configures the process the debugger launches, it is not + # part of the attach schema. + launch_config["stopOnEntry"] = False + launch_config["cwd"] = "/tmp" if source_map: launch_config["sourceMap"] = source_map if modified_recipe.b: diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py index 559940fda2..eba576bfdb 100644 --- a/scripts/lib/devtool/ide_plugins/ide_none.py +++ b/scripts/lib/devtool/ide_plugins/ide_none.py @@ -205,20 +205,15 @@ class LldbServerConfigNone(LldbServerConfig): if server_mode is None: server_mode = self.default_mode lines = ['# This file is generated by devtool ide-sdk'] - if server_mode == DebuggerServerModes.MULTI: - lines.append('# On the remote target:') - lines.append('# lldb-server platform --server --listen *:%d --gdbserver-port %d' % ( - self.debug_server_port, self.gdbserver_port)) - else: - lines.append('# On the remote target:') - lines.append('# lldb-server platform --one-shot --server --listen *:%d --gdbserver-port %d' % ( - self.debug_server_port, self.gdbserver_port)) + lines.append('# On the remote target:') + lines.append('# lldb-server platform --server --listen *:%d --gdbserver-port %d' % ( + self.port(server_mode), self.gdbserver_ports[server_mode])) lines.append('# On the build machine:') lines.append('# cd ' + self.modified_recipe.real_srctree) lines.append('# ' + self.debugger_cross.lldb + ' -s ' + self.lldbinit) lines.append('platform select remote-linux') lines.append('platform connect connect://%s:%d' % ( - self.debugger_cross.host, self.debug_server_port)) + self.debugger_cross.host, self.port(server_mode))) lines.append('settings set target.process.thread.step-avoid-regexp ""') if self.image_recipe.rootfs_dbg: @@ -267,6 +262,8 @@ class LldbServerConfigNone(LldbServerConfig): def initialize(self): self._gen_lldb_server_start_script() + if self.binary.runs_as_service and self.default_mode != DebuggerServerModes.ATTACH: + self._gen_lldb_server_start_script(DebuggerServerModes.ATTACH) self._gen_lldbinit_config() self._gen_lldb_start_script() From patchwork Wed Sep 9 21:53:14 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97784 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id A6EC0C88E49 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-64-227.siemens.flowmailer.net (mta-64-227.siemens.flowmailer.net [185.136.64.227]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.85.1788990825683465618 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=IYlCl37v; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.227, mailfrom: fm-1329275-20260909215343ca994ca35d0002071a-7o1dqy@rts-flowmailer.siemens.com) Received: by mta-64-227.siemens.flowmailer.net with ESMTPSA id 20260909215343ca994ca35d0002071a for ; Wed, 09 Sep 2026 23:53:43 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=WWARcVVKkjj/aJj5tGxhtGafJHKBJdsHhPJEpUtIY9I=; b=IYlCl37vXlVihbkme5l9Oteb4OHSvH3ZCZaGjwmypMX/smsYiz2lR8GUxoWMprycxnPCf0 V+jzVv4yxkXe0CMET1hgkFZ4yYXzTi+6nqGSqAuyIHmoBWqbCzdI+qWhZ9v3ZMzYzbDPeceG YL9TkvVA3IZWXLd7aNM7412uzeGGzNl+DvnmfjRW1zZLPD7jiEzU/WS0T+RHAYVeA7m3VSKY r1xFCtGasKA+iqKIQ4tmOxxBp/heS3Q9Uy1aV/9Q8B55ZRUzj8HJ58/ovoiJ4hl9v5+RJgaL jfqecIx4norLIJUGikdbjbahr0scJbRkEx5lrLXip14TgfLO3DdE8dmA==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 14/15] devtool: ide-sdk: pin BBPATH in generated do_install script Date: Wed, 9 Sep 2026 23:53:14 +0200 Message-ID: <20260909215337.89106-15-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245508 From: Adrian Freihofer The generated bb_run_do_install script already os.chdir()s into the recipe's build topdir before starting tinfoil, but that only covers the fallback case: bb.cookerdata.findConfigFile() checks BBPATH entries for conf/bblayers.conf before ever falling back to cwd. If BBPATH happens to still be set in the caller's environment and points at another valid build directory (e.g. a leftover from a sibling checkout), bitbake would pick up that build's config instead of the intended one. Set os.environ["BBPATH"] to the recipe's topdir alongside the chdir so the generated script doesn't depend on the caller's environment being clean. Signed-off-by: Adrian Freihofer --- scripts/lib/devtool/ide_sdk.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py index f9a92ac5ca..21442ce83b 100755 --- a/scripts/lib/devtool/ide_sdk.py +++ b/scripts/lib/devtool/ide_sdk.py @@ -1633,6 +1633,11 @@ class RecipeModified: os.path.join(self.bitbakepath, '..', 'lib')), 'import bb.tinfoil', 'os.chdir(%r)' % self.topdir, + # A stale BBPATH from the caller's environment (e.g. a + # different, still-valid build dir) would otherwise take + # precedence over cwd when bitbake looks for + # conf/bblayers.conf (see bb.cookerdata.findConfigFile). + 'os.environ["BBPATH"] = %r' % self.topdir, 'tinfoil = bb.tinfoil.Tinfoil()', 'try:', ' tinfoil.prepare(config_only=False, quiet=2)', From patchwork Wed Sep 9 21:53:15 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 97782 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 549B2C88E42 for ; Wed, 9 Sep 2026 21:53:52 +0000 (UTC) Received: from mta-65-227.siemens.flowmailer.net (mta-65-227.siemens.flowmailer.net [185.136.65.227]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.81.1788990824466403916 for ; Wed, 09 Sep 2026 14:53:46 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=NBmnCRA/; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.227, mailfrom: fm-1329275-20260909215343d9485539f600020750-cqh4w6@rts-flowmailer.siemens.com) Received: by mta-65-227.siemens.flowmailer.net with ESMTPSA id 20260909215343d9485539f600020750 for ; Wed, 09 Sep 2026 23:53:43 +0200 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=+3VPKBmLBnTNSbnibqNqcc0BpCniONJAWDyB1eh43rM=; b=NBmnCRA/1yrWAD70/tNI5YA/5aewZeX/yNzLGIlwtv6w+QTH8lzNCIcbSbAaIwKzK2JN2T LU6jjjlw2OwoFUBG6o0jFl6bmOmWqJ0zMkO8iYYWSYadhwKKDnbnMbcNfZKJCbmcdi868pzu DM58ECxibqPspvoMqPlwxcUjzZanZfTxItkxc8PRi/uBAnBqXRxNKkOdLtj3hxHGpaSXOWAJ 9VOXtoy2lsaPm5d/uu7M3j9/xOStD6WDXAKgABaYFtM4ZvYZOi3FfO878nkUbo9mFmfNPkYq +d45qRWN2Nnp0j652L3g8rflAPwnrm4sBpDP0Ci83xnireGya2MMwz/A==; From: AdrianF To: openembedded-core@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH 15/15] buildstats: close proc/disk log files via weakref.finalize safety net Date: Wed, 9 Sep 2026 23:53:15 +0200 Message-ID: <20260909215337.89106-16-adrian.freihofer@siemens.com> In-Reply-To: <20260909215337.89106-1-adrian.freihofer@siemens.com> References: <20260909215337.89106-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 09 Sep 2026 21:53:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/245517 From: Adrian Freihofer SystemStats.close() is normally invoked by the buildstats.bbclass handler on bb.event.BuildCompleted, but that event is skipped for internal, event-less task runs (e.g. tinfoil's run_prepared_task(), used by devtool ide-sdk). Without a BuildCompleted event, the open proc/disk log files were only closed implicitly at GC/interpreter-exit time, causing a ResourceWarning for leaked file handles. Register a weakref.finalize() callback at construction time as a safety net. close() now just triggers the finalizer, which is idempotent and keeps no reference back to SystemStats itself. AI-Generated: Uses GitHub Copilot Signed-off-by: Adrian Freihofer --- meta/lib/oe/buildstats.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/meta/lib/oe/buildstats.py b/meta/lib/oe/buildstats.py index db3d26afb8..56228246e4 100644 --- a/meta/lib/oe/buildstats.py +++ b/meta/lib/oe/buildstats.py @@ -10,6 +10,7 @@ import os import time import re +import weakref import bb.event from collections import deque @@ -56,6 +57,12 @@ class SystemStats: destfile = os.path.join(bsdir, '%sproc_%s.log' % ('reduced_' if handler else '', filename)) self.proc_files.append((filename, open(destfile, 'ab'), handler)) self.monitor_disk = open(os.path.join(bsdir, 'monitor_disk.log'), 'ab') + # Safety net: normally close() is called when bb.event.BuildCompleted + # fires, but that event is skipped for internal, event-less task runs + # (e.g. tinfoil's run_prepared_task()). Without this, the open files + # would only get closed implicitly at GC/interpreter-exit time, + # triggering a ResourceWarning. + self._finalizer = weakref.finalize(self, self._close_files, self.proc_files, self.monitor_disk) # Last time that we sampled /proc data resp. recorded disk monitoring data. self.last_proc = 0 self.last_disk_monitor = 0 @@ -81,8 +88,12 @@ class SystemStats: self.net_stats = {} def close(self): - self.monitor_disk.close() - for _, output, _ in self.proc_files: + self._finalizer() + + @staticmethod + def _close_files(proc_files, monitor_disk): + monitor_disk.close() + for _, output, _ in proc_files: output.close() def _reduce_meminfo(self, time, data, filename):