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