summaryrefslogtreecommitdiffstats
path: root/scripts/lib/devtool/ide_plugins/__init__.py
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/lib/devtool/ide_plugins/__init__.py
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/lib/devtool/ide_plugins/__init__.py')
-rw-r--r--scripts/lib/devtool/ide_plugins/__init__.py267
1 files changed, 267 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