diff mbox series

[06/14] devtool: new ide plugin

Message ID 20230821161120.171956-7-adrian.freihofer@siemens.com
State New
Headers show
Series devtool ide plugin | expand

Commit Message

Adrian Freihofer Aug. 21, 2023, 4:07 p.m. UTC
Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 scripts/lib/devtool/ide.py | 1190 ++++++++++++++++++++++++++++++++++++
 1 file changed, 1190 insertions(+)
 create mode 100755 scripts/lib/devtool/ide.py
diff mbox series

Patch

diff --git a/scripts/lib/devtool/ide.py b/scripts/lib/devtool/ide.py
new file mode 100755
index 00000000000..7c7040232c4
--- /dev/null
+++ b/scripts/lib/devtool/ide.py
@@ -0,0 +1,1190 @@ 
+#! /usr/bin/env python3
+#
+# Copyright (C) 2023 Siemens AG
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+
+"""Devtool ide plugin"""
+
+import os
+import stat
+import logging
+import json
+import re
+import shutil
+from argparse import RawTextHelpFormatter
+from enum import IntEnum, auto
+
+import bb
+from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError, parse_recipe
+from devtool.standard import get_real_srctree
+
+SHARED_SYSROOT_RECIPES = ['none', 'meta-ide-support', 'build-sysroots']
+SUPPORTED_IDES = ['code', 'none']
+
+logger = logging.getLogger('devtool')
+
+
+class TargetDevice:
+    """SSH remote login parameters"""
+
+    def __init__(self, args):
+        self.extraoptions = ''
+        if args.no_host_check:
+            self.extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+        self.ssh_sshexec = 'ssh'
+        if args.ssh_exec:
+            self.ssh_sshexec = args.ssh_exec
+        self.ssh_port = ''
+        if args.port:
+            self.ssh_port = "-p %s" % args.port
+        if args.key:
+            self.extraoptions += ' -i %s' % args.key
+
+        self.target = args.target
+        target_sp = args.target.split('@')
+        if len(target_sp) == 1:
+            self.login = ""
+            self.host = target_sp[0]
+        elif len(target_sp) == 2:
+            self.login = target_sp[0]
+            self.host = target_sp[1]
+        else:
+            logger.error("Invalid target argument: %s" % args.target)
+
+
+class RecipeNative:
+    def __init__(self, name, target_arch=None):
+        self.name = name
+        self.target_arch = target_arch
+        self.bootstrap_tasks = [self.name + ':do_addto_recipe_sysroot']
+        self.staging_bindir_native = None
+        self.target_sys = None
+        self.__native_bin = None
+
+    def initialize(self, config, workspace, tinfoil):
+        recipe_d = parse_recipe(
+            config, tinfoil, self.name, appends=True, filter_workspace=False)
+        if not recipe_d:
+            raise DevtoolError("Parsing %s recipe failed" % self.name)
+        self.staging_bindir_native = os.path.realpath(
+            recipe_d.getVar('STAGING_BINDIR_NATIVE'))
+        self.target_sys = recipe_d.getVar('TARGET_SYS')
+
+    @property
+    def native_bin(self):
+        if not self.__native_bin:
+            raise DevtoolError("native binary name is not defined.")
+        return self.__native_bin
+
+
+class RecipeGdbCross(RecipeNative):
+    def __init__(self, args, target_arch, target_device, gdbserver_multi=True):
+        super().__init__('gdb-cross-' + target_arch, target_arch)
+        self.target_device = target_device
+        self.gdb = None
+        self.gdbserver_port_next = int(args.gdbserver_port_start)
+        self.gdbserver_multi = gdbserver_multi
+        self.config_db = {}
+
+    def initialize(self, config, workspace, tinfoil):
+        super().initialize(config, workspace, tinfoil)
+        gdb_bin = self.target_sys + '-gdb'
+        gdb_path = os.path.join(
+            self.staging_bindir_native, self.target_sys, gdb_bin)
+        self.gdb = gdb_path
+
+    @property
+    def host(self):
+        return self.target_device.host
+
+    def __gdbserver_start_cmd(self, binary, port):
+        if self.gdbserver_multi:
+            gdbserver_cmd = "/usr/bin/gdbserver --multi :%s" % (
+                port)
+        else:
+            gdbserver_cmd = "/usr/bin/gdbserver --once :%s %s" % (
+                port, binary)
+        return "%s %s %s %s 'sh -c \"%s\"'" % (
+            self.target_device.ssh_sshexec, self.target_device.ssh_port, self.target_device.extraoptions, self.target_device.target, gdbserver_cmd)
+
+    def setup_gdbserver_config(self, binary, script_dir):
+        if binary in self.config_db:
+            raise DevtoolError(
+                "gdbserver config for binary %s is already generated" % binary)
+
+        port = self.gdbserver_port_next
+        self.gdbserver_port_next += 1
+        config_entry = {
+            "port": port,
+        }
+        if script_dir:
+            cmd_lines = ['#!/bin/sh']
+            cmd_lines.append(self.__gdbserver_start_cmd(binary, port))
+            binary_name_pretty = binary.replace(os.sep, '-')
+            start_script_name = 'gdbserver_start_%d_%s' % (
+                port, binary_name_pretty)
+            if self.gdbserver_multi:
+                start_script_name += "_m"
+            start_script_file = os.path.join(script_dir, start_script_name)
+            config_entry['gdbserver_start_script'] = start_script_file
+            config_entry['pretty_id'] = 'gdbserver start %d %s' % (
+                port, binary)
+
+            bb.utils.mkdirhier(script_dir)
+            with open(start_script_file, 'w') as script_f:
+                script_f.write(os.linesep.join(cmd_lines))
+                script_f.write(os.linesep)
+            st = os.stat(start_script_file)
+            os.chmod(start_script_file, st.st_mode | stat.S_IEXEC)
+
+        self.config_db[binary] = config_entry
+        return config_entry
+
+    def get_gdbserver_start_scripts(self):
+        for conf in self.config_db.values():
+            yield (conf['pretty_id'], conf['gdbserver_start_script'])
+
+    def get_gdbserver_pretty_id(self, binary):
+        return self.config_db[binary]['pretty_id']
+
+    def get_gdbserver_port(self, binary):
+        return self.config_db[binary]['port']
+
+    def get_gdbserver_start_script(self, binary):
+        return self.config_db[binary]['gdbserver_start_script']
+
+
+class RecipeImage:
+    """Handle some image recipe related properties"""
+
+    def __init__(self, name):
+        self.combine_dbg_image = False
+        self.debuginfod = False
+        self.name = name
+        self.package_debug_split_style = None
+        self.rootfs = None
+        self.__rootfs_dbg = None
+        self.bootstrap_tasks = [self.name + ':do_build']
+
+    def initialize(self, config, tinfoil):
+        image_d = parse_recipe(
+            config, tinfoil, self.name, appends=True, filter_workspace=False)
+        if not image_d:
+            raise DevtoolError(
+                "Parsing image recipe %s failed" % self.name)
+        if 'debuginfod' in image_d.getVar('DISTRO_FEATURES').split():
+            # image_config.debuginfod = True
+            logger.warning("Support for debuginfod is not implemented yet.")
+
+        self.package_debug_split_style = image_d.getVar(
+            'PACKAGE_DEBUG_SPLIT_STYLE')
+
+        workdir = image_d.getVar('WORKDIR')
+        self.rootfs = os.path.join(workdir, 'rootfs')
+        if image_d.getVar('IMAGE_GEN_DEBUGFS') == "1":
+            self.__rootfs_dbg = os.path.join(workdir, 'rootfs-dbg')
+
+        self.combine_dbg_image = bb.data.inherits_class(
+            'image-combined-dbg', image_d)
+
+    @property
+    def debug_support(self):
+        return bool(self.debuginfod or self.rootfs_dbg)
+
+    @property
+    def rootfs_dbg(self):
+        if self.__rootfs_dbg and os.path.isdir(self.__rootfs_dbg):
+            return self.__rootfs_dbg
+        return None
+
+    def solib_search_path_rootfs(self):
+        """Search for folders with shared libraries in the rootfs and rootfs-dbg
+
+        This is based on the assumption that the PACKAGE_DEBUG_SPLIT_STYLE variable from the image
+        is the global setting which is used by most packages. Even if this variable does not seem
+        to make sense in the image context.
+        """
+        rootfs_solib_search_path = []
+        rootfs_dbg_solib_search_path = []
+        if self.package_debug_split_style in ['debug-with-srcpkg', '.debug']:
+            if self.combine_dbg_image:
+                rootfs_dbg_solib_search_path = [
+                    "/lib", "/lib/.debug", "/usr/lib", "/usr/lib/.debug"]
+            else:
+                logger.warn(
+                    'Adding IMAGE_CLASSES += "image-combined-dbg" offers better remote debugging experience.')
+                rootfs_solib_search_path = [
+                    "/lib", "/usr/lib"]
+                rootfs_dbg_solib_search_path = [
+                    "/lib/.debug", "/usr/lib/.debug"]
+        elif self.package_debug_split_style == 'debug-file-directory':
+            rootfs_dbg_solib_search_path = ["/usr/lib/debug"]
+        else:
+            logger.warning(
+                "Cannot find solib search path for a rootfs built with PACKAGE_DEBUG_SPLIT_STYLE=%s." % self.package_debug_split_style)
+
+        sym_dirs = []
+        for dbgdir in rootfs_solib_search_path:
+            sym_dirs.append(os.path.join(
+                self.rootfs, dbgdir.lstrip('/')))
+        for dbgdir in rootfs_dbg_solib_search_path:
+            sym_dirs.append(os.path.join(
+                self.rootfs_dbg, dbgdir.lstrip('/')))
+
+        return sym_dirs
+
+
+class RecipeMetaIdeSupport:
+    """Handle some meta-ide-support recipe related properties"""
+
+    def __init__(self):
+        self.bootstrap_tasks = ['meta-ide-support:do_build']
+        self.topdir = None
+        self.datadir = None
+        self.deploy_dir_image = None
+        self.build_sys = None
+        # From toolchain-scripts
+        self.real_multimach_target_sys = None
+
+    def initialize(self, config, tinfoil):
+        meta_ide_support_d = parse_recipe(
+            config, tinfoil, 'meta-ide-support', appends=True, filter_workspace=False)
+        if not meta_ide_support_d:
+            raise DevtoolError("Parsing meta-ide-support recipe failed")
+
+        self.topdir = meta_ide_support_d.getVar('TOPDIR')
+        self.datadir = meta_ide_support_d.getVar('datadir')
+        self.deploy_dir_image = meta_ide_support_d.getVar(
+            'DEPLOY_DIR_IMAGE')
+        self.build_sys = meta_ide_support_d.getVar('BUILD_SYS')
+        self.real_multimach_target_sys = meta_ide_support_d.getVar(
+            'REAL_MULTIMACH_TARGET_SYS')
+
+
+class RecipeBuildSysroots:
+    def __init__(self):
+        self.standalone_sysroot = None
+        self.standalone_sysroot_native = None
+        self.bootstrap_tasks = ['build-sysroots:do_build']
+
+    def initialize(self, config, tinfoil):
+        build_sysroots_d = parse_recipe(
+            config, tinfoil, 'build-sysroots', appends=True, filter_workspace=False)
+        if not build_sysroots_d:
+            raise DevtoolError("Parsing build-sysroots recipe failed")
+        self.standalone_sysroot = build_sysroots_d.getVar(
+            'STANDALONE_SYSROOT')
+        self.standalone_sysroot_native = build_sysroots_d.getVar(
+            'STANDALONE_SYSROOT_NATIVE')
+
+
+class SharedSysrootsEnv:
+    def __init__(self):
+        self.ide_support = None
+        self.build_sysroots = None
+
+    def initialize(self, ide_support, build_sysroots):
+        self.ide_support = ide_support
+        self.build_sysroots = build_sysroots
+
+    def __vscode_update_kits(self):
+        """Expose the toolchain of the dSDK"""
+        datadir = self.ide_support.datadir
+        deploy_dir_image = self.ide_support.deploy_dir_image
+        real_multimach_target_sys = self.ide_support.real_multimach_target_sys
+        standalone_sysroot_native = self.build_sysroots.standalone_sysroot_native
+        vscode_ws_path = os.path.join(
+            os.environ['HOME'], '.local', 'share', 'CMakeTools')
+        cmake_kits_path = os.path.join(vscode_ws_path, 'cmake-tools-kits.json')
+        oecmake_generator = "Ninja"
+        env_script = os.path.join(
+            deploy_dir_image, 'environment-setup-' + real_multimach_target_sys)
+
+        logger.info("updating %s" % cmake_kits_path)
+
+        if not os.path.isdir(vscode_ws_path):
+            os.makedirs(vscode_ws_path)
+        cmake_kits_old = []
+        if os.path.exists(cmake_kits_path):
+            with open(cmake_kits_path, 'r', encoding='utf-8') as cmake_kits_file:
+                cmake_kits_old = json.load(cmake_kits_file)
+        cmake_kits = cmake_kits_old.copy()
+
+        cmake_kit_new = {
+            "name": "OE " + real_multimach_target_sys,
+            "environmentSetupScript": env_script,
+            "toolchainFile": standalone_sysroot_native + datadir + "/cmake/OEToolchainConfig.cmake",
+            "preferredGenerator": {
+                "name": oecmake_generator
+            }
+        }
+
+        def merge_kit(cmake_kits, cmake_kit_new):
+            i = 0
+            while i < len(cmake_kits):
+                if 'environmentSetupScript' in cmake_kits[i] and \
+                        cmake_kits[i]['environmentSetupScript'] == cmake_kit_new['environmentSetupScript']:
+                    cmake_kits[i] = cmake_kit_new
+                    return
+                i += 1
+            cmake_kits.append(cmake_kit_new)
+        merge_kit(cmake_kits, cmake_kit_new)
+
+        if cmake_kits != cmake_kits_old:
+            bb.note("Updating: %s" % cmake_kits_path)
+            with open(cmake_kits_path, 'w', encoding='utf-8') as cmake_kits_file:
+                json.dump(cmake_kits, cmake_kits_file, indent=4)
+
+    def setup_ide(self, args):
+        if args.ide == 'code':
+            self.__vscode_update_kits()
+
+
+class BuildTool(IntEnum):
+    UNDEFINED = auto()
+    CMAKE = auto()
+    MESON = auto()
+
+
+class RecipeModified:
+
+    def __init__(self, name):
+        self.name = name
+        self.bootstrap_tasks = [name + ':do_install']
+        # workspace
+        self.real_srctree = None
+        self.srctree = None
+        self.temp_dir = None
+        self.bbappend = None
+        # recipe variables from d.getVar
+        self.b = None
+        self.bpn = None
+        self.d = None
+        self.fakerootcmd = None
+        self.fakerootenv = None
+        self.package_arch = None
+        self.path = None
+        self.recipe_sysroot = None
+        self.recipe_sysroot_native = None
+        self.staging_incdir = None
+        self.target_arch = None
+        self.workdir = None
+        # replicate bitbake build environment
+        self.__exported_vars = None
+        self.cmd_compile = None
+        # main build tool used by this recipe
+        self.build_tool = BuildTool.UNDEFINED
+        # build_tool = cmake
+        self.oecmake_generator = None
+        self.__cmake_cache_vars = None
+        # build_tool = meson
+        self.meson_buildtype = None
+        self.meson_wrapper = None
+        self.mesonopts = None
+        self.extra_oemeson = None
+        self.meson_cross_file = None
+        # vscode
+        self.dot_code_dir = None
+        # TODO: remove calling devtool
+        self.bb_env_passthrough_additions = None
+        self.bbpath = None
+        self.bitbakepath = None
+        self.topdir = None
+
+    def initialize(self, config, workspace, tinfoil):
+        recipe_d = parse_recipe(
+            config, tinfoil, self.name, appends=True, filter_workspace=False)
+        if not recipe_d:
+            raise DevtoolError("Parsing %s recipe failed" % self.name)
+
+        # Verify this recipe is built as externalsrc setup by devtool modify
+        workspacepn = check_workspace_recipe(
+            workspace, self.name, bbclassextend=True)
+        self.srctree = workspace[workspacepn]['srctree']
+        # Need to grab this here in case the source is within a subdirectory
+        self.real_srctree = get_real_srctree(
+            self.srctree, recipe_d.getVar('S'), recipe_d.getVar('WORKDIR'))
+        self.bbappend = workspace[workspacepn]['bbappend']
+
+        self.temp_dir = os.path.join(config.workspace_path, 'temp', self.name)
+        if os.path.exists(self.temp_dir):
+            shutil.rmtree(self.temp_dir)
+
+        self.b = recipe_d.getVar('B')
+        self.bpn = recipe_d.getVar('BPN')
+        self.d = recipe_d.getVar('D')
+        self.fakerootcmd = recipe_d.getVar('FAKEROOTCMD')
+        self.fakerootenv = recipe_d.getVar('FAKEROOTENV')
+        self.package_arch = recipe_d.getVar('PACKAGE_ARCH')
+        self.path = recipe_d.getVar('PATH')
+        self.recipe_sysroot = os.path.realpath(
+            recipe_d.getVar('RECIPE_SYSROOT'))
+        self.recipe_sysroot_native = os.path.realpath(
+            recipe_d.getVar('RECIPE_SYSROOT_NATIVE'))
+        self.staging_incdir = os.path.realpath(
+            recipe_d.getVar('STAGING_INCDIR'))
+        self.target_arch = recipe_d.getVar('TARGET_ARCH')
+        self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR'))
+
+        self.bb_env_passthrough_additions = recipe_d.getVar(
+            'BB_ENV_PASSTHROUGH_ADDITIONS')
+        self.bbpath = recipe_d.getVar('BBPATH')
+        self.bitbakepath = recipe_d.getVar('BITBAKEPATH')
+        self.topdir = recipe_d.getVar('TOPDIR')
+
+        self.__init_exported_variables(recipe_d)
+
+        if bb.data.inherits_class('cmake', recipe_d):
+            self.oecmake_generator = recipe_d.getVar('OECMAKE_GENERATOR')
+            self.__init_cmake_preset_cache(recipe_d)
+            self.build_tool = BuildTool.CMAKE
+        elif bb.data.inherits_class('meson', recipe_d):
+            self.meson_buildtype = recipe_d.getVar('MESON_BUILDTYPE')
+            self.mesonopts = recipe_d.getVar('MESONOPTS')
+            self.extra_oemeson = recipe_d.getVar('EXTRA_OEMESON')
+            self.meson_cross_file = recipe_d.getVar('MESON_CROSS_FILE')
+            self.build_tool = BuildTool.MESON
+
+        self.dot_code_dir = os.path.join(self.srctree, '.vscode')
+
+    def append_to_bbappend(self, append_text):
+        with open(self.bbappend, 'a') as bbap:
+            bbap.write(append_text)
+
+    def remove_from_bbappend(self, append_text):
+        with open(self.bbappend, 'r') as bbap:
+            text = bbap.read()
+        new_text = text.replace(append_text, '')
+        with open(self.bbappend, 'w') as bbap:
+            bbap.write(new_text)
+
+    def debug_build_config(self, args):
+        """Explicitely set for example CMAKE_BUILD_TYPE to Debug if not defined otherwise"""
+        if self.build_tool == BuildTool.CMAKE:
+            append_text = os.linesep + \
+                'OECMAKE_ARGS:append = " -DCMAKE_BUILD_TYPE:STRING=Debug"' + os.linesep
+            if args.debug_build_config and not 'CMAKE_BUILD_TYPE' in self.__cmake_cache_vars:
+                self.__cmake_cache_vars['CMAKE_BUILD_TYPE'] = {
+                    "type": "STRING",
+                    "value": "Debug",
+                }
+                self.append_to_bbappend(append_text)
+            elif 'CMAKE_BUILD_TYPE' in self.__cmake_cache_vars:
+                del self.__cmake_cache_vars['CMAKE_BUILD_TYPE']
+                self.remove_from_bbappend(append_text)
+        elif self.build_tool == BuildTool.MESON:
+            append_text = os.linesep + 'MESON_BUILDTYPE = "debug"' + os.linesep
+            if args.debug_build_config and self.meson_buildtype != "debug":
+                self.mesonopts.replace(
+                    '--buildtype ' + self.meson_buildtype, '--buildtype debug')
+                self.append_to_bbappend(append_text)
+            elif self.meson_buildtype == "debug":
+                self.mesonopts.replace(
+                    '--buildtype debug', '--buildtype plain')
+                self.remove_from_bbappend(append_text)
+        elif args.debug_build_config:
+            logger.warn(
+                "--debug-build-config is not implemented for this build tool yet.")
+
+    def solib_search_path_sysroot(self):
+        return [os.path.join(self.recipe_sysroot, p) for p in ['lib', 'usr/lib']]
+
+    def solib_search_path(self, image):
+        return image.solib_search_path_rootfs() + self.solib_search_path_sysroot()
+
+    def solib_search_path_str(self, image):
+        return ':'.join(self.solib_search_path(image))
+
+    def __init_exported_variables(self, d):
+        """Find all variables with export flag set."""
+        exported_vars = {}
+
+        vars = (key for key in d.keys() if not key.startswith(
+            "__") and not d.getVarFlag(key, "func", False))
+        for var in vars:
+            func = d.getVarFlag(var, "func", False)
+            if d.getVarFlag(var, 'python', False) and func:
+                continue
+            export = d.getVarFlag(var, "export", False)
+            unexport = d.getVarFlag(var, "unexport", False)
+            if not export and not unexport and not func:
+                continue
+            if unexport:
+                continue
+
+            val = d.getVar(var)
+            if val is None:
+                continue
+            if set(var) & set("-.{}+"):
+                logger.warn(
+                    "Warning: Found invalid character in variable name %s", str(var))
+                continue
+            varExpanded = d.expand(var)
+            val = str(val)
+
+            if varExpanded.startswith("BASH_FUNC_"):
+                varExpanded = varExpanded[10:-2]
+                val = val[3:]  # Strip off "() "
+                logger.warn("Warning: BASH_FUNC_ is not exported to cmake presets (%s() %s)" % (
+                    varExpanded, val))
+                continue
+
+            if func:
+                code_line = "line: {0}, file: {1}\n".format(
+                    d.getVarFlag(var, "lineno", False),
+                    d.getVarFlag(var, "filename", False))
+                val = val.rstrip('\n')
+                logger.warn("Warning: exported shell function %s() is not exported (%s)" %
+                            (varExpanded, code_line))
+                continue
+
+            if export:
+                exported_vars[varExpanded] = val.strip()
+                continue
+
+        self.__exported_vars = exported_vars
+
+    def __init_cmake_preset_cache(self, d):
+        """Replicate the cmake configure arguments with all details to share on build folder between bitbake and SDK."""
+        site_file = os.path.join(self.workdir, 'site-file.cmake')
+        if os.path.exists(site_file):
+            print("Warning: site-file.cmake is not supported")
+
+        cache_vars = {}
+        oecmake_args = d.getVar('OECMAKE_ARGS').split()
+        extra_oecmake = d.getVar('EXTRA_OECMAKE').split()
+        for param in oecmake_args + extra_oecmake:
+            d_pref = "-D"
+            if param.startswith(d_pref):
+                param = param[len(d_pref):]
+            else:
+                print("Error: expected a -D")
+            param_s = param.split('=', 1)
+            param_nt = param_s[0].split(':', 1)
+
+            def handle_undefined_variable(var):
+                if var.startswith('${') and var.endswith('}'):
+                    return ''
+                else:
+                    return var
+            # Example: FOO=ON
+            if len(param_nt) == 1:
+                cache_vars[param_s[0]] = handle_undefined_variable(param_s[1])
+            # Example: FOO:PATH=/tmp
+            elif len(param_nt) == 2:
+                cache_vars[param_nt[0]] = {
+                    "type": param_nt[1],
+                    "value": handle_undefined_variable(param_s[1]),
+                }
+            else:
+                print("Error: cannot parse %s" % param)
+        self.__cmake_cache_vars = cache_vars
+
+    def __cmake_preset(self):
+        toolchain_file = os.path.join(self.workdir, 'toolchain.cmake')
+        preset_name = self.bpn + "-" + self.package_arch
+        preset_display_name = self.bpn + ": " + self.package_arch
+        cmake_executable = os.path.join(
+            self.recipe_sysroot_native, 'usr', 'bin', 'cmake')
+        self.cmd_compile = cmake_executable + " --build --preset " + preset_name
+
+        preset_dict_configure = {
+            "name": preset_name,
+            "displayName": preset_display_name,
+            "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
+            "binaryDir": self.b,
+            "generator": self.oecmake_generator,
+            "toolchainFile": toolchain_file,
+            "cacheVariables": self.__cmake_cache_vars,
+            "environment": self.__exported_vars,
+            "cmakeExecutable": cmake_executable
+        }
+
+        preset_dict_build = {
+            "name": preset_name,
+            "displayName": preset_display_name,
+            "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
+            "configurePreset": preset_name,
+            "inheritConfigureEnvironment": True
+        }
+
+        preset_dict_test = {
+            "name": preset_name,
+            "displayName": preset_display_name,
+            "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
+            "configurePreset": preset_name,
+            "inheritConfigureEnvironment": True
+        }
+
+        preset_dict = {
+            "version": 3,  # cmake 3.21, backward compatible with kirkstone
+            "configurePresets": [preset_dict_configure],
+            "buildPresets": [preset_dict_build],
+            "testPresets": [preset_dict_test]
+        }
+
+        logger.info("generating cmake preset for recipe %s" % self.bpn)
+        preset_file = os.path.join(self.real_srctree, 'CMakeUserPresets.json')
+        with open(preset_file, 'w') as outfile:
+            json.dump(preset_dict, outfile, indent=4)
+
+    @staticmethod
+    def update_json_file(dot_code_dir, json_file, update_dict):
+        json_path = os.path.join(dot_code_dir, json_file)
+        logger.info("Updating vscode %s (%s)" % (json_file, json_path))
+        if not os.path.exists(dot_code_dir):
+            os.makedirs(dot_code_dir)
+        try:
+            with open(json_path) as f:
+                orig_dict = json.load(f)
+        except json.decoder.JSONDecodeError:
+            logger.info(
+                "Decoding %s failed. Probably because of comments in the json file" % json_path)
+            orig_dict = {}
+        except FileNotFoundError:
+            orig_dict = {}
+        orig_dict.update(update_dict)
+        with open(json_path, 'w') as f:
+            json.dump(orig_dict, f, indent=4)
+
+    def __vscode_settings_cmake(self, settings_dict):
+        """Add cmake specific settings to settings.json.
+
+        Note: most settings are passed to the cmake preset.
+        """
+        if self.build_tool != BuildTool.CMAKE:
+            return
+        settings_dict["cmake.configureOnOpen"] = False
+        settings_dict["cmake.sourceDirectory"] = self.real_srctree
+
+    def __gen_meson_wrapper(self):
+        """Generate a wrapper script to call meson with cross environment"""
+        bb.utils.mkdirhier(self.temp_dir)
+        meson_wrapper = os.path.join(self.temp_dir, 'meson')
+        meson_real = os.path.join(
+            self.recipe_sysroot_native, 'usr', 'bin', 'meson.real')
+        with open(meson_wrapper, 'w') as mwrap:
+            mwrap.write("#!/bin/sh" + os.linesep)
+            for var, val in self.__exported_vars.items():
+                mwrap.write('export %s="%s"' % (var, val) + os.linesep)
+            mwrap.write("unset CC CXX CPP LD AR NM STRIP" + os.linesep)
+            private_temp = os.path.join(self.b, "meson-private", "tmp")
+            mwrap.write('mkdir -p "%s"' % private_temp + os.linesep)
+            mwrap.write('export TMPDIR="%s"' % private_temp + os.linesep)
+            mwrap.write('exec "%s" "$@"' % meson_real + os.linesep)
+        st = os.stat(meson_wrapper)
+        os.chmod(meson_wrapper, st.st_mode | stat.S_IEXEC)
+        self.meson_wrapper = meson_wrapper
+        self.cmd_compile = meson_wrapper + " compile -C " + self.b
+
+    def __vscode_settings_meson(self, settings_dict):
+        if self.build_tool != BuildTool.MESON:
+            return
+        settings_dict["mesonbuild.mesonPath"] = self.meson_wrapper
+
+        confopts = self.mesonopts.split()
+        confopts += self.meson_cross_file.split()
+        confopts += self.extra_oemeson.split()
+        settings_dict["mesonbuild.configureOptions"] = confopts
+        settings_dict["mesonbuild.buildFolder"] = self.b
+
+    def vscode_settings(self):
+        files_excludes = {
+            "**/.git/**": True,
+            "**/oe-local-files/**": True,
+            "**/oe-logs/**": True,
+            "**/oe-workdir/**": True,
+            "**/source-date-epoch/**": True
+        }
+        python_exclude = [
+            "**/.git/**",
+            "**/oe-local-files/**",
+            "**/oe-logs/**",
+            "**/oe-workdir/**",
+            "**/source-date-epoch/**"
+        ]
+        settings_dict = {
+            "files.watcherExclude": files_excludes,
+            "files.exclude": files_excludes,
+            "python.analysis.exclude": python_exclude
+        }
+        self.__vscode_settings_cmake(settings_dict)
+        self.__vscode_settings_meson(settings_dict)
+
+        settings_file = 'settings.json'
+        RecipeModified.update_json_file(
+            self.dot_code_dir, settings_file, settings_dict)
+
+    def __vscode_extensions_cmake(self, recommendations):
+        if self.build_tool != BuildTool.CMAKE:
+            return
+        recommendations += [
+            "twxs.cmake",
+            "ms-vscode.cmake-tools",
+            "ms-vscode.cpptools",
+            "ms-vscode.cpptools-extension-pack",
+            "ms-vscode.cpptools-themes"
+        ]
+
+    def __vscode_extensions_meson(self, recommendations):
+        if self.build_tool != BuildTool.MESON:
+            return
+        recommendations += [
+            'mesonbuild.mesonbuild'
+        ]
+
+    def vscode_extensions(self):
+        recommendations = [
+            "EditorConfig.EditorConfig"
+        ]
+        self.__vscode_extensions_cmake(recommendations)
+        self.__vscode_extensions_meson(recommendations)
+        extensions_file = 'extensions.json'
+        RecipeModified.update_json_file(
+            self.dot_code_dir, extensions_file, {"recommendations": recommendations})
+
+    def which(self, executable):
+        bin_path = shutil.which(executable, path=self.path)
+        if not bin_path:
+            raise DevtoolError(
+                'Cannot find %s. Probably the recipe %s is not built yet.' % (executable, self.bpn))
+        return bin_path
+
+    @staticmethod
+    def vscode_intelli_sense_mode(compiler):
+        unknown = False
+        linux = ''
+        if 'linux' in compiler:
+            linux = "linux-"
+
+        cxx = 'unknown-'
+        if 'g++' in compiler:
+            cxx = 'gcc-'
+        elif 'clang' in compiler:
+            cxx = 'clang-'
+        else:
+            unknown = True
+
+        arch = 'unknown'
+        if 'aarch64' in compiler:
+            arch = 'arm64'
+        elif 'arm' in compiler:
+            arch = 'arm'
+        elif 'x86_64' in compiler:
+            arch = 'x64'
+        elif 'i386' in compiler:
+            arch = 'x86'
+        else:
+            unknown = True
+
+        mode = linux + cxx + arch
+        if unknown:
+            logger.warn(
+                "Cannot guess intelliSenseMode for compiler %s (%s)" % (compiler, mode))
+            return '${default}'
+        return mode
+
+    def vscode_c_cpp_properties(self):
+        properties_dict = {
+            "name": "Yocto " + self.package_arch,
+        }
+        if self.build_tool == BuildTool.CMAKE:
+            properties_dict["configurationProvider"] = "ms-vscode.cmake-tools"
+        elif self.build_tool == BuildTool.MESON:
+            properties_dict["configurationProvider"] = "mesonbuild.mesonbuild"
+
+        properties_dicts = {
+            "configurations": [
+                properties_dict
+            ],
+            "version": 4
+        }
+        prop_file = 'c_cpp_properties.json'
+        dot_code_dir = os.path.join(self.srctree, '.vscode')
+        RecipeModified.update_json_file(
+            dot_code_dir, prop_file, properties_dicts)
+
+    def vscode_launch_cppdbg(self, image, gdb_cross, binary):
+        gdb_cross.setup_gdbserver_config(binary, self.temp_dir)
+        pretty_id = gdb_cross.get_gdbserver_pretty_id(binary)
+        gdbserver_port = gdb_cross.get_gdbserver_port(binary)
+
+        launch_config = {
+            "name": pretty_id,
+            "type": "cppdbg",
+            "request": "launch",
+            "program": os.path.join(self.d, binary.lstrip('/')),
+            "stopAtEntry": True,
+            "cwd": "${workspaceFolder}",
+            "environment": [],
+            "externalConsole": False,
+            "MIMode": "gdb",
+            "preLaunchTask": pretty_id,
+            "miDebuggerPath": gdb_cross.gdb,
+            "miDebuggerServerAddress": "%s:%d" % (gdb_cross.host, gdbserver_port)
+        }
+
+        # Search for header files in recipe-sysroot.
+        src_file_map = {
+            "/usr/include": os.path.join(self.recipe_sysroot, "usr", "include")
+        }
+        # First of all search for not stripped binaries in the image folder.
+        # These binaries are copied (and optionally stripped) by deploy-target
+        setup_commands = [
+            {
+                "description": "sysroot",
+                "text": "set sysroot " + self.d
+            }
+        ]
+
+        if image.rootfs_dbg:
+            launch_config['additionalSOLibSearchPath'] = self.solib_search_path_str(
+                image)
+            src_file_map["/usr/src/debug"] = os.path.join(
+                image.rootfs_dbg, "usr", "src", "debug")
+        else:
+            logger.warning(
+                "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
+
+        launch_config['sourceFileMap'] = src_file_map
+        launch_config['setupCommands'] = setup_commands
+        return launch_config
+
+    @staticmethod
+    def is_elf_file(file_path):
+        with open(file_path, "rb") as f:
+            data = f.read(4)
+        if data == b'\x7fELF':
+            return True
+        return False
+
+    def find_installed_binaries(self):
+        """find all executable elf files in the image directory"""
+        binaries = []
+        d_len = len(self.d)
+        re_so = re.compile('.*\.so[.0-9]*$')
+        for root, _, files in os.walk(self.d, followlinks=False):
+            for file in files:
+                if os.path.islink(file):
+                    continue
+                if re_so.match(file):
+                    continue
+                abs_name = os.path.join(root, file)
+                if os.access(abs_name, os.X_OK) and RecipeModified.is_elf_file(abs_name):
+                    binaries.append(abs_name[d_len:])
+        return binaries
+
+    def vscode_launch(self, image, gdb_cross):
+        binaries = self.find_installed_binaries()
+        configurations = [self.vscode_launch_cppdbg(
+            image, gdb_cross, binary) for binary in binaries]
+        launch_dict = {
+            "version": "0.2.0",
+            "configurations": configurations
+        }
+        launch_file = 'launch.json'
+        RecipeModified.update_json_file(
+            self.dot_code_dir, launch_file, launch_dict)
+
+    @staticmethod
+    def get_unique_gdbinit_name(binary):
+        return 'gdbinit' + binary.replace(os.sep, '-')
+
+    def none_launch(self, image, gdb_cross):
+        """generate a gdbinit file per executable"""
+        binaries = self.find_installed_binaries()
+        for binary in binaries:
+            gdb_cross.setup_gdbserver_config(binary, self.temp_dir)
+            gdbserver_port = str(gdb_cross.get_gdbserver_port(binary))
+            if gdb_cross.gdbserver_multi:
+                target_help = '#   gdbserver --multi :' + gdbserver_port
+                remote_cmd = 'target extended-remote '
+            else:
+                target_help = '#   gdbserver :' + gdbserver_port + ' ' + binary
+                remote_cmd = 'target remote '
+            gdbinit_path = os.path.join(
+                self.real_srctree, RecipeModified.get_unique_gdbinit_name(binary))
+
+            gdbinit_lines = ['# This file is generated by devtool ide']
+            gdbinit_lines.append('# On the remote target:')
+            gdbinit_lines.append(target_help)
+            gdbinit_lines.append('# On the build machine:')
+            gdbinit_lines.append('#   cd ' + self.real_srctree)
+            gdbinit_lines.append(
+                '#   ' + gdb_cross.gdb + ' -ix ' + gdbinit_path)
+
+            gdbinit_lines.append('set sysroot ' + self.d)
+            gdbinit_lines.append('set substitute-path "/usr/include" "' +
+                                 os.path.join(self.recipe_sysroot, 'usr', 'include') + '"')
+            if image.debuginfod:
+                gdbinit_lines.append('set debuginfod enabled on')
+            else:
+                gdbinit_lines.append('set debuginfod enabled off')
+                if image.rootfs_dbg:
+                    gdbinit_lines.append(
+                        'set solib-search-path "' + self.solib_search_path_str(image) + '"')
+                    gdbinit_lines.append('set substitute-path "/usr/src/debug" "' + os.path.join(
+                        image.rootfs_dbg, 'usr', 'src', 'debug') + '"')
+            gdbinit_lines.append(
+                remote_cmd + gdb_cross.host + ':' + gdbserver_port)
+            gdbinit_lines.append('set remote exec-file ' + binary)
+            gdbinit_lines.append('run ' + os.path.join(self.d, binary))
+
+            with open(gdbinit_path, 'w') as gdbinit_file:
+                gdbinit_file.write('\n'.join(gdbinit_lines))
+
+    def gen_fakeroot_install_script(self):
+        """Run the run.do_install script from bitbake under pseudo so that it picks up the appropriate file permissions"""
+        cmd_lines = ['#!/bin/sh']
+        # Ensure the do compile step gets always executed without pseuso before do install
+        if self.cmd_compile:
+            cmd_compile = "( cd %s && %s)" % (
+                self.real_srctree, self.cmd_compile)
+            cmd_lines.append(cmd_compile)
+        if not os.access(self.fakerootcmd, os.X_OK):
+            raise DevtoolError(
+                "pseudo executable %s could not be found" % self.fakerootcmd)
+        run_do_install = os.path.join(self.workdir, 'temp', 'run.do_install')
+
+        if not os.access(run_do_install, os.X_OK):
+            raise DevtoolError(
+                "run script does not exists: %s" % run_do_install)
+
+        # Set up the appropriate environment
+        newenv = dict(os.environ)
+        for varvalue in self.fakerootenv.split():
+            if '=' in varvalue:
+                splitval = varvalue.split('=', 1)
+                newenv[splitval[0]] = splitval[1]
+
+        # Cleanup TMPDIR before calling do_install independently from bitbake
+        # This is anyway outdated after do_install has been executed.
+        # But maybe there should be a cleaner solution here.
+        rm_in_workdir = ' '.join([os.path.join(self.workdir, d) for d in [
+                                 "package", "packages-split", "pkgdata", "sstate-install-package", "debugsources.list", "*.spec"]])
+        install_cmd = '%s /bin/sh -c "rm -rf %s/* %s && %s"' % (
+            self.fakerootcmd, self.d, rm_in_workdir, run_do_install)
+        for var, val in newenv.items():
+            cmd_lines.append('export %s="%s"' % (var, val))
+        cmd_lines.append(install_cmd)
+        return self.write_script(cmd_lines, 'bb_run_do_install')
+
+    def write_script(self, cmd_lines, script_name):
+        bb.utils.mkdirhier(self.temp_dir)
+        script_file = os.path.join(self.temp_dir, script_name)
+        with open(script_file, 'w') as script_f:
+            script_f.write(os.linesep.join(cmd_lines))
+        st = os.stat(script_file)
+        os.chmod(script_file, st.st_mode | stat.S_IEXEC)
+        return script_file
+
+    def vscode_tasks(self, args, gdb_cross):
+        run_do_install = self.gen_fakeroot_install_script()
+        pythonpath_new = os.path.realpath(
+            os.path.join(self.bitbakepath, '../lib'))
+        try:
+            pythonpath = os.environ['PYTONPATH'].split(':')
+            if pythonpath_new not in pythonpath:
+                pythonpath = pythonpath_new + ":" + pythonpath
+        except KeyError:
+            pythonpath = pythonpath_new
+        install_label = "do_install %s" % self.bpn
+        deploy_label = "devtool deploy-target %s" % self.bpn
+        bb_env = {
+            # "BBPATH": self.bbpath,
+            "PYTHONPATH": pythonpath,
+            "BUILDDIR": self.topdir,
+            # Use e.g. Python from host, not python-native
+            "PATH": os.environ['PATH'],
+            "BB_ENV_PASSTHROUGH_ADDITIONS": self.bb_env_passthrough_additions
+        }
+        bb_options = {
+            "cwd": self.topdir
+        }
+        tasks_dict = {
+            "version": "2.0.0",
+            "tasks": [
+                {
+                    "label": install_label,
+                    "type": "shell",
+                    "command": run_do_install,
+                    "problemMatcher": []
+                },
+                {
+                    "label": deploy_label,
+                    "type": "shell",
+                    "command": "devtool",  # TODO: Generate a self contained script which does what devtool target-deploy does but without connection to the bitbake server
+                    "args": ["--bbpath", self.bbpath, "deploy-target", self.bpn, args.target, "--strip", "--no-host-check"],
+                    "linux": {
+                        "options": {
+                            "env": bb_env
+                        }},
+                    "options": bb_options,
+                    "problemMatcher": []
+                },
+                {
+                    "label": "install && deploy-target %s" % self.bpn,
+                    "dependsOrder": "sequence",
+                    "dependsOn": [
+                        install_label,
+                        deploy_label
+                    ],
+                    "problemMatcher": [],
+                    "group": {
+                        "kind": "build",
+                        "isDefault": True
+                    }
+                }
+            ]
+        }
+        for pretty_id, start_script in gdb_cross.get_gdbserver_start_scripts():
+            tasks_dict['tasks'].append(
+                {
+                    "label": pretty_id,
+                    "type": "shell",
+                    "isBackground": True,
+                    "command": start_script,
+                    "problemMatcher": [
+                        {
+                            "pattern": [
+                                {
+                                    "regexp": ".",
+                                    "file": 1,
+                                    "location": 2,
+                                    "message": 3
+                                }
+                            ],
+                            "background": {
+                                "activeOnStart": True,
+                                "beginsPattern": ".",
+                                "endsPattern": ".",
+                            }
+                        }
+                    ]
+                })
+        tasks_file = 'tasks.json'
+        RecipeModified.update_json_file(
+            self.dot_code_dir, tasks_file, tasks_dict)
+
+    def setup_ide(self, args, image, gdb_cross):
+        if self.build_tool == BuildTool.CMAKE:
+            self.__cmake_preset()
+        if self.build_tool == BuildTool.MESON:
+            self.__gen_meson_wrapper()
+
+        if args.ide == 'code':
+            self.vscode_settings()
+            self.vscode_extensions()
+            self.vscode_c_cpp_properties()
+            if args.target:
+                self.vscode_launch(image, gdb_cross)
+                self.vscode_tasks(args, gdb_cross)
+        if args.ide == 'none' and self.build_tool == BuildTool.CMAKE:
+            self.none_launch(image, gdb_cross)
+
+
+def ide_setup(args, config, basepath, workspace):
+    bootstap_tasks = []
+
+    tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
+    try:
+        # Provide a rootfs and the corresponding debug symbols via rootfs-dbg or debuginfod
+        image_config = RecipeImage(args.image)
+        image_config.initialize(config, tinfoil)
+        bootstap_tasks += image_config.bootstrap_tasks
+
+        target_device = TargetDevice(args)
+
+        sdk_env = None
+        gdb_cross = None
+        if args.recipename in SHARED_SYSROOT_RECIPES:
+            ide_support = RecipeMetaIdeSupport()
+            ide_support.initialize(config, tinfoil)
+            bootstap_tasks += ide_support.bootstrap_tasks
+
+            build_sysroots = RecipeBuildSysroots()
+            build_sysroots.initialize(config, tinfoil)
+            bootstap_tasks += build_sysroots.bootstrap_tasks
+
+            sdk_env = SharedSysrootsEnv()
+            sdk_env.initialize(ide_support, build_sysroots)
+        else:
+            sdk_env = RecipeModified(args.recipename)
+            sdk_env.initialize(config, workspace, tinfoil)
+            bootstap_tasks += sdk_env.bootstrap_tasks
+
+            gdb_multi_mode = not bool(args.ide == 'code')
+            gdb_cross = RecipeGdbCross(
+                args, sdk_env.target_arch, target_device, gdb_multi_mode)
+            gdb_cross.initialize(config, workspace, tinfoil)
+            bootstap_tasks += gdb_cross.bootstrap_tasks
+    finally:
+        tinfoil.shutdown()
+
+    shared_sysroot = bool(args.recipename in SHARED_SYSROOT_RECIPES)
+    if not shared_sysroot:
+        sdk_env.debug_build_config(args)
+
+    if not args.skip_bitbake:
+        bb_cmd = 'bitbake '
+        if args.bitbake_k:
+            bb_cmd += "-k "
+        bb_cmd += ' '.join(bootstap_tasks)
+        exec_build_env_command(config.init_path, basepath, bb_cmd, watch=True)
+
+    if shared_sysroot:
+        sdk_env.setup_ide(args)
+    else:
+        sdk_env.setup_ide(args, image_config, gdb_cross)
+
+
+def get_default_ide():
+    for an_ide in SUPPORTED_IDES[:-1]:
+        if shutil.which(an_ide):
+            return an_ide
+    return SUPPORTED_IDES[-1:]
+
+
+def register_commands(subparsers, context):
+    """Register devtool subcommands from this plugin"""
+    parser_ide = subparsers.add_parser('ide', help='Setup the IDE (VSCode)',
+                                       description='Configure the IDE to work with the source code of a recipe.',
+                                       group='working', order=50, formatter_class=RawTextHelpFormatter)
+    parser_ide.add_argument(
+        'recipename', help='Generate a IDE configuration in the workspace of the given recipe. '
+        'By default the workspace is configured to use the recipe sysroot prepared by devtool modify. '
+        'For some special recipes the generated configuration referes to the shared sysroots '
+        'provided by meta-ide-setup and build-sysroots recipes. '
+        'The following recipes use a shared sysroot: %s' % SHARED_SYSROOT_RECIPES)
+    parser_ide.add_argument(
+        'image', help='The image running on the target device. This is required for remote debugging.'
+        'It is important to deploy the image built by this command to the target device because '
+        'otherwise the debug symbols used on the build machine (rootfs-dbg) are probably out of sync '
+        'with the binaries executed on the target device.')
+    parser_ide.add_argument(
+        '-i', '--ide', choices=SUPPORTED_IDES, default=get_default_ide(),
+        help='Setup the configuration for this IDE')
+    parser_ide.add_argument(
+        '-t', '--target', default='root@192.168.7.2',
+        help='Live target machine running an ssh server: user@hostname.')
+    parser_ide.add_argument(
+        '-G', '--gdbserver-port-start', default="1234", help='port where gdbserver is listening.')
+    parser_ide.add_argument(
+        '-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
+    parser_ide.add_argument(
+        '-e', '--ssh-exec', help='Executable to use in place of ssh')
+    parser_ide.add_argument(
+        '-P', '--port', help='Specify ssh port to use for connection to the target')
+    parser_ide.add_argument(
+        '-I', '--key', help='Specify ssh private key for connection to the target')
+    parser_ide.add_argument(
+        '--skip-bitbake', help='Generate IDE configuration but skip calling bibtake to update the SDK.', action='store_true')
+    parser_ide.add_argument(
+        '-k', '--bitbake-k', help='Pass -k parameter to bitbake', action='store_true')
+    parser_ide.add_argument(
+        '--debug-build-config', help='Use debug build flags, for example set CMAKE_BUILD_TYPE=Debug', action='store_true')
+    parser_ide.set_defaults(func=ide_setup)
+
+    # TODO: Better support for multiple recipes. E.g. a list of recipes with auto-detection for the image recipe or all modified recipes