diff options
| author | Adrian Freihofer <adrian.freihofer@gmail.com> | 2024-01-22 14:58:21 +0100 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2024-02-18 07:34:42 +0000 |
| commit | 3ccb4d8ab1d7f4103f245f754086ec19f0195cc1 (patch) | |
| tree | fee7516c0628dd0cdb93c38a534507404cfd9620 /scripts/lib/devtool/ide_plugins | |
| parent | f909d235c95d89bd44a7d3fc719adfc82cdc1d98 (diff) | |
| download | poky-3ccb4d8ab1d7f4103f245f754086ec19f0195cc1.tar.gz | |
devtool: new ide-sdk plugin
The new devtool ide plugin provides the eSDK and configures an IDE to
work with the eSDK. In doing so, bitbake should be used to generate the
IDE configuration and update the SDK, but it should no longer play a
role when working on the source code. The work on the source code should
take place exclusively with the IDE, which, for example, calls cmake
directly to compile the code and execute the unit tests from the IDE.
The plugin works for recipes inheriting the cmake or the meson bbclass.
Support for more programming languages and build tools may be added in
the future.
There are various IDEs that can be used for the development of embedded
Linux applications. Therefore, devtool ide-sdk, like devtool itself,
supports plugins to support IDEs.
VSCode is the default IDE for this first implementation. Additionally,
some generic helper scripts can be generated with --ide none instead of
a specific IDE configuration. This can be used for any IDE that
supports calling some scripts.
There are two different modes supported:
- devtool modify mode (default):
devtool ide-sdk configures the IDE to manage the build-tool used by the
recipe (e.g. cmake or meson). The workflow looks like:
$ devtool modify a-recipe
$ devtool ide-sdk a-recipe a-image
$ code "$BUILDDIR/workspace/sources/a-recipe"
Work in VSCode, after installing the proposed plugins
Deploying the artifacts to the target device and running a remote
debugging session is supported as well.
This first implementation still calls bitbake and devtool to copy the
binary artifacts to the target device. In contrast to compiling,
installation and copying must be performed with the file rights of the
target device. The pseudo tool must be used for this. Therefore
bitbake -c install a-recipe && devtool deploy-target a-recipe
are called by the IDE for the deployment. This might be improved later
on.
Executing the unit tests out of the IDE is supported via Qemu user if
the build tool supports that. CMake (if cmake-qemu.bbclass is
inherited) and Meson support Qemu usermode.
- Shared sysroots mode: bootstraps the eSDK with shared sysroots for
all the recipes passed to devtool ide-sdk. This is basically a wrapper
for bitbake meta-ide-support && bitbake build-sysroots. The workflow
looks like:
$ devtool ide-sdk --share-sysroots a-recipe another-recipe
vscode where/the/sources/are
If the IDE and the build tool support it, the IDE gets configured to
offer the cross tool-chain provided by the eSDK. In case of VSCode and
cmake a cmake-kit is generated. This offers to use the cross
tool-chain from the UI of the IDE.
Many thanks to Enguerrand de Ribaucourt for testing and bug fixing.
(From OE-Core rev: 3f8af7a36589cd05fd07d16cbdd03d6b3dff1f82)
Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/lib/devtool/ide_plugins')
| -rw-r--r-- | scripts/lib/devtool/ide_plugins/__init__.py | 267 | ||||
| -rw-r--r-- | scripts/lib/devtool/ide_plugins/ide_code.py | 438 | ||||
| -rw-r--r-- | scripts/lib/devtool/ide_plugins/ide_none.py | 53 |
3 files changed, 758 insertions, 0 deletions
diff --git a/scripts/lib/devtool/ide_plugins/__init__.py b/scripts/lib/devtool/ide_plugins/__init__.py new file mode 100644 index 0000000000..3371b24264 --- /dev/null +++ b/scripts/lib/devtool/ide_plugins/__init__.py | |||
| @@ -0,0 +1,267 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2023-2024 Siemens AG | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | """Devtool ide-sdk IDE plugin interface definition and helper functions""" | ||
| 7 | |||
| 8 | import errno | ||
| 9 | import json | ||
| 10 | import logging | ||
| 11 | import os | ||
| 12 | import stat | ||
| 13 | from enum import Enum, auto | ||
| 14 | from devtool import DevtoolError | ||
| 15 | from bb.utils import mkdirhier | ||
| 16 | |||
| 17 | logger = logging.getLogger('devtool') | ||
| 18 | |||
| 19 | |||
| 20 | class BuildTool(Enum): | ||
| 21 | UNDEFINED = auto() | ||
| 22 | CMAKE = auto() | ||
| 23 | MESON = auto() | ||
| 24 | |||
| 25 | @property | ||
| 26 | def is_c_ccp(self): | ||
| 27 | if self is BuildTool.CMAKE: | ||
| 28 | return True | ||
| 29 | if self is BuildTool.MESON: | ||
| 30 | return True | ||
| 31 | return False | ||
| 32 | |||
| 33 | |||
| 34 | class GdbCrossConfig: | ||
| 35 | """Base class defining the GDB configuration generator interface | ||
| 36 | |||
| 37 | Generate a GDB configuration for a binary on the target device. | ||
| 38 | Only one instance per binary is allowed. This allows to assign unique port | ||
| 39 | numbers for all gdbserver instances. | ||
| 40 | """ | ||
| 41 | _gdbserver_port_next = 1234 | ||
| 42 | _binaries = [] | ||
| 43 | |||
| 44 | def __init__(self, image_recipe, modified_recipe, binary, gdbserver_multi=True): | ||
| 45 | self.image_recipe = image_recipe | ||
| 46 | self.modified_recipe = modified_recipe | ||
| 47 | self.gdb_cross = modified_recipe.gdb_cross | ||
| 48 | self.binary = binary | ||
| 49 | if binary in GdbCrossConfig._binaries: | ||
| 50 | raise DevtoolError( | ||
| 51 | "gdbserver config for binary %s is already generated" % binary) | ||
| 52 | GdbCrossConfig._binaries.append(binary) | ||
| 53 | self.script_dir = modified_recipe.ide_sdk_scripts_dir | ||
| 54 | self.gdbinit_dir = os.path.join(self.script_dir, 'gdbinit') | ||
| 55 | self.gdbserver_multi = gdbserver_multi | ||
| 56 | self.binary_pretty = self.binary.replace(os.sep, '-').lstrip('-') | ||
| 57 | self.gdbserver_port = GdbCrossConfig._gdbserver_port_next | ||
| 58 | GdbCrossConfig._gdbserver_port_next += 1 | ||
| 59 | self.id_pretty = "%d_%s" % (self.gdbserver_port, self.binary_pretty) | ||
| 60 | # gdbserver start script | ||
| 61 | gdbserver_script_file = 'gdbserver_' + self.id_pretty | ||
| 62 | if self.gdbserver_multi: | ||
| 63 | gdbserver_script_file += "_m" | ||
| 64 | self.gdbserver_script = os.path.join( | ||
| 65 | self.script_dir, gdbserver_script_file) | ||
| 66 | # gdbinit file | ||
| 67 | self.gdbinit = os.path.join( | ||
| 68 | self.gdbinit_dir, 'gdbinit_' + self.id_pretty) | ||
| 69 | # gdb start script | ||
| 70 | self.gdb_script = os.path.join( | ||
| 71 | self.script_dir, 'gdb_' + self.id_pretty) | ||
| 72 | |||
| 73 | def _gen_gdbserver_start_script(self): | ||
| 74 | """Generate a shell command starting the gdbserver on the remote device via ssh | ||
| 75 | |||
| 76 | GDB supports two modes: | ||
| 77 | multi: gdbserver remains running over several debug sessions | ||
| 78 | once: gdbserver terminates after the debugged process terminates | ||
| 79 | """ | ||
| 80 | cmd_lines = ['#!/bin/sh'] | ||
| 81 | if self.gdbserver_multi: | ||
| 82 | temp_dir = "TEMP_DIR=/tmp/gdbserver_%s; " % self.id_pretty | ||
| 83 | gdbserver_cmd_start = temp_dir | ||
| 84 | gdbserver_cmd_start += "test -f \$TEMP_DIR/pid && exit 0; " | ||
| 85 | gdbserver_cmd_start += "mkdir -p \$TEMP_DIR; " | ||
| 86 | gdbserver_cmd_start += "%s --multi :%s > \$TEMP_DIR/log 2>&1 & " % ( | ||
| 87 | self.gdb_cross.gdbserver_path, self.gdbserver_port) | ||
| 88 | gdbserver_cmd_start += "echo \$! > \$TEMP_DIR/pid;" | ||
| 89 | |||
| 90 | gdbserver_cmd_stop = temp_dir | ||
| 91 | gdbserver_cmd_stop += "test -f \$TEMP_DIR/pid && kill \$(cat \$TEMP_DIR/pid); " | ||
| 92 | gdbserver_cmd_stop += "rm -rf \$TEMP_DIR; " | ||
| 93 | |||
| 94 | gdbserver_cmd_l = [] | ||
| 95 | gdbserver_cmd_l.append('if [ "$1" = "stop" ]; then') | ||
| 96 | gdbserver_cmd_l.append(' shift') | ||
| 97 | gdbserver_cmd_l.append(" %s %s %s %s 'sh -c \"%s\"'" % ( | ||
| 98 | self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_stop)) | ||
| 99 | gdbserver_cmd_l.append('else') | ||
| 100 | gdbserver_cmd_l.append(" %s %s %s %s 'sh -c \"%s\"'" % ( | ||
| 101 | self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_start)) | ||
| 102 | gdbserver_cmd_l.append('fi') | ||
| 103 | gdbserver_cmd = os.linesep.join(gdbserver_cmd_l) | ||
| 104 | else: | ||
| 105 | gdbserver_cmd_start = "%s --once :%s %s" % ( | ||
| 106 | self.gdb_cross.gdbserver_path, self.gdbserver_port, self.binary) | ||
| 107 | gdbserver_cmd = "%s %s %s %s 'sh -c \"%s\"'" % ( | ||
| 108 | self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_start) | ||
| 109 | cmd_lines.append(gdbserver_cmd) | ||
| 110 | GdbCrossConfig.write_file(self.gdbserver_script, cmd_lines, True) | ||
| 111 | |||
| 112 | def _gen_gdbinit_config(self): | ||
| 113 | """Generate a gdbinit file for this binary and the corresponding gdbserver configuration""" | ||
| 114 | gdbinit_lines = ['# This file is generated by devtool ide-sdk'] | ||
| 115 | if self.gdbserver_multi: | ||
| 116 | target_help = '# gdbserver --multi :%d' % self.gdbserver_port | ||
| 117 | remote_cmd = 'target extended-remote' | ||
| 118 | else: | ||
| 119 | target_help = '# gdbserver :%d %s' % ( | ||
| 120 | self.gdbserver_port, self.binary) | ||
| 121 | remote_cmd = 'target remote' | ||
| 122 | gdbinit_lines.append('# On the remote target:') | ||
| 123 | gdbinit_lines.append(target_help) | ||
| 124 | gdbinit_lines.append('# On the build machine:') | ||
| 125 | gdbinit_lines.append('# cd ' + self.modified_recipe.real_srctree) | ||
| 126 | gdbinit_lines.append( | ||
| 127 | '# ' + self.gdb_cross.gdb + ' -ix ' + self.gdbinit) | ||
| 128 | |||
| 129 | gdbinit_lines.append('set sysroot ' + self.modified_recipe.d) | ||
| 130 | gdbinit_lines.append('set substitute-path "/usr/include" "' + | ||
| 131 | os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"') | ||
| 132 | # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir. | ||
| 133 | gdbinit_lines.append('set debuginfod enabled off') | ||
| 134 | if self.image_recipe.rootfs_dbg: | ||
| 135 | gdbinit_lines.append( | ||
| 136 | 'set solib-search-path "' + self.modified_recipe.solib_search_path_str(self.image_recipe) + '"') | ||
| 137 | gdbinit_lines.append('set substitute-path "/usr/src/debug" "' + os.path.join( | ||
| 138 | self.image_recipe.rootfs_dbg, 'usr', 'src', 'debug') + '"') | ||
| 139 | gdbinit_lines.append( | ||
| 140 | '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port)) | ||
| 141 | gdbinit_lines.append('set remote exec-file ' + self.binary) | ||
| 142 | gdbinit_lines.append( | ||
| 143 | 'run ' + os.path.join(self.modified_recipe.d, self.binary)) | ||
| 144 | |||
| 145 | GdbCrossConfig.write_file(self.gdbinit, gdbinit_lines) | ||
| 146 | |||
| 147 | def _gen_gdb_start_script(self): | ||
| 148 | """Generate a script starting GDB with the corresponding gdbinit configuration.""" | ||
| 149 | cmd_lines = ['#!/bin/sh'] | ||
| 150 | cmd_lines.append('cd ' + self.modified_recipe.real_srctree) | ||
| 151 | cmd_lines.append(self.gdb_cross.gdb + ' -ix ' + | ||
| 152 | self.gdbinit + ' "$@"') | ||
| 153 | GdbCrossConfig.write_file(self.gdb_script, cmd_lines, True) | ||
| 154 | |||
| 155 | def initialize(self): | ||
| 156 | self._gen_gdbserver_start_script() | ||
| 157 | self._gen_gdbinit_config() | ||
| 158 | self._gen_gdb_start_script() | ||
| 159 | |||
| 160 | @staticmethod | ||
| 161 | def write_file(script_file, cmd_lines, executable=False): | ||
| 162 | script_dir = os.path.dirname(script_file) | ||
| 163 | mkdirhier(script_dir) | ||
| 164 | with open(script_file, 'w') as script_f: | ||
| 165 | script_f.write(os.linesep.join(cmd_lines)) | ||
| 166 | script_f.write(os.linesep) | ||
| 167 | if executable: | ||
| 168 | st = os.stat(script_file) | ||
| 169 | os.chmod(script_file, st.st_mode | stat.S_IEXEC) | ||
| 170 | logger.info("Created: %s" % script_file) | ||
| 171 | |||
| 172 | |||
| 173 | class IdeBase: | ||
| 174 | """Base class defining the interface for IDE plugins""" | ||
| 175 | |||
| 176 | def __init__(self): | ||
| 177 | self.ide_name = 'undefined' | ||
| 178 | self.gdb_cross_configs = [] | ||
| 179 | |||
| 180 | @classmethod | ||
| 181 | def ide_plugin_priority(cls): | ||
| 182 | """Used to find the default ide handler if --ide is not passed""" | ||
| 183 | return 10 | ||
| 184 | |||
| 185 | def setup_shared_sysroots(self, shared_env): | ||
| 186 | logger.warn("Shared sysroot mode is not supported for IDE %s" % | ||
| 187 | self.ide_name) | ||
| 188 | |||
| 189 | def setup_modified_recipe(self, args, image_recipe, modified_recipe): | ||
| 190 | logger.warn("Modified recipe mode is not supported for IDE %s" % | ||
| 191 | self.ide_name) | ||
| 192 | |||
| 193 | def initialize_gdb_cross_configs(self, image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfig): | ||
| 194 | binaries = modified_recipe.find_installed_binaries() | ||
| 195 | for binary in binaries: | ||
| 196 | gdb_cross_config = gdb_cross_config_class( | ||
| 197 | image_recipe, modified_recipe, binary) | ||
| 198 | gdb_cross_config.initialize() | ||
| 199 | self.gdb_cross_configs.append(gdb_cross_config) | ||
| 200 | |||
| 201 | @staticmethod | ||
| 202 | def gen_oe_scrtips_sym_link(modified_recipe): | ||
| 203 | # create a sym-link from sources to the scripts directory | ||
| 204 | if os.path.isdir(modified_recipe.ide_sdk_scripts_dir): | ||
| 205 | IdeBase.symlink_force(modified_recipe.ide_sdk_scripts_dir, | ||
| 206 | os.path.join(modified_recipe.real_srctree, 'oe-scripts')) | ||
| 207 | |||
| 208 | @staticmethod | ||
| 209 | def update_json_file(json_dir, json_file, update_dict): | ||
| 210 | """Update a json file | ||
| 211 | |||
| 212 | By default it uses the dict.update function. If this is not sutiable | ||
| 213 | the update function might be passed via update_func parameter. | ||
| 214 | """ | ||
| 215 | json_path = os.path.join(json_dir, json_file) | ||
| 216 | logger.info("Updating IDE config file: %s (%s)" % | ||
| 217 | (json_file, json_path)) | ||
| 218 | if not os.path.exists(json_dir): | ||
| 219 | os.makedirs(json_dir) | ||
| 220 | try: | ||
| 221 | with open(json_path) as f: | ||
| 222 | orig_dict = json.load(f) | ||
| 223 | except json.decoder.JSONDecodeError: | ||
| 224 | logger.info( | ||
| 225 | "Decoding %s failed. Probably because of comments in the json file" % json_path) | ||
| 226 | orig_dict = {} | ||
| 227 | except FileNotFoundError: | ||
| 228 | orig_dict = {} | ||
| 229 | orig_dict.update(update_dict) | ||
| 230 | with open(json_path, 'w') as f: | ||
| 231 | json.dump(orig_dict, f, indent=4) | ||
| 232 | |||
| 233 | @staticmethod | ||
| 234 | def symlink_force(tgt, dst): | ||
| 235 | try: | ||
| 236 | os.symlink(tgt, dst) | ||
| 237 | except OSError as err: | ||
| 238 | if err.errno == errno.EEXIST: | ||
| 239 | if os.readlink(dst) != tgt: | ||
| 240 | os.remove(dst) | ||
| 241 | os.symlink(tgt, dst) | ||
| 242 | else: | ||
| 243 | raise err | ||
| 244 | |||
| 245 | |||
| 246 | def get_devtool_deploy_opts(args): | ||
| 247 | """Filter args for devtool deploy-target args""" | ||
| 248 | if not args.target: | ||
| 249 | return None | ||
| 250 | devtool_deploy_opts = [args.target] | ||
| 251 | if args.no_host_check: | ||
| 252 | devtool_deploy_opts += ["-c"] | ||
| 253 | if args.show_status: | ||
| 254 | devtool_deploy_opts += ["-s"] | ||
| 255 | if args.no_preserve: | ||
| 256 | devtool_deploy_opts += ["-p"] | ||
| 257 | if args.no_check_space: | ||
| 258 | devtool_deploy_opts += ["--no-check-space"] | ||
| 259 | if args.ssh_exec: | ||
| 260 | devtool_deploy_opts += ["-e", args.ssh.exec] | ||
| 261 | if args.port: | ||
| 262 | devtool_deploy_opts += ["-P", args.port] | ||
| 263 | if args.key: | ||
| 264 | devtool_deploy_opts += ["-I", args.key] | ||
| 265 | if args.strip is False: | ||
| 266 | devtool_deploy_opts += ["--no-strip"] | ||
| 267 | return devtool_deploy_opts | ||
diff --git a/scripts/lib/devtool/ide_plugins/ide_code.py b/scripts/lib/devtool/ide_plugins/ide_code.py new file mode 100644 index 0000000000..b2193130d2 --- /dev/null +++ b/scripts/lib/devtool/ide_plugins/ide_code.py | |||
| @@ -0,0 +1,438 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2023-2024 Siemens AG | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | """Devtool ide-sdk IDE plugin for VSCode and VSCodium""" | ||
| 7 | |||
| 8 | import json | ||
| 9 | import logging | ||
| 10 | import os | ||
| 11 | import shutil | ||
| 12 | from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, get_devtool_deploy_opts | ||
| 13 | |||
| 14 | logger = logging.getLogger('devtool') | ||
| 15 | |||
| 16 | |||
| 17 | class GdbCrossConfigVSCode(GdbCrossConfig): | ||
| 18 | def __init__(self, image_recipe, modified_recipe, binary): | ||
| 19 | super().__init__(image_recipe, modified_recipe, binary, False) | ||
| 20 | |||
| 21 | def initialize(self): | ||
| 22 | self._gen_gdbserver_start_script() | ||
| 23 | |||
| 24 | |||
| 25 | class IdeVSCode(IdeBase): | ||
| 26 | """Manage IDE configurations for VSCode | ||
| 27 | |||
| 28 | Modified recipe mode: | ||
| 29 | - cmake: use the cmake-preset generated by devtool ide-sdk | ||
| 30 | - meson: meson is called via a wrapper script generated by devtool ide-sdk | ||
| 31 | |||
| 32 | Shared sysroot mode: | ||
| 33 | In shared sysroot mode, the cross tool-chain is exported to the user's global configuration. | ||
| 34 | A workspace cannot be created because there is no recipe that defines how a workspace could | ||
| 35 | be set up. | ||
| 36 | - cmake: adds a cmake-kit to .local/share/CMakeTools/cmake-tools-kits.json | ||
| 37 | The cmake-kit uses the environment script and the tool-chain file | ||
| 38 | generated by meta-ide-support. | ||
| 39 | - meson: Meson needs manual workspace configuration. | ||
| 40 | """ | ||
| 41 | |||
| 42 | @classmethod | ||
| 43 | def ide_plugin_priority(cls): | ||
| 44 | """If --ide is not passed this is the default plugin""" | ||
| 45 | if shutil.which('code'): | ||
| 46 | return 100 | ||
| 47 | return 0 | ||
| 48 | |||
| 49 | def setup_shared_sysroots(self, shared_env): | ||
| 50 | """Expose the toolchain of the shared sysroots SDK""" | ||
| 51 | datadir = shared_env.ide_support.datadir | ||
| 52 | deploy_dir_image = shared_env.ide_support.deploy_dir_image | ||
| 53 | real_multimach_target_sys = shared_env.ide_support.real_multimach_target_sys | ||
| 54 | standalone_sysroot_native = shared_env.build_sysroots.standalone_sysroot_native | ||
| 55 | vscode_ws_path = os.path.join( | ||
| 56 | os.environ['HOME'], '.local', 'share', 'CMakeTools') | ||
| 57 | cmake_kits_path = os.path.join(vscode_ws_path, 'cmake-tools-kits.json') | ||
| 58 | oecmake_generator = "Ninja" | ||
| 59 | env_script = os.path.join( | ||
| 60 | deploy_dir_image, 'environment-setup-' + real_multimach_target_sys) | ||
| 61 | |||
| 62 | if not os.path.isdir(vscode_ws_path): | ||
| 63 | os.makedirs(vscode_ws_path) | ||
| 64 | cmake_kits_old = [] | ||
| 65 | if os.path.exists(cmake_kits_path): | ||
| 66 | with open(cmake_kits_path, 'r', encoding='utf-8') as cmake_kits_file: | ||
| 67 | cmake_kits_old = json.load(cmake_kits_file) | ||
| 68 | cmake_kits = cmake_kits_old.copy() | ||
| 69 | |||
| 70 | cmake_kit_new = { | ||
| 71 | "name": "OE " + real_multimach_target_sys, | ||
| 72 | "environmentSetupScript": env_script, | ||
| 73 | "toolchainFile": standalone_sysroot_native + datadir + "/cmake/OEToolchainConfig.cmake", | ||
| 74 | "preferredGenerator": { | ||
| 75 | "name": oecmake_generator | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | def merge_kit(cmake_kits, cmake_kit_new): | ||
| 80 | i = 0 | ||
| 81 | while i < len(cmake_kits): | ||
| 82 | if 'environmentSetupScript' in cmake_kits[i] and \ | ||
| 83 | cmake_kits[i]['environmentSetupScript'] == cmake_kit_new['environmentSetupScript']: | ||
| 84 | cmake_kits[i] = cmake_kit_new | ||
| 85 | return | ||
| 86 | i += 1 | ||
| 87 | cmake_kits.append(cmake_kit_new) | ||
| 88 | merge_kit(cmake_kits, cmake_kit_new) | ||
| 89 | |||
| 90 | if cmake_kits != cmake_kits_old: | ||
| 91 | logger.info("Updating: %s" % cmake_kits_path) | ||
| 92 | with open(cmake_kits_path, 'w', encoding='utf-8') as cmake_kits_file: | ||
| 93 | json.dump(cmake_kits, cmake_kits_file, indent=4) | ||
| 94 | else: | ||
| 95 | logger.info("Already up to date: %s" % cmake_kits_path) | ||
| 96 | |||
| 97 | cmake_native = os.path.join( | ||
| 98 | shared_env.build_sysroots.standalone_sysroot_native, 'usr', 'bin', 'cmake') | ||
| 99 | if os.path.isfile(cmake_native): | ||
| 100 | logger.info('cmake-kits call cmake by default. If the cmake provided by this SDK should be used, please add the following line to ".vscode/settings.json" file: "cmake.cmakePath": "%s"' % cmake_native) | ||
| 101 | else: | ||
| 102 | logger.error("Cannot find cmake native at: %s" % cmake_native) | ||
| 103 | |||
| 104 | def dot_code_dir(self, modified_recipe): | ||
| 105 | return os.path.join(modified_recipe.srctree, '.vscode') | ||
| 106 | |||
| 107 | def __vscode_settings_meson(self, settings_dict, modified_recipe): | ||
| 108 | if modified_recipe.build_tool is not BuildTool.MESON: | ||
| 109 | return | ||
| 110 | settings_dict["mesonbuild.mesonPath"] = modified_recipe.meson_wrapper | ||
| 111 | |||
| 112 | confopts = modified_recipe.mesonopts.split() | ||
| 113 | confopts += modified_recipe.meson_cross_file.split() | ||
| 114 | confopts += modified_recipe.extra_oemeson.split() | ||
| 115 | settings_dict["mesonbuild.configureOptions"] = confopts | ||
| 116 | settings_dict["mesonbuild.buildFolder"] = modified_recipe.b | ||
| 117 | |||
| 118 | def __vscode_settings_cmake(self, settings_dict, modified_recipe): | ||
| 119 | """Add cmake specific settings to settings.json. | ||
| 120 | |||
| 121 | Note: most settings are passed to the cmake preset. | ||
| 122 | """ | ||
| 123 | if modified_recipe.build_tool is not BuildTool.CMAKE: | ||
| 124 | return | ||
| 125 | settings_dict["cmake.configureOnOpen"] = True | ||
| 126 | settings_dict["cmake.sourceDirectory"] = modified_recipe.real_srctree | ||
| 127 | |||
| 128 | def vscode_settings(self, modified_recipe): | ||
| 129 | files_excludes = { | ||
| 130 | "**/.git/**": True, | ||
| 131 | "**/oe-logs/**": True, | ||
| 132 | "**/oe-workdir/**": True, | ||
| 133 | "**/source-date-epoch/**": True | ||
| 134 | } | ||
| 135 | python_exclude = [ | ||
| 136 | "**/.git/**", | ||
| 137 | "**/oe-logs/**", | ||
| 138 | "**/oe-workdir/**", | ||
| 139 | "**/source-date-epoch/**" | ||
| 140 | ] | ||
| 141 | settings_dict = { | ||
| 142 | "files.watcherExclude": files_excludes, | ||
| 143 | "files.exclude": files_excludes, | ||
| 144 | "python.analysis.exclude": python_exclude | ||
| 145 | } | ||
| 146 | self.__vscode_settings_cmake(settings_dict, modified_recipe) | ||
| 147 | self.__vscode_settings_meson(settings_dict, modified_recipe) | ||
| 148 | |||
| 149 | settings_file = 'settings.json' | ||
| 150 | IdeBase.update_json_file( | ||
| 151 | self.dot_code_dir(modified_recipe), settings_file, settings_dict) | ||
| 152 | |||
| 153 | def __vscode_extensions_cmake(self, modified_recipe, recommendations): | ||
| 154 | if modified_recipe.build_tool is not BuildTool.CMAKE: | ||
| 155 | return | ||
| 156 | recommendations += [ | ||
| 157 | "twxs.cmake", | ||
| 158 | "ms-vscode.cmake-tools", | ||
| 159 | "ms-vscode.cpptools", | ||
| 160 | "ms-vscode.cpptools-extension-pack", | ||
| 161 | "ms-vscode.cpptools-themes" | ||
| 162 | ] | ||
| 163 | |||
| 164 | def __vscode_extensions_meson(self, modified_recipe, recommendations): | ||
| 165 | if modified_recipe.build_tool is not BuildTool.MESON: | ||
| 166 | return | ||
| 167 | recommendations += [ | ||
| 168 | 'mesonbuild.mesonbuild', | ||
| 169 | "ms-vscode.cpptools", | ||
| 170 | "ms-vscode.cpptools-extension-pack", | ||
| 171 | "ms-vscode.cpptools-themes" | ||
| 172 | ] | ||
| 173 | |||
| 174 | def vscode_extensions(self, modified_recipe): | ||
| 175 | recommendations = [] | ||
| 176 | self.__vscode_extensions_cmake(modified_recipe, recommendations) | ||
| 177 | self.__vscode_extensions_meson(modified_recipe, recommendations) | ||
| 178 | extensions_file = 'extensions.json' | ||
| 179 | IdeBase.update_json_file( | ||
| 180 | self.dot_code_dir(modified_recipe), extensions_file, {"recommendations": recommendations}) | ||
| 181 | |||
| 182 | def vscode_c_cpp_properties(self, modified_recipe): | ||
| 183 | properties_dict = { | ||
| 184 | "name": modified_recipe.recipe_id_pretty, | ||
| 185 | } | ||
| 186 | if modified_recipe.build_tool is BuildTool.CMAKE: | ||
| 187 | properties_dict["configurationProvider"] = "ms-vscode.cmake-tools" | ||
| 188 | elif modified_recipe.build_tool is BuildTool.MESON: | ||
| 189 | properties_dict["configurationProvider"] = "mesonbuild.mesonbuild" | ||
| 190 | else: # no C/C++ build | ||
| 191 | return | ||
| 192 | |||
| 193 | properties_dicts = { | ||
| 194 | "configurations": [ | ||
| 195 | properties_dict | ||
| 196 | ], | ||
| 197 | "version": 4 | ||
| 198 | } | ||
| 199 | prop_file = 'c_cpp_properties.json' | ||
| 200 | IdeBase.update_json_file( | ||
| 201 | self.dot_code_dir(modified_recipe), prop_file, properties_dicts) | ||
| 202 | |||
| 203 | def vscode_launch_bin_dbg(self, gdb_cross_config): | ||
| 204 | modified_recipe = gdb_cross_config.modified_recipe | ||
| 205 | |||
| 206 | launch_config = { | ||
| 207 | "name": gdb_cross_config.id_pretty, | ||
| 208 | "type": "cppdbg", | ||
| 209 | "request": "launch", | ||
| 210 | "program": os.path.join(modified_recipe.d, gdb_cross_config.binary.lstrip('/')), | ||
| 211 | "stopAtEntry": True, | ||
| 212 | "cwd": "${workspaceFolder}", | ||
| 213 | "environment": [], | ||
| 214 | "externalConsole": False, | ||
| 215 | "MIMode": "gdb", | ||
| 216 | "preLaunchTask": gdb_cross_config.id_pretty, | ||
| 217 | "miDebuggerPath": modified_recipe.gdb_cross.gdb, | ||
| 218 | "miDebuggerServerAddress": "%s:%d" % (modified_recipe.gdb_cross.host, gdb_cross_config.gdbserver_port) | ||
| 219 | } | ||
| 220 | |||
| 221 | # Search for header files in recipe-sysroot. | ||
| 222 | src_file_map = { | ||
| 223 | "/usr/include": os.path.join(modified_recipe.recipe_sysroot, "usr", "include") | ||
| 224 | } | ||
| 225 | # First of all search for not stripped binaries in the image folder. | ||
| 226 | # These binaries are copied (and optionally stripped) by deploy-target | ||
| 227 | setup_commands = [ | ||
| 228 | { | ||
| 229 | "description": "sysroot", | ||
| 230 | "text": "set sysroot " + modified_recipe.d | ||
| 231 | } | ||
| 232 | ] | ||
| 233 | |||
| 234 | if gdb_cross_config.image_recipe.rootfs_dbg: | ||
| 235 | launch_config['additionalSOLibSearchPath'] = modified_recipe.solib_search_path_str( | ||
| 236 | gdb_cross_config.image_recipe) | ||
| 237 | src_file_map["/usr/src/debug"] = os.path.join( | ||
| 238 | gdb_cross_config.image_recipe.rootfs_dbg, "usr", "src", "debug") | ||
| 239 | else: | ||
| 240 | logger.warning( | ||
| 241 | "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.") | ||
| 242 | |||
| 243 | launch_config['sourceFileMap'] = src_file_map | ||
| 244 | launch_config['setupCommands'] = setup_commands | ||
| 245 | return launch_config | ||
| 246 | |||
| 247 | def vscode_launch(self, modified_recipe): | ||
| 248 | """GDB Launch configuration for binaries (elf files)""" | ||
| 249 | |||
| 250 | configurations = [self.vscode_launch_bin_dbg( | ||
| 251 | gdb_cross_config) for gdb_cross_config in self.gdb_cross_configs] | ||
| 252 | launch_dict = { | ||
| 253 | "version": "0.2.0", | ||
| 254 | "configurations": configurations | ||
| 255 | } | ||
| 256 | launch_file = 'launch.json' | ||
| 257 | IdeBase.update_json_file( | ||
| 258 | self.dot_code_dir(modified_recipe), launch_file, launch_dict) | ||
| 259 | |||
| 260 | def vscode_tasks_cpp(self, args, modified_recipe): | ||
| 261 | run_install_deploy = modified_recipe.gen_install_deploy_script(args) | ||
| 262 | install_task_name = "install && deploy-target %s" % modified_recipe.recipe_id_pretty | ||
| 263 | tasks_dict = { | ||
| 264 | "version": "2.0.0", | ||
| 265 | "tasks": [ | ||
| 266 | { | ||
| 267 | "label": install_task_name, | ||
| 268 | "type": "shell", | ||
| 269 | "command": run_install_deploy, | ||
| 270 | "problemMatcher": [] | ||
| 271 | } | ||
| 272 | ] | ||
| 273 | } | ||
| 274 | for gdb_cross_config in self.gdb_cross_configs: | ||
| 275 | tasks_dict['tasks'].append( | ||
| 276 | { | ||
| 277 | "label": gdb_cross_config.id_pretty, | ||
| 278 | "type": "shell", | ||
| 279 | "isBackground": True, | ||
| 280 | "dependsOn": [ | ||
| 281 | install_task_name | ||
| 282 | ], | ||
| 283 | "command": gdb_cross_config.gdbserver_script, | ||
| 284 | "problemMatcher": [ | ||
| 285 | { | ||
| 286 | "pattern": [ | ||
| 287 | { | ||
| 288 | "regexp": ".", | ||
| 289 | "file": 1, | ||
| 290 | "location": 2, | ||
| 291 | "message": 3 | ||
| 292 | } | ||
| 293 | ], | ||
| 294 | "background": { | ||
| 295 | "activeOnStart": True, | ||
| 296 | "beginsPattern": ".", | ||
| 297 | "endsPattern": ".", | ||
| 298 | } | ||
| 299 | } | ||
| 300 | ] | ||
| 301 | }) | ||
| 302 | tasks_file = 'tasks.json' | ||
| 303 | IdeBase.update_json_file( | ||
| 304 | self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) | ||
| 305 | |||
| 306 | def vscode_tasks_fallback(self, args, modified_recipe): | ||
| 307 | oe_init_dir = modified_recipe.oe_init_dir | ||
| 308 | oe_init = ". %s %s > /dev/null && " % (modified_recipe.oe_init_build_env, modified_recipe.topdir) | ||
| 309 | dt_build = "devtool build " | ||
| 310 | dt_build_label = dt_build + modified_recipe.recipe_id_pretty | ||
| 311 | dt_build_cmd = dt_build + modified_recipe.bpn | ||
| 312 | clean_opt = " --clean" | ||
| 313 | dt_build_clean_label = dt_build + modified_recipe.recipe_id_pretty + clean_opt | ||
| 314 | dt_build_clean_cmd = dt_build + modified_recipe.bpn + clean_opt | ||
| 315 | dt_deploy = "devtool deploy-target " | ||
| 316 | dt_deploy_label = dt_deploy + modified_recipe.recipe_id_pretty | ||
| 317 | dt_deploy_cmd = dt_deploy + modified_recipe.bpn | ||
| 318 | dt_build_deploy_label = "devtool build & deploy-target %s" % modified_recipe.recipe_id_pretty | ||
| 319 | deploy_opts = ' '.join(get_devtool_deploy_opts(args)) | ||
| 320 | tasks_dict = { | ||
| 321 | "version": "2.0.0", | ||
| 322 | "tasks": [ | ||
| 323 | { | ||
| 324 | "label": dt_build_label, | ||
| 325 | "type": "shell", | ||
| 326 | "command": "bash", | ||
| 327 | "linux": { | ||
| 328 | "options": { | ||
| 329 | "cwd": oe_init_dir | ||
| 330 | } | ||
| 331 | }, | ||
| 332 | "args": [ | ||
| 333 | "--login", | ||
| 334 | "-c", | ||
| 335 | "%s%s" % (oe_init, dt_build_cmd) | ||
| 336 | ], | ||
| 337 | "problemMatcher": [] | ||
| 338 | }, | ||
| 339 | { | ||
| 340 | "label": dt_deploy_label, | ||
| 341 | "type": "shell", | ||
| 342 | "command": "bash", | ||
| 343 | "linux": { | ||
| 344 | "options": { | ||
| 345 | "cwd": oe_init_dir | ||
| 346 | } | ||
| 347 | }, | ||
| 348 | "args": [ | ||
| 349 | "--login", | ||
| 350 | "-c", | ||
| 351 | "%s%s %s" % ( | ||
| 352 | oe_init, dt_deploy_cmd, deploy_opts) | ||
| 353 | ], | ||
| 354 | "problemMatcher": [] | ||
| 355 | }, | ||
| 356 | { | ||
| 357 | "label": dt_build_deploy_label, | ||
| 358 | "dependsOrder": "sequence", | ||
| 359 | "dependsOn": [ | ||
| 360 | dt_build_label, | ||
| 361 | dt_deploy_label | ||
| 362 | ], | ||
| 363 | "problemMatcher": [], | ||
| 364 | "group": { | ||
| 365 | "kind": "build", | ||
| 366 | "isDefault": True | ||
| 367 | } | ||
| 368 | }, | ||
| 369 | { | ||
| 370 | "label": dt_build_clean_label, | ||
| 371 | "type": "shell", | ||
| 372 | "command": "bash", | ||
| 373 | "linux": { | ||
| 374 | "options": { | ||
| 375 | "cwd": oe_init_dir | ||
| 376 | } | ||
| 377 | }, | ||
| 378 | "args": [ | ||
| 379 | "--login", | ||
| 380 | "-c", | ||
| 381 | "%s%s" % (oe_init, dt_build_clean_cmd) | ||
| 382 | ], | ||
| 383 | "problemMatcher": [] | ||
| 384 | } | ||
| 385 | ] | ||
| 386 | } | ||
| 387 | if modified_recipe.gdb_cross: | ||
| 388 | for gdb_cross_config in self.gdb_cross_configs: | ||
| 389 | tasks_dict['tasks'].append( | ||
| 390 | { | ||
| 391 | "label": gdb_cross_config.id_pretty, | ||
| 392 | "type": "shell", | ||
| 393 | "isBackground": True, | ||
| 394 | "dependsOn": [ | ||
| 395 | dt_build_deploy_label | ||
| 396 | ], | ||
| 397 | "command": gdb_cross_config.gdbserver_script, | ||
| 398 | "problemMatcher": [ | ||
| 399 | { | ||
| 400 | "pattern": [ | ||
| 401 | { | ||
| 402 | "regexp": ".", | ||
| 403 | "file": 1, | ||
| 404 | "location": 2, | ||
| 405 | "message": 3 | ||
| 406 | } | ||
| 407 | ], | ||
| 408 | "background": { | ||
| 409 | "activeOnStart": True, | ||
| 410 | "beginsPattern": ".", | ||
| 411 | "endsPattern": ".", | ||
| 412 | } | ||
| 413 | } | ||
| 414 | ] | ||
| 415 | }) | ||
| 416 | tasks_file = 'tasks.json' | ||
| 417 | IdeBase.update_json_file( | ||
| 418 | self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) | ||
| 419 | |||
| 420 | def vscode_tasks(self, args, modified_recipe): | ||
| 421 | if modified_recipe.build_tool.is_c_ccp: | ||
| 422 | self.vscode_tasks_cpp(args, modified_recipe) | ||
| 423 | else: | ||
| 424 | self.vscode_tasks_fallback(args, modified_recipe) | ||
| 425 | |||
| 426 | def setup_modified_recipe(self, args, image_recipe, modified_recipe): | ||
| 427 | self.vscode_settings(modified_recipe) | ||
| 428 | self.vscode_extensions(modified_recipe) | ||
| 429 | self.vscode_c_cpp_properties(modified_recipe) | ||
| 430 | if args.target: | ||
| 431 | self.initialize_gdb_cross_configs( | ||
| 432 | image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfigVSCode) | ||
| 433 | self.vscode_launch(modified_recipe) | ||
| 434 | self.vscode_tasks(args, modified_recipe) | ||
| 435 | |||
| 436 | |||
| 437 | def register_ide_plugin(ide_plugins): | ||
| 438 | ide_plugins['code'] = IdeVSCode | ||
diff --git a/scripts/lib/devtool/ide_plugins/ide_none.py b/scripts/lib/devtool/ide_plugins/ide_none.py new file mode 100644 index 0000000000..f106c5a026 --- /dev/null +++ b/scripts/lib/devtool/ide_plugins/ide_none.py | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2023-2024 Siemens AG | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | """Devtool ide-sdk generic IDE plugin""" | ||
| 7 | |||
| 8 | import os | ||
| 9 | import logging | ||
| 10 | from devtool.ide_plugins import IdeBase, GdbCrossConfig | ||
| 11 | |||
| 12 | logger = logging.getLogger('devtool') | ||
| 13 | |||
| 14 | |||
| 15 | class IdeNone(IdeBase): | ||
| 16 | """Generate some generic helpers for other IDEs | ||
| 17 | |||
| 18 | Modified recipe mode: | ||
| 19 | Generate some helper scripts for remote debugging with GDB | ||
| 20 | |||
| 21 | Shared sysroot mode: | ||
| 22 | A wrapper for bitbake meta-ide-support and bitbake build-sysroots | ||
| 23 | """ | ||
| 24 | |||
| 25 | def __init__(self): | ||
| 26 | super().__init__() | ||
| 27 | |||
| 28 | def setup_shared_sysroots(self, shared_env): | ||
| 29 | real_multimach_target_sys = shared_env.ide_support.real_multimach_target_sys | ||
| 30 | deploy_dir_image = shared_env.ide_support.deploy_dir_image | ||
| 31 | env_script = os.path.join( | ||
| 32 | deploy_dir_image, 'environment-setup-' + real_multimach_target_sys) | ||
| 33 | logger.info( | ||
| 34 | "To use this SDK please source this: %s" % env_script) | ||
| 35 | |||
| 36 | def setup_modified_recipe(self, args, image_recipe, modified_recipe): | ||
| 37 | """generate some helper scripts and config files | ||
| 38 | |||
| 39 | - Execute the do_install task | ||
| 40 | - Execute devtool deploy-target | ||
| 41 | - Generate a gdbinit file per executable | ||
| 42 | - Generate the oe-scripts sym-link | ||
| 43 | """ | ||
| 44 | script_path = modified_recipe.gen_install_deploy_script(args) | ||
| 45 | logger.info("Created: %s" % script_path) | ||
| 46 | |||
| 47 | self.initialize_gdb_cross_configs(image_recipe, modified_recipe) | ||
| 48 | |||
| 49 | IdeBase.gen_oe_scrtips_sym_link(modified_recipe) | ||
| 50 | |||
| 51 | |||
| 52 | def register_ide_plugin(ide_plugins): | ||
| 53 | ide_plugins['none'] = IdeNone | ||
