summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorAdrian Freihofer <adrian.freihofer@gmail.com>2024-01-22 14:58:21 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2024-02-18 07:34:42 +0000
commit3ccb4d8ab1d7f4103f245f754086ec19f0195cc1 (patch)
treefee7516c0628dd0cdb93c38a534507404cfd9620 /scripts
parentf909d235c95d89bd44a7d3fc719adfc82cdc1d98 (diff)
downloadpoky-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')
-rw-r--r--scripts/lib/devtool/ide_plugins/__init__.py267
-rw-r--r--scripts/lib/devtool/ide_plugins/ide_code.py438
-rw-r--r--scripts/lib/devtool/ide_plugins/ide_none.py53
-rwxr-xr-xscripts/lib/devtool/ide_sdk.py1032
4 files changed, 1790 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
8import errno
9import json
10import logging
11import os
12import stat
13from enum import Enum, auto
14from devtool import DevtoolError
15from bb.utils import mkdirhier
16
17logger = logging.getLogger('devtool')
18
19
20class 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
34class 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
173class 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
246def 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
8import json
9import logging
10import os
11import shutil
12from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, get_devtool_deploy_opts
13
14logger = logging.getLogger('devtool')
15
16
17class 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
25class 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
437def 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
8import os
9import logging
10from devtool.ide_plugins import IdeBase, GdbCrossConfig
11
12logger = logging.getLogger('devtool')
13
14
15class 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
52def register_ide_plugin(ide_plugins):
53 ide_plugins['none'] = IdeNone
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
new file mode 100755
index 0000000000..27389026be
--- /dev/null
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -0,0 +1,1032 @@
1# Development tool - ide-sdk command plugin
2#
3# Copyright (C) 2023-2024 Siemens AG
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7"""Devtool ide-sdk plugin"""
8
9import json
10import logging
11import os
12import re
13import shutil
14import stat
15import subprocess
16from argparse import RawTextHelpFormatter
17from enum import Enum
18
19import scriptutils
20import bb
21from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError, parse_recipe
22from devtool.standard import get_real_srctree
23from devtool.ide_plugins import BuildTool, get_devtool_deploy_opts
24
25
26logger = logging.getLogger('devtool')
27
28# dict of classes derived from IdeBase
29ide_plugins = {}
30
31
32class DevtoolIdeMode(Enum):
33 """Different modes are supported by the ide-sdk plugin.
34
35 The enum might be extended by more advanced modes in the future. Some ideas:
36 - auto: modified if all recipes are modified, shared if none of the recipes is modified.
37 - mixed: modified mode for modified recipes, shared mode for all other recipes.
38 """
39
40 modified = 'modified'
41 shared = 'shared'
42
43
44class TargetDevice:
45 """SSH remote login parameters"""
46
47 def __init__(self, args):
48 self.extraoptions = ''
49 if args.no_host_check:
50 self.extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
51 self.ssh_sshexec = 'ssh'
52 if args.ssh_exec:
53 self.ssh_sshexec = args.ssh_exec
54 self.ssh_port = ''
55 if args.port:
56 self.ssh_port = "-p %s" % args.port
57 if args.key:
58 self.extraoptions += ' -i %s' % args.key
59
60 self.target = args.target
61 target_sp = args.target.split('@')
62 if len(target_sp) == 1:
63 self.login = ""
64 self.host = target_sp[0]
65 elif len(target_sp) == 2:
66 self.login = target_sp[0]
67 self.host = target_sp[1]
68 else:
69 logger.error("Invalid target argument: %s" % args.target)
70
71
72class RecipeNative:
73 """Base class for calling bitbake to provide a -native recipe"""
74
75 def __init__(self, name, target_arch=None):
76 self.name = name
77 self.target_arch = target_arch
78 self.bootstrap_tasks = [self.name + ':do_addto_recipe_sysroot']
79 self.staging_bindir_native = None
80 self.target_sys = None
81 self.__native_bin = None
82
83 def _initialize(self, config, workspace, tinfoil):
84 """Get the parsed recipe"""
85 recipe_d = parse_recipe(
86 config, tinfoil, self.name, appends=True, filter_workspace=False)
87 if not recipe_d:
88 raise DevtoolError("Parsing %s recipe failed" % self.name)
89 self.staging_bindir_native = os.path.realpath(
90 recipe_d.getVar('STAGING_BINDIR_NATIVE'))
91 self.target_sys = recipe_d.getVar('TARGET_SYS')
92 return recipe_d
93
94 def initialize(self, config, workspace, tinfoil):
95 """Basic initialization that can be overridden by a derived class"""
96 self._initialize(config, workspace, tinfoil)
97
98 @property
99 def native_bin(self):
100 if not self.__native_bin:
101 raise DevtoolError("native binary name is not defined.")
102 return self.__native_bin
103
104
105class RecipeGdbCross(RecipeNative):
106 """Handle handle gdb-cross on the host and the gdbserver on the target device"""
107
108 def __init__(self, args, target_arch, target_device):
109 super().__init__('gdb-cross-' + target_arch, target_arch)
110 self.target_device = target_device
111 self.gdb = None
112 self.gdbserver_port_next = int(args.gdbserver_port_start)
113 self.config_db = {}
114
115 def __find_gdbserver(self, config, tinfoil):
116 """Absolute path of the gdbserver"""
117 recipe_d_gdb = parse_recipe(
118 config, tinfoil, 'gdb', appends=True, filter_workspace=False)
119 if not recipe_d_gdb:
120 raise DevtoolError("Parsing gdb recipe failed")
121 return os.path.join(recipe_d_gdb.getVar('bindir'), 'gdbserver')
122
123 def initialize(self, config, workspace, tinfoil):
124 super()._initialize(config, workspace, tinfoil)
125 gdb_bin = self.target_sys + '-gdb'
126 gdb_path = os.path.join(
127 self.staging_bindir_native, self.target_sys, gdb_bin)
128 self.gdb = gdb_path
129 self.gdbserver_path = self.__find_gdbserver(config, tinfoil)
130
131 @property
132 def host(self):
133 return self.target_device.host
134
135
136class RecipeImage:
137 """Handle some image recipe related properties
138
139 Most workflows require firmware that runs on the target device.
140 This firmware must be consistent with the setup of the host system.
141 In particular, the debug symbols must be compatible. For this, the
142 rootfs must be created as part of the SDK.
143 """
144
145 def __init__(self, name):
146 self.combine_dbg_image = False
147 self.gdbserver_missing = False
148 self.name = name
149 self.rootfs = None
150 self.__rootfs_dbg = None
151 self.bootstrap_tasks = [self.name + ':do_build']
152
153 def initialize(self, config, tinfoil):
154 image_d = parse_recipe(
155 config, tinfoil, self.name, appends=True, filter_workspace=False)
156 if not image_d:
157 raise DevtoolError(
158 "Parsing image recipe %s failed" % self.name)
159
160 self.combine_dbg_image = bb.data.inherits_class(
161 'image-combined-dbg', image_d)
162
163 workdir = image_d.getVar('WORKDIR')
164 self.rootfs = os.path.join(workdir, 'rootfs')
165 if image_d.getVar('IMAGE_GEN_DEBUGFS') == "1":
166 self.__rootfs_dbg = os.path.join(workdir, 'rootfs-dbg')
167
168 self.gdbserver_missing = 'gdbserver' not in image_d.getVar(
169 'IMAGE_INSTALL')
170
171 @property
172 def debug_support(self):
173 return bool(self.rootfs_dbg)
174
175 @property
176 def rootfs_dbg(self):
177 if self.__rootfs_dbg and os.path.isdir(self.__rootfs_dbg):
178 return self.__rootfs_dbg
179 return None
180
181
182class RecipeMetaIdeSupport:
183 """For the shared sysroots mode meta-ide-support is needed
184
185 For use cases where just a cross tool-chain is required but
186 no recipe is used, devtool ide-sdk abstracts calling bitbake meta-ide-support
187 and bitbake build-sysroots. This also allows to expose the cross-toolchains
188 to IDEs. For example VSCode support different tool-chains with e.g. cmake-kits.
189 """
190
191 def __init__(self):
192 self.bootstrap_tasks = ['meta-ide-support:do_build']
193 self.topdir = None
194 self.datadir = None
195 self.deploy_dir_image = None
196 self.build_sys = None
197 # From toolchain-scripts
198 self.real_multimach_target_sys = None
199
200 def initialize(self, config, tinfoil):
201 meta_ide_support_d = parse_recipe(
202 config, tinfoil, 'meta-ide-support', appends=True, filter_workspace=False)
203 if not meta_ide_support_d:
204 raise DevtoolError("Parsing meta-ide-support recipe failed")
205
206 self.topdir = meta_ide_support_d.getVar('TOPDIR')
207 self.datadir = meta_ide_support_d.getVar('datadir')
208 self.deploy_dir_image = meta_ide_support_d.getVar(
209 'DEPLOY_DIR_IMAGE')
210 self.build_sys = meta_ide_support_d.getVar('BUILD_SYS')
211 self.real_multimach_target_sys = meta_ide_support_d.getVar(
212 'REAL_MULTIMACH_TARGET_SYS')
213
214
215class RecipeBuildSysroots:
216 """For the shared sysroots mode build-sysroots is needed"""
217
218 def __init__(self):
219 self.standalone_sysroot = None
220 self.standalone_sysroot_native = None
221 self.bootstrap_tasks = [
222 'build-sysroots:do_build_target_sysroot',
223 'build-sysroots:do_build_native_sysroot'
224 ]
225
226 def initialize(self, config, tinfoil):
227 build_sysroots_d = parse_recipe(
228 config, tinfoil, 'build-sysroots', appends=True, filter_workspace=False)
229 if not build_sysroots_d:
230 raise DevtoolError("Parsing build-sysroots recipe failed")
231 self.standalone_sysroot = build_sysroots_d.getVar(
232 'STANDALONE_SYSROOT')
233 self.standalone_sysroot_native = build_sysroots_d.getVar(
234 'STANDALONE_SYSROOT_NATIVE')
235
236
237class SharedSysrootsEnv:
238 """Handle the shared sysroots based workflow
239
240 Support the workflow with just a tool-chain without a recipe.
241 It's basically like:
242 bitbake some-dependencies
243 bitbake meta-ide-support
244 bitbake build-sysroots
245 Use the environment-* file found in the deploy folder
246 """
247
248 def __init__(self):
249 self.ide_support = None
250 self.build_sysroots = None
251
252 def initialize(self, ide_support, build_sysroots):
253 self.ide_support = ide_support
254 self.build_sysroots = build_sysroots
255
256 def setup_ide(self, ide):
257 ide.setup(self)
258
259
260class RecipeNotModified:
261 """Handling of recipes added to the Direct DSK shared sysroots."""
262
263 def __init__(self, name):
264 self.name = name
265 self.bootstrap_tasks = [name + ':do_populate_sysroot']
266
267
268class RecipeModified:
269 """Handling of recipes in the workspace created by devtool modify"""
270 OE_INIT_BUILD_ENV = 'oe-init-build-env'
271
272 VALID_BASH_ENV_NAME_CHARS = re.compile(r"^[a-zA-Z0-9_]*$")
273
274 def __init__(self, name):
275 self.name = name
276 self.bootstrap_tasks = [name + ':do_install']
277 self.gdb_cross = None
278 # workspace
279 self.real_srctree = None
280 self.srctree = None
281 self.ide_sdk_dir = None
282 self.ide_sdk_scripts_dir = None
283 self.bbappend = None
284 # recipe variables from d.getVar
285 self.b = None
286 self.base_libdir = None
287 self.bblayers = None
288 self.bpn = None
289 self.d = None
290 self.fakerootcmd = None
291 self.fakerootenv = None
292 self.libdir = None
293 self.max_process = None
294 self.package_arch = None
295 self.package_debug_split_style = None
296 self.path = None
297 self.pn = None
298 self.recipe_sysroot = None
299 self.recipe_sysroot_native = None
300 self.staging_incdir = None
301 self.strip_cmd = None
302 self.target_arch = None
303 self.topdir = None
304 self.workdir = None
305 self.recipe_id = None
306 # replicate bitbake build environment
307 self.exported_vars = None
308 self.cmd_compile = None
309 self.__oe_init_dir = None
310 # main build tool used by this recipe
311 self.build_tool = BuildTool.UNDEFINED
312 # build_tool = cmake
313 self.oecmake_generator = None
314 self.cmake_cache_vars = None
315 # build_tool = meson
316 self.meson_buildtype = None
317 self.meson_wrapper = None
318 self.mesonopts = None
319 self.extra_oemeson = None
320 self.meson_cross_file = None
321
322 def initialize(self, config, workspace, tinfoil):
323 recipe_d = parse_recipe(
324 config, tinfoil, self.name, appends=True, filter_workspace=False)
325 if not recipe_d:
326 raise DevtoolError("Parsing %s recipe failed" % self.name)
327
328 # Verify this recipe is built as externalsrc setup by devtool modify
329 workspacepn = check_workspace_recipe(
330 workspace, self.name, bbclassextend=True)
331 self.srctree = workspace[workspacepn]['srctree']
332 # Need to grab this here in case the source is within a subdirectory
333 self.real_srctree = get_real_srctree(
334 self.srctree, recipe_d.getVar('S'), recipe_d.getVar('WORKDIR'))
335 self.bbappend = workspace[workspacepn]['bbappend']
336
337 self.ide_sdk_dir = os.path.join(
338 config.workspace_path, 'ide-sdk', self.name)
339 if os.path.exists(self.ide_sdk_dir):
340 shutil.rmtree(self.ide_sdk_dir)
341 self.ide_sdk_scripts_dir = os.path.join(self.ide_sdk_dir, 'scripts')
342
343 self.b = recipe_d.getVar('B')
344 self.base_libdir = recipe_d.getVar('base_libdir')
345 self.bblayers = recipe_d.getVar('BBLAYERS').split()
346 self.bpn = recipe_d.getVar('BPN')
347 self.d = recipe_d.getVar('D')
348 self.fakerootcmd = recipe_d.getVar('FAKEROOTCMD')
349 self.fakerootenv = recipe_d.getVar('FAKEROOTENV')
350 self.libdir = recipe_d.getVar('libdir')
351 self.max_process = int(recipe_d.getVar(
352 "BB_NUMBER_THREADS") or os.cpu_count() or 1)
353 self.package_arch = recipe_d.getVar('PACKAGE_ARCH')
354 self.package_debug_split_style = recipe_d.getVar(
355 'PACKAGE_DEBUG_SPLIT_STYLE')
356 self.path = recipe_d.getVar('PATH')
357 self.pn = recipe_d.getVar('PN')
358 self.recipe_sysroot = os.path.realpath(
359 recipe_d.getVar('RECIPE_SYSROOT'))
360 self.recipe_sysroot_native = os.path.realpath(
361 recipe_d.getVar('RECIPE_SYSROOT_NATIVE'))
362 self.staging_incdir = os.path.realpath(
363 recipe_d.getVar('STAGING_INCDIR'))
364 self.strip_cmd = recipe_d.getVar('STRIP')
365 self.target_arch = recipe_d.getVar('TARGET_ARCH')
366 self.topdir = recipe_d.getVar('TOPDIR')
367 self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR'))
368
369 self.__init_exported_variables(recipe_d)
370
371 if bb.data.inherits_class('cmake', recipe_d):
372 self.oecmake_generator = recipe_d.getVar('OECMAKE_GENERATOR')
373 self.__init_cmake_preset_cache(recipe_d)
374 self.build_tool = BuildTool.CMAKE
375 elif bb.data.inherits_class('meson', recipe_d):
376 self.meson_buildtype = recipe_d.getVar('MESON_BUILDTYPE')
377 self.mesonopts = recipe_d.getVar('MESONOPTS')
378 self.extra_oemeson = recipe_d.getVar('EXTRA_OEMESON')
379 self.meson_cross_file = recipe_d.getVar('MESON_CROSS_FILE')
380 self.build_tool = BuildTool.MESON
381
382 # Recipe ID is the identifier for IDE config sections
383 self.recipe_id = self.bpn + "-" + self.package_arch
384 self.recipe_id_pretty = self.bpn + ": " + self.package_arch
385
386 def append_to_bbappend(self, append_text):
387 with open(self.bbappend, 'a') as bbap:
388 bbap.write(append_text)
389
390 def remove_from_bbappend(self, append_text):
391 with open(self.bbappend, 'r') as bbap:
392 text = bbap.read()
393 new_text = text.replace(append_text, '')
394 with open(self.bbappend, 'w') as bbap:
395 bbap.write(new_text)
396
397 @staticmethod
398 def is_valid_shell_variable(var):
399 """Skip strange shell variables like systemd
400
401 prevent from strange bugs because of strange variables which
402 are not used in this context but break various tools.
403 """
404 if RecipeModified.VALID_BASH_ENV_NAME_CHARS.match(var):
405 bb.debug(1, "ignoring variable: %s" % var)
406 return True
407 return False
408
409 def debug_build_config(self, args):
410 """Explicitely set for example CMAKE_BUILD_TYPE to Debug if not defined otherwise"""
411 if self.build_tool is BuildTool.CMAKE:
412 append_text = os.linesep + \
413 'OECMAKE_ARGS:append = " -DCMAKE_BUILD_TYPE:STRING=Debug"' + os.linesep
414 if args.debug_build_config and not 'CMAKE_BUILD_TYPE' in self.cmake_cache_vars:
415 self.cmake_cache_vars['CMAKE_BUILD_TYPE'] = {
416 "type": "STRING",
417 "value": "Debug",
418 }
419 self.append_to_bbappend(append_text)
420 elif 'CMAKE_BUILD_TYPE' in self.cmake_cache_vars:
421 del self.cmake_cache_vars['CMAKE_BUILD_TYPE']
422 self.remove_from_bbappend(append_text)
423 elif self.build_tool is BuildTool.MESON:
424 append_text = os.linesep + 'MESON_BUILDTYPE = "debug"' + os.linesep
425 if args.debug_build_config and self.meson_buildtype != "debug":
426 self.mesonopts.replace(
427 '--buildtype ' + self.meson_buildtype, '--buildtype debug')
428 self.append_to_bbappend(append_text)
429 elif self.meson_buildtype == "debug":
430 self.mesonopts.replace(
431 '--buildtype debug', '--buildtype plain')
432 self.remove_from_bbappend(append_text)
433 elif args.debug_build_config:
434 logger.warn(
435 "--debug-build-config is not implemented for this build tool yet.")
436
437 def solib_search_path(self, image):
438 """Search for debug symbols in the rootfs and rootfs-dbg
439
440 The debug symbols of shared libraries which are provided by other packages
441 are grabbed from the -dbg packages in the rootfs-dbg.
442
443 But most cross debugging tools like gdb, perf, and systemtap need to find
444 executable/library first and through it debuglink note find corresponding
445 symbols file. Therefore the library paths from the rootfs are added as well.
446
447 Note: For the devtool modified recipe compiled from the IDE, the debug
448 symbols are taken from the unstripped binaries in the image folder.
449 Also, devtool deploy-target takes the files from the image folder.
450 debug symbols in the image folder refer to the corresponding source files
451 with absolute paths of the build machine. Debug symbols found in the
452 rootfs-dbg are relocated and contain paths which refer to the source files
453 installed on the target device e.g. /usr/src/...
454 """
455 base_libdir = self.base_libdir.lstrip('/')
456 libdir = self.libdir.lstrip('/')
457 so_paths = [
458 # debug symbols for package_debug_split_style: debug-with-srcpkg or .debug
459 os.path.join(image.rootfs_dbg, base_libdir, ".debug"),
460 os.path.join(image.rootfs_dbg, libdir, ".debug"),
461 # debug symbols for package_debug_split_style: debug-file-directory
462 os.path.join(image.rootfs_dbg, "usr", "lib", "debug"),
463
464 # The binaries are required as well, the debug packages are not enough
465 # With image-combined-dbg.bbclass the binaries are copied into rootfs-dbg
466 os.path.join(image.rootfs_dbg, base_libdir),
467 os.path.join(image.rootfs_dbg, libdir),
468 # Without image-combined-dbg.bbclass the binaries are only in rootfs.
469 # Note: Stepping into source files located in rootfs-dbg does not
470 # work without image-combined-dbg.bbclass yet.
471 os.path.join(image.rootfs, base_libdir),
472 os.path.join(image.rootfs, libdir)
473 ]
474 return so_paths
475
476 def solib_search_path_str(self, image):
477 """Return a : separated list of paths usable by GDB's set solib-search-path"""
478 return ':'.join(self.solib_search_path(image))
479
480 def __init_exported_variables(self, d):
481 """Find all variables with export flag set.
482
483 This allows to generate IDE configurations which compile with the same
484 environment as bitbake does. That's at least a reasonable default behavior.
485 """
486 exported_vars = {}
487
488 vars = (key for key in d.keys() if not key.startswith(
489 "__") and not d.getVarFlag(key, "func", False))
490 for var in vars:
491 func = d.getVarFlag(var, "func", False)
492 if d.getVarFlag(var, 'python', False) and func:
493 continue
494 export = d.getVarFlag(var, "export", False)
495 unexport = d.getVarFlag(var, "unexport", False)
496 if not export and not unexport and not func:
497 continue
498 if unexport:
499 continue
500
501 val = d.getVar(var)
502 if val is None:
503 continue
504 if set(var) & set("-.{}+"):
505 logger.warn(
506 "Warning: Found invalid character in variable name %s", str(var))
507 continue
508 varExpanded = d.expand(var)
509 val = str(val)
510
511 if not RecipeModified.is_valid_shell_variable(varExpanded):
512 continue
513
514 if func:
515 code_line = "line: {0}, file: {1}\n".format(
516 d.getVarFlag(var, "lineno", False),
517 d.getVarFlag(var, "filename", False))
518 val = val.rstrip('\n')
519 logger.warn("Warning: exported shell function %s() is not exported (%s)" %
520 (varExpanded, code_line))
521 continue
522
523 if export:
524 exported_vars[varExpanded] = val.strip()
525 continue
526
527 self.exported_vars = exported_vars
528
529 def __init_cmake_preset_cache(self, d):
530 """Get the arguments passed to cmake
531
532 Replicate the cmake configure arguments with all details to
533 share on build folder between bitbake and SDK.
534 """
535 site_file = os.path.join(self.workdir, 'site-file.cmake')
536 if os.path.exists(site_file):
537 print("Warning: site-file.cmake is not supported")
538
539 cache_vars = {}
540 oecmake_args = d.getVar('OECMAKE_ARGS').split()
541 extra_oecmake = d.getVar('EXTRA_OECMAKE').split()
542 for param in oecmake_args + extra_oecmake:
543 d_pref = "-D"
544 if param.startswith(d_pref):
545 param = param[len(d_pref):]
546 else:
547 print("Error: expected a -D")
548 param_s = param.split('=', 1)
549 param_nt = param_s[0].split(':', 1)
550
551 def handle_undefined_variable(var):
552 if var.startswith('${') and var.endswith('}'):
553 return ''
554 else:
555 return var
556 # Example: FOO=ON
557 if len(param_nt) == 1:
558 cache_vars[param_s[0]] = handle_undefined_variable(param_s[1])
559 # Example: FOO:PATH=/tmp
560 elif len(param_nt) == 2:
561 cache_vars[param_nt[0]] = {
562 "type": param_nt[1],
563 "value": handle_undefined_variable(param_s[1]),
564 }
565 else:
566 print("Error: cannot parse %s" % param)
567 self.cmake_cache_vars = cache_vars
568
569 def cmake_preset(self):
570 """Create a preset for cmake that mimics how bitbake calls cmake"""
571 toolchain_file = os.path.join(self.workdir, 'toolchain.cmake')
572 cmake_executable = os.path.join(
573 self.recipe_sysroot_native, 'usr', 'bin', 'cmake')
574 self.cmd_compile = cmake_executable + " --build --preset " + self.recipe_id
575
576 preset_dict_configure = {
577 "name": self.recipe_id,
578 "displayName": self.recipe_id_pretty,
579 "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
580 "binaryDir": self.b,
581 "generator": self.oecmake_generator,
582 "toolchainFile": toolchain_file,
583 "cacheVariables": self.cmake_cache_vars,
584 "environment": self.exported_vars,
585 "cmakeExecutable": cmake_executable
586 }
587
588 preset_dict_build = {
589 "name": self.recipe_id,
590 "displayName": self.recipe_id_pretty,
591 "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
592 "configurePreset": self.recipe_id,
593 "inheritConfigureEnvironment": True
594 }
595
596 preset_dict_test = {
597 "name": self.recipe_id,
598 "displayName": self.recipe_id_pretty,
599 "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
600 "configurePreset": self.recipe_id,
601 "inheritConfigureEnvironment": True
602 }
603
604 preset_dict = {
605 "version": 3, # cmake 3.21, backward compatible with kirkstone
606 "configurePresets": [preset_dict_configure],
607 "buildPresets": [preset_dict_build],
608 "testPresets": [preset_dict_test]
609 }
610
611 # Finally write the json file
612 json_file = 'CMakeUserPresets.json'
613 json_path = os.path.join(self.real_srctree, json_file)
614 logger.info("Updating CMake preset: %s (%s)" % (json_file, json_path))
615 if not os.path.exists(self.real_srctree):
616 os.makedirs(self.real_srctree)
617 try:
618 with open(json_path) as f:
619 orig_dict = json.load(f)
620 except json.decoder.JSONDecodeError:
621 logger.info(
622 "Decoding %s failed. Probably because of comments in the json file" % json_path)
623 orig_dict = {}
624 except FileNotFoundError:
625 orig_dict = {}
626
627 # Add or update the presets for the recipe and keep other presets
628 for k, v in preset_dict.items():
629 if isinstance(v, list):
630 update_preset = v[0]
631 preset_added = False
632 if k in orig_dict:
633 for index, orig_preset in enumerate(orig_dict[k]):
634 if 'name' in orig_preset:
635 if orig_preset['name'] == update_preset['name']:
636 logger.debug("Updating preset: %s" %
637 orig_preset['name'])
638 orig_dict[k][index] = update_preset
639 preset_added = True
640 break
641 else:
642 logger.debug("keeping preset: %s" %
643 orig_preset['name'])
644 else:
645 logger.warn("preset without a name found")
646 if not preset_added:
647 if not k in orig_dict:
648 orig_dict[k] = []
649 orig_dict[k].append(update_preset)
650 logger.debug("Added preset: %s" %
651 update_preset['name'])
652 else:
653 orig_dict[k] = v
654
655 with open(json_path, 'w') as f:
656 json.dump(orig_dict, f, indent=4)
657
658 def gen_meson_wrapper(self):
659 """Generate a wrapper script to call meson with the cross environment"""
660 bb.utils.mkdirhier(self.ide_sdk_scripts_dir)
661 meson_wrapper = os.path.join(self.ide_sdk_scripts_dir, 'meson')
662 meson_real = os.path.join(
663 self.recipe_sysroot_native, 'usr', 'bin', 'meson.real')
664 with open(meson_wrapper, 'w') as mwrap:
665 mwrap.write("#!/bin/sh" + os.linesep)
666 for var, val in self.exported_vars.items():
667 mwrap.write('export %s="%s"' % (var, val) + os.linesep)
668 mwrap.write("unset CC CXX CPP LD AR NM STRIP" + os.linesep)
669 private_temp = os.path.join(self.b, "meson-private", "tmp")
670 mwrap.write('mkdir -p "%s"' % private_temp + os.linesep)
671 mwrap.write('export TMPDIR="%s"' % private_temp + os.linesep)
672 mwrap.write('exec "%s" "$@"' % meson_real + os.linesep)
673 st = os.stat(meson_wrapper)
674 os.chmod(meson_wrapper, st.st_mode | stat.S_IEXEC)
675 self.meson_wrapper = meson_wrapper
676 self.cmd_compile = meson_wrapper + " compile -C " + self.b
677
678 def which(self, executable):
679 bin_path = shutil.which(executable, path=self.path)
680 if not bin_path:
681 raise DevtoolError(
682 'Cannot find %s. Probably the recipe %s is not built yet.' % (executable, self.bpn))
683 return bin_path
684
685 @staticmethod
686 def is_elf_file(file_path):
687 with open(file_path, "rb") as f:
688 data = f.read(4)
689 if data == b'\x7fELF':
690 return True
691 return False
692
693 def find_installed_binaries(self):
694 """find all executable elf files in the image directory"""
695 binaries = []
696 d_len = len(self.d)
697 re_so = re.compile('.*\.so[.0-9]*$')
698 for root, _, files in os.walk(self.d, followlinks=False):
699 for file in files:
700 if os.path.islink(file):
701 continue
702 if re_so.match(file):
703 continue
704 abs_name = os.path.join(root, file)
705 if os.access(abs_name, os.X_OK) and RecipeModified.is_elf_file(abs_name):
706 binaries.append(abs_name[d_len:])
707 return sorted(binaries)
708
709 def gen_delete_package_dirs(self):
710 """delete folders of package tasks
711
712 This is a workaround for and issue with recipes having their sources
713 downloaded as file://
714 This likely breaks pseudo like:
715 path mismatch [3 links]: ino 79147802 db
716 .../build/tmp/.../cmake-example/1.0/package/usr/src/debug/
717 cmake-example/1.0-r0/oe-local-files/cpp-example-lib.cpp
718 .../build/workspace/sources/cmake-example/oe-local-files/cpp-example-lib.cpp
719 Since the files are anyway outdated lets deleted them (also from pseudo's db) to workaround this issue.
720 """
721 cmd_lines = ['#!/bin/sh']
722
723 # Set up the appropriate environment
724 newenv = dict(os.environ)
725 for varvalue in self.fakerootenv.split():
726 if '=' in varvalue:
727 splitval = varvalue.split('=', 1)
728 newenv[splitval[0]] = splitval[1]
729
730 # Replicate the environment variables from bitbake
731 for var, val in newenv.items():
732 if not RecipeModified.is_valid_shell_variable(var):
733 continue
734 cmd_lines.append('%s="%s"' % (var, val))
735 cmd_lines.append('export %s' % var)
736
737 # Delete the folders
738 pkg_dirs = ' '.join([os.path.join(self.workdir, d) for d in [
739 "package", "packages-split", "pkgdata", "sstate-install-package", "debugsources.list", "*.spec"]])
740 cmd = "%s rm -rf %s" % (self.fakerootcmd, pkg_dirs)
741 cmd_lines.append('%s || { "%s failed"; exit 1; }' % (cmd, cmd))
742
743 return self.write_script(cmd_lines, 'delete_package_dirs')
744
745 def gen_install_deploy_script(self, args):
746 """Generate a script which does install and deploy"""
747 cmd_lines = ['#!/bin/bash']
748
749 cmd_lines.append(self.gen_delete_package_dirs())
750
751 # . oe-init-build-env $BUILDDIR
752 # Note: Sourcing scripts with arguments requires bash
753 cmd_lines.append('cd "%s" || { echo "cd %s failed"; exit 1; }' % (
754 self.oe_init_dir, self.oe_init_dir))
755 cmd_lines.append('. "%s" "%s" || { echo ". %s %s failed"; exit 1; }' % (
756 self.oe_init_build_env, self.topdir, self.oe_init_build_env, self.topdir))
757
758 # bitbake -c install
759 cmd_lines.append(
760 'bitbake %s -c install --force || { echo "bitbake %s -c install --force failed"; exit 1; }' % (self.bpn, self.bpn))
761
762 # devtool deploy-target
763 deploy_opts = ' '.join(get_devtool_deploy_opts(args))
764 cmd_lines.append("devtool deploy-target %s %s" %
765 (self.bpn, deploy_opts))
766 return self.write_script(cmd_lines, 'install_and_deploy')
767
768 def write_script(self, cmd_lines, script_name):
769 bb.utils.mkdirhier(self.ide_sdk_scripts_dir)
770 script_name_arch = script_name + '_' + self.recipe_id
771 script_file = os.path.join(self.ide_sdk_scripts_dir, script_name_arch)
772 with open(script_file, 'w') as script_f:
773 script_f.write(os.linesep.join(cmd_lines))
774 st = os.stat(script_file)
775 os.chmod(script_file, st.st_mode | stat.S_IEXEC)
776 return script_file
777
778 @property
779 def oe_init_build_env(self):
780 """Find the oe-init-build-env used for this setup"""
781 oe_init_dir = self.oe_init_dir
782 if oe_init_dir:
783 return os.path.join(oe_init_dir, RecipeModified.OE_INIT_BUILD_ENV)
784 return None
785
786 @property
787 def oe_init_dir(self):
788 """Find the directory where the oe-init-build-env is located
789
790 Assumption: There might be a layer with higher priority than poky
791 which provides to oe-init-build-env in the layer's toplevel folder.
792 """
793 if not self.__oe_init_dir:
794 for layer in reversed(self.bblayers):
795 result = subprocess.run(
796 ['git', 'rev-parse', '--show-toplevel'], cwd=layer, capture_output=True)
797 if result.returncode == 0:
798 oe_init_dir = result.stdout.decode('utf-8').strip()
799 oe_init_path = os.path.join(
800 oe_init_dir, RecipeModified.OE_INIT_BUILD_ENV)
801 if os.path.exists(oe_init_path):
802 logger.debug("Using %s from: %s" % (
803 RecipeModified.OE_INIT_BUILD_ENV, oe_init_path))
804 self.__oe_init_dir = oe_init_dir
805 break
806 if not self.__oe_init_dir:
807 logger.error("Cannot find the bitbake top level folder")
808 return self.__oe_init_dir
809
810
811def ide_setup(args, config, basepath, workspace):
812 """Generate the IDE configuration for the workspace"""
813
814 # Explicitely passing some special recipes does not make sense
815 for recipe in args.recipenames:
816 if recipe in ['meta-ide-support', 'build-sysroots']:
817 raise DevtoolError("Invalid recipe: %s." % recipe)
818
819 # Collect information about tasks which need to be bitbaked
820 bootstrap_tasks = []
821 bootstrap_tasks_late = []
822 tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
823 try:
824 # define mode depending on recipes which need to be processed
825 recipes_image_names = []
826 recipes_modified_names = []
827 recipes_other_names = []
828 for recipe in args.recipenames:
829 try:
830 check_workspace_recipe(
831 workspace, recipe, bbclassextend=True)
832 recipes_modified_names.append(recipe)
833 except DevtoolError:
834 recipe_d = parse_recipe(
835 config, tinfoil, recipe, appends=True, filter_workspace=False)
836 if not recipe_d:
837 raise DevtoolError("Parsing recipe %s failed" % recipe)
838 if bb.data.inherits_class('image', recipe_d):
839 recipes_image_names.append(recipe)
840 else:
841 recipes_other_names.append(recipe)
842
843 invalid_params = False
844 if args.mode == DevtoolIdeMode.shared:
845 if len(recipes_modified_names):
846 logger.error("In shared sysroots mode modified recipes %s cannot be handled." % str(
847 recipes_modified_names))
848 invalid_params = True
849 if args.mode == DevtoolIdeMode.modified:
850 if len(recipes_other_names):
851 logger.error("Only in shared sysroots mode not modified recipes %s can be handled." % str(
852 recipes_other_names))
853 invalid_params = True
854 if len(recipes_image_names) != 1:
855 logger.error(
856 "One image recipe is required as the rootfs for the remote development.")
857 invalid_params = True
858 for modified_recipe_name in recipes_modified_names:
859 if modified_recipe_name.startswith('nativesdk-') or modified_recipe_name.endswith('-native'):
860 logger.error(
861 "Only cross compiled recipes are support. %s is not cross." % modified_recipe_name)
862 invalid_params = True
863
864 if invalid_params:
865 raise DevtoolError("Invalid parameters are passed.")
866
867 # For the shared sysroots mode, add all dependencies of all the images to the sysroots
868 # For the modified mode provide one rootfs and the corresponding debug symbols via rootfs-dbg
869 recipes_images = []
870 for recipes_image_name in recipes_image_names:
871 logger.info("Using image: %s" % recipes_image_name)
872 recipe_image = RecipeImage(recipes_image_name)
873 recipe_image.initialize(config, tinfoil)
874 bootstrap_tasks += recipe_image.bootstrap_tasks
875 recipes_images.append(recipe_image)
876
877 # Provide a Direct SDK with shared sysroots
878 recipes_not_modified = []
879 if args.mode == DevtoolIdeMode.shared:
880 ide_support = RecipeMetaIdeSupport()
881 ide_support.initialize(config, tinfoil)
882 bootstrap_tasks += ide_support.bootstrap_tasks
883
884 logger.info("Adding %s to the Direct SDK sysroots." %
885 str(recipes_other_names))
886 for recipe_name in recipes_other_names:
887 recipe_not_modified = RecipeNotModified(recipe_name)
888 bootstrap_tasks += recipe_not_modified.bootstrap_tasks
889 recipes_not_modified.append(recipe_not_modified)
890
891 build_sysroots = RecipeBuildSysroots()
892 build_sysroots.initialize(config, tinfoil)
893 bootstrap_tasks_late += build_sysroots.bootstrap_tasks
894 shared_env = SharedSysrootsEnv()
895 shared_env.initialize(ide_support, build_sysroots)
896
897 recipes_modified = []
898 if args.mode == DevtoolIdeMode.modified:
899 logger.info("Setting up workspaces for modified recipe: %s" %
900 str(recipes_modified_names))
901 gdbs_cross = {}
902 for recipe_name in recipes_modified_names:
903 recipe_modified = RecipeModified(recipe_name)
904 recipe_modified.initialize(config, workspace, tinfoil)
905 bootstrap_tasks += recipe_modified.bootstrap_tasks
906 recipes_modified.append(recipe_modified)
907
908 if recipe_modified.target_arch not in gdbs_cross:
909 target_device = TargetDevice(args)
910 gdb_cross = RecipeGdbCross(
911 args, recipe_modified.target_arch, target_device)
912 gdb_cross.initialize(config, workspace, tinfoil)
913 bootstrap_tasks += gdb_cross.bootstrap_tasks
914 gdbs_cross[recipe_modified.target_arch] = gdb_cross
915 recipe_modified.gdb_cross = gdbs_cross[recipe_modified.target_arch]
916
917 finally:
918 tinfoil.shutdown()
919
920 if not args.skip_bitbake:
921 bb_cmd = 'bitbake '
922 if args.bitbake_k:
923 bb_cmd += "-k "
924 bb_cmd_early = bb_cmd + ' '.join(bootstrap_tasks)
925 exec_build_env_command(
926 config.init_path, basepath, bb_cmd_early, watch=True)
927 if bootstrap_tasks_late:
928 bb_cmd_late = bb_cmd + ' '.join(bootstrap_tasks_late)
929 exec_build_env_command(
930 config.init_path, basepath, bb_cmd_late, watch=True)
931
932 for recipe_image in recipes_images:
933 if (recipe_image.gdbserver_missing):
934 logger.warning(
935 "gdbserver not installed in image %s. Remote debugging will not be available" % recipe_image)
936
937 if recipe_image.combine_dbg_image is False:
938 logger.warning(
939 'IMAGE_CLASSES += "image-combined-dbg" is missing for image %s. Remote debugging will not find debug symbols from rootfs-dbg.' % recipe_image)
940
941 # Instantiate the active IDE plugin
942 ide = ide_plugins[args.ide]()
943 if args.mode == DevtoolIdeMode.shared:
944 ide.setup_shared_sysroots(shared_env)
945 elif args.mode == DevtoolIdeMode.modified:
946 for recipe_modified in recipes_modified:
947 if recipe_modified.build_tool is BuildTool.CMAKE:
948 recipe_modified.cmake_preset()
949 if recipe_modified.build_tool is BuildTool.MESON:
950 recipe_modified.gen_meson_wrapper()
951 ide.setup_modified_recipe(
952 args, recipe_image, recipe_modified)
953 else:
954 raise DevtoolError("Must not end up here.")
955
956
957def register_commands(subparsers, context):
958 """Register devtool subcommands from this plugin"""
959
960 global ide_plugins
961
962 # Search for IDE plugins in all sub-folders named ide_plugins where devtool seraches for plugins.
963 pluginpaths = [os.path.join(path, 'ide_plugins')
964 for path in context.pluginpaths]
965 ide_plugin_modules = []
966 for pluginpath in pluginpaths:
967 scriptutils.load_plugins(logger, ide_plugin_modules, pluginpath)
968
969 for ide_plugin_module in ide_plugin_modules:
970 if hasattr(ide_plugin_module, 'register_ide_plugin'):
971 ide_plugin_module.register_ide_plugin(ide_plugins)
972 # Sort plugins according to their priority. The first entry is the default IDE plugin.
973 ide_plugins = dict(sorted(ide_plugins.items(),
974 key=lambda p: p[1].ide_plugin_priority(), reverse=True))
975
976 parser_ide_sdk = subparsers.add_parser('ide-sdk', group='working', order=50, formatter_class=RawTextHelpFormatter,
977 help='Setup the SDK and configure the IDE')
978 parser_ide_sdk.add_argument(
979 'recipenames', nargs='+', help='Generate an IDE configuration suitable to work on the given recipes.\n'
980 'Depending on the --mode paramter different types of SDKs and IDE configurations are generated.')
981 parser_ide_sdk.add_argument(
982 '-m', '--mode', type=DevtoolIdeMode, default=DevtoolIdeMode.modified,
983 help='Different SDK types are supported:\n'
984 '- "' + DevtoolIdeMode.modified.name + '" (default):\n'
985 ' devtool modify creates a workspace to work on the source code of a recipe.\n'
986 ' devtool ide-sdk builds the SDK and generates the IDE configuration(s) in the workspace directorie(s)\n'
987 ' Usage example:\n'
988 ' devtool modify cmake-example\n'
989 ' devtool ide-sdk cmake-example core-image-minimal\n'
990 ' Start the IDE in the workspace folder\n'
991 ' At least one devtool modified recipe plus one image recipe are required:\n'
992 ' The image recipe is used to generate the target image and the remote debug configuration.\n'
993 '- "' + DevtoolIdeMode.shared.name + '":\n'
994 ' Usage example:\n'
995 ' devtool ide-sdk -m ' + DevtoolIdeMode.shared.name + ' recipe(s)\n'
996 ' This command generates a cross-toolchain as well as the corresponding shared sysroot directories.\n'
997 ' To use this tool-chain the environment-* file found in the deploy..image folder needs to be sourced into a shell.\n'
998 ' In case of VSCode and cmake the tool-chain is also exposed as a cmake-kit')
999 default_ide = list(ide_plugins.keys())[0]
1000 parser_ide_sdk.add_argument(
1001 '-i', '--ide', choices=ide_plugins.keys(), default=default_ide,
1002 help='Setup the configuration for this IDE (default: %s)' % default_ide)
1003 parser_ide_sdk.add_argument(
1004 '-t', '--target', default='root@192.168.7.2',
1005 help='Live target machine running an ssh server: user@hostname.')
1006 parser_ide_sdk.add_argument(
1007 '-G', '--gdbserver-port-start', default="1234", help='port where gdbserver is listening.')
1008 parser_ide_sdk.add_argument(
1009 '-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
1010 parser_ide_sdk.add_argument(
1011 '-e', '--ssh-exec', help='Executable to use in place of ssh')
1012 parser_ide_sdk.add_argument(
1013 '-P', '--port', help='Specify ssh port to use for connection to the target')
1014 parser_ide_sdk.add_argument(
1015 '-I', '--key', help='Specify ssh private key for connection to the target')
1016 parser_ide_sdk.add_argument(
1017 '--skip-bitbake', help='Generate IDE configuration but skip calling bibtake to update the SDK.', action='store_true')
1018 parser_ide_sdk.add_argument(
1019 '-k', '--bitbake-k', help='Pass -k parameter to bitbake', action='store_true')
1020 parser_ide_sdk.add_argument(
1021 '--no-strip', help='Do not strip executables prior to deploy', dest='strip', action='store_false')
1022 parser_ide_sdk.add_argument(
1023 '-n', '--dry-run', help='List files to be undeployed only', action='store_true')
1024 parser_ide_sdk.add_argument(
1025 '-s', '--show-status', help='Show progress/status output', action='store_true')
1026 parser_ide_sdk.add_argument(
1027 '-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
1028 parser_ide_sdk.add_argument(
1029 '--no-check-space', help='Do not check for available space before deploying', action='store_true')
1030 parser_ide_sdk.add_argument(
1031 '--debug-build-config', help='Use debug build flags, for example set CMAKE_BUILD_TYPE=Debug', action='store_true')
1032 parser_ide_sdk.set_defaults(func=ide_setup)