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.py1012
1 files changed, 0 insertions, 1012 deletions
diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
deleted file mode 100755
index 87a4c13ec5..0000000000
--- a/scripts/lib/devtool/ide_sdk.py
+++ /dev/null
@@ -1,1012 +0,0 @@
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 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.bitbakepath = None
290 self.bpn = None
291 self.d = None
292 self.debug_build = None
293 self.fakerootcmd = None
294 self.fakerootenv = None
295 self.libdir = None
296 self.max_process = None
297 self.package_arch = None
298 self.package_debug_split_style = None
299 self.path = None
300 self.pn = None
301 self.recipe_id = None
302 self.recipe_sysroot = None
303 self.recipe_sysroot_native = None
304 self.staging_incdir = None
305 self.strip_cmd = None
306 self.target_arch = None
307 self.target_dbgsrc_dir = None
308 self.topdir = None
309 self.workdir = None
310 # replicate bitbake build environment
311 self.exported_vars = None
312 self.cmd_compile = None
313 self.__oe_init_dir = None
314 # main build tool used by this recipe
315 self.build_tool = BuildTool.UNDEFINED
316 # build_tool = cmake
317 self.oecmake_generator = None
318 self.cmake_cache_vars = None
319 # build_tool = meson
320 self.meson_buildtype = None
321 self.meson_wrapper = None
322 self.mesonopts = None
323 self.extra_oemeson = None
324 self.meson_cross_file = None
325
326 def initialize(self, config, workspace, tinfoil):
327 recipe_d = parse_recipe(
328 config, tinfoil, self.name, appends=True, filter_workspace=False)
329 if not recipe_d:
330 raise DevtoolError("Parsing %s recipe failed" % self.name)
331
332 # Verify this recipe is built as externalsrc setup by devtool modify
333 workspacepn = check_workspace_recipe(
334 workspace, self.name, bbclassextend=True)
335 self.srctree = workspace[workspacepn]['srctree']
336 # Need to grab this here in case the source is within a subdirectory
337 self.real_srctree = get_real_srctree(
338 self.srctree, recipe_d.getVar('S'), recipe_d.getVar('UNPACKDIR'))
339 self.bbappend = workspace[workspacepn]['bbappend']
340
341 self.ide_sdk_dir = os.path.join(
342 config.workspace_path, 'ide-sdk', self.name)
343 if os.path.exists(self.ide_sdk_dir):
344 shutil.rmtree(self.ide_sdk_dir)
345 self.ide_sdk_scripts_dir = os.path.join(self.ide_sdk_dir, 'scripts')
346
347 self.b = recipe_d.getVar('B')
348 self.base_libdir = recipe_d.getVar('base_libdir')
349 self.bblayers = recipe_d.getVar('BBLAYERS').split()
350 self.bitbakepath = recipe_d.getVar('BITBAKEPATH')
351 self.bpn = recipe_d.getVar('BPN')
352 self.cxx = recipe_d.getVar('CXX')
353 self.d = recipe_d.getVar('D')
354 self.debug_build = recipe_d.getVar('DEBUG_BUILD')
355 self.fakerootcmd = recipe_d.getVar('FAKEROOTCMD')
356 self.fakerootenv = recipe_d.getVar('FAKEROOTENV')
357 self.libdir = recipe_d.getVar('libdir')
358 self.max_process = int(recipe_d.getVar(
359 "BB_NUMBER_THREADS") or os.cpu_count() or 1)
360 self.package_arch = recipe_d.getVar('PACKAGE_ARCH')
361 self.package_debug_split_style = recipe_d.getVar(
362 'PACKAGE_DEBUG_SPLIT_STYLE')
363 self.path = recipe_d.getVar('PATH')
364 self.pn = recipe_d.getVar('PN')
365 self.recipe_sysroot = os.path.realpath(
366 recipe_d.getVar('RECIPE_SYSROOT'))
367 self.recipe_sysroot_native = os.path.realpath(
368 recipe_d.getVar('RECIPE_SYSROOT_NATIVE'))
369 self.staging_bindir_toolchain = os.path.realpath(
370 recipe_d.getVar('STAGING_BINDIR_TOOLCHAIN'))
371 self.staging_incdir = os.path.realpath(
372 recipe_d.getVar('STAGING_INCDIR'))
373 self.strip_cmd = recipe_d.getVar('STRIP')
374 self.target_arch = recipe_d.getVar('TARGET_ARCH')
375 self.target_dbgsrc_dir = recipe_d.getVar('TARGET_DBGSRC_DIR')
376 self.topdir = recipe_d.getVar('TOPDIR')
377 self.workdir = os.path.realpath(recipe_d.getVar('WORKDIR'))
378
379 self.__init_exported_variables(recipe_d)
380
381 if bb.data.inherits_class('cmake', recipe_d):
382 self.oecmake_generator = recipe_d.getVar('OECMAKE_GENERATOR')
383 self.__init_cmake_preset_cache(recipe_d)
384 self.build_tool = BuildTool.CMAKE
385 elif bb.data.inherits_class('meson', recipe_d):
386 self.meson_buildtype = recipe_d.getVar('MESON_BUILDTYPE')
387 self.mesonopts = recipe_d.getVar('MESONOPTS')
388 self.extra_oemeson = recipe_d.getVar('EXTRA_OEMESON')
389 self.meson_cross_file = recipe_d.getVar('MESON_CROSS_FILE')
390 self.build_tool = BuildTool.MESON
391
392 # Recipe ID is the identifier for IDE config sections
393 self.recipe_id = self.bpn + "-" + self.package_arch
394 self.recipe_id_pretty = self.bpn + ": " + self.package_arch
395
396 @staticmethod
397 def is_valid_shell_variable(var):
398 """Skip strange shell variables like systemd
399
400 prevent from strange bugs because of strange variables which
401 are not used in this context but break various tools.
402 """
403 if RecipeModified.VALID_BASH_ENV_NAME_CHARS.match(var):
404 bb.debug(1, "ignoring variable: %s" % var)
405 return True
406 return False
407
408 def solib_search_path(self, image):
409 """Search for debug symbols in the rootfs and rootfs-dbg
410
411 The debug symbols of shared libraries which are provided by other packages
412 are grabbed from the -dbg packages in the rootfs-dbg.
413
414 But most cross debugging tools like gdb, perf, and systemtap need to find
415 executable/library first and through it debuglink note find corresponding
416 symbols file. Therefore the library paths from the rootfs are added as well.
417
418 Note: For the devtool modified recipe compiled from the IDE, the debug
419 symbols are taken from the unstripped binaries in the image folder.
420 Also, devtool deploy-target takes the files from the image folder.
421 debug symbols in the image folder refer to the corresponding source files
422 with absolute paths of the build machine. Debug symbols found in the
423 rootfs-dbg are relocated and contain paths which refer to the source files
424 installed on the target device e.g. /usr/src/...
425 """
426 base_libdir = self.base_libdir.lstrip('/')
427 libdir = self.libdir.lstrip('/')
428 so_paths = [
429 # debug symbols for package_debug_split_style: debug-with-srcpkg or .debug
430 os.path.join(image.rootfs_dbg, base_libdir, ".debug"),
431 os.path.join(image.rootfs_dbg, libdir, ".debug"),
432 # debug symbols for package_debug_split_style: debug-file-directory
433 os.path.join(image.rootfs_dbg, "usr", "lib", "debug"),
434
435 # The binaries are required as well, the debug packages are not enough
436 # With image-combined-dbg.bbclass the binaries are copied into rootfs-dbg
437 os.path.join(image.rootfs_dbg, base_libdir),
438 os.path.join(image.rootfs_dbg, libdir),
439 # Without image-combined-dbg.bbclass the binaries are only in rootfs.
440 # Note: Stepping into source files located in rootfs-dbg does not
441 # work without image-combined-dbg.bbclass yet.
442 os.path.join(image.rootfs, base_libdir),
443 os.path.join(image.rootfs, libdir)
444 ]
445 return so_paths
446
447 def solib_search_path_str(self, image):
448 """Return a : separated list of paths usable by GDB's set solib-search-path"""
449 return ':'.join(self.solib_search_path(image))
450
451 def __init_exported_variables(self, d):
452 """Find all variables with export flag set.
453
454 This allows to generate IDE configurations which compile with the same
455 environment as bitbake does. That's at least a reasonable default behavior.
456 """
457 exported_vars = {}
458
459 vars = (key for key in d.keys() if not key.startswith(
460 "__") and not d.getVarFlag(key, "func", False))
461 for var in sorted(vars):
462 func = d.getVarFlag(var, "func", False)
463 if d.getVarFlag(var, 'python', False) and func:
464 continue
465 export = d.getVarFlag(var, "export", False)
466 unexport = d.getVarFlag(var, "unexport", False)
467 if not export and not unexport and not func:
468 continue
469 if unexport:
470 continue
471
472 val = d.getVar(var)
473 if val is None:
474 continue
475 if set(var) & set("-.{}+"):
476 logger.warn(
477 "Warning: Found invalid character in variable name %s", str(var))
478 continue
479 varExpanded = d.expand(var)
480 val = str(val)
481
482 if not RecipeModified.is_valid_shell_variable(varExpanded):
483 continue
484
485 if func:
486 code_line = "line: {0}, file: {1}\n".format(
487 d.getVarFlag(var, "lineno", False),
488 d.getVarFlag(var, "filename", False))
489 val = val.rstrip('\n')
490 logger.warn("Warning: exported shell function %s() is not exported (%s)" %
491 (varExpanded, code_line))
492 continue
493
494 if export:
495 exported_vars[varExpanded] = val.strip()
496 continue
497
498 self.exported_vars = exported_vars
499
500 def __init_cmake_preset_cache(self, d):
501 """Get the arguments passed to cmake
502
503 Replicate the cmake configure arguments with all details to
504 share on build folder between bitbake and SDK.
505 """
506 site_file = os.path.join(self.workdir, 'site-file.cmake')
507 if os.path.exists(site_file):
508 print("Warning: site-file.cmake is not supported")
509
510 cache_vars = {}
511 oecmake_args = d.getVar('OECMAKE_ARGS').split()
512 extra_oecmake = d.getVar('EXTRA_OECMAKE').split()
513 for param in sorted(oecmake_args + extra_oecmake):
514 d_pref = "-D"
515 if param.startswith(d_pref):
516 param = param[len(d_pref):]
517 else:
518 print("Error: expected a -D")
519 param_s = param.split('=', 1)
520 param_nt = param_s[0].split(':', 1)
521
522 def handle_undefined_variable(var):
523 if var.startswith('${') and var.endswith('}'):
524 return ''
525 else:
526 return var
527 # Example: FOO=ON
528 if len(param_nt) == 1:
529 cache_vars[param_s[0]] = handle_undefined_variable(param_s[1])
530 # Example: FOO:PATH=/tmp
531 elif len(param_nt) == 2:
532 cache_vars[param_nt[0]] = {
533 "type": param_nt[1],
534 "value": handle_undefined_variable(param_s[1]),
535 }
536 else:
537 print("Error: cannot parse %s" % param)
538 self.cmake_cache_vars = cache_vars
539
540 def cmake_preset(self):
541 """Create a preset for cmake that mimics how bitbake calls cmake"""
542 toolchain_file = os.path.join(self.workdir, 'toolchain.cmake')
543 cmake_executable = os.path.join(
544 self.recipe_sysroot_native, 'usr', 'bin', 'cmake')
545 self.cmd_compile = cmake_executable + " --build --preset " + self.recipe_id
546
547 preset_dict_configure = {
548 "name": self.recipe_id,
549 "displayName": self.recipe_id_pretty,
550 "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
551 "binaryDir": self.b,
552 "generator": self.oecmake_generator,
553 "toolchainFile": toolchain_file,
554 "cacheVariables": self.cmake_cache_vars,
555 "environment": self.exported_vars,
556 "cmakeExecutable": cmake_executable
557 }
558
559 preset_dict_build = {
560 "name": self.recipe_id,
561 "displayName": self.recipe_id_pretty,
562 "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
563 "configurePreset": self.recipe_id,
564 "inheritConfigureEnvironment": True
565 }
566
567 preset_dict_test = {
568 "name": self.recipe_id,
569 "displayName": self.recipe_id_pretty,
570 "description": "Bitbake build environment for the recipe %s compiled for %s" % (self.bpn, self.package_arch),
571 "configurePreset": self.recipe_id,
572 "inheritConfigureEnvironment": True
573 }
574
575 preset_dict = {
576 "version": 3, # cmake 3.21, backward compatible with kirkstone
577 "configurePresets": [preset_dict_configure],
578 "buildPresets": [preset_dict_build],
579 "testPresets": [preset_dict_test]
580 }
581
582 # Finally write the json file
583 json_file = 'CMakeUserPresets.json'
584 json_path = os.path.join(self.real_srctree, json_file)
585 logger.info("Updating CMake preset: %s (%s)" % (json_file, json_path))
586 if not os.path.exists(self.real_srctree):
587 os.makedirs(self.real_srctree)
588 try:
589 with open(json_path) as f:
590 orig_dict = json.load(f)
591 except json.decoder.JSONDecodeError:
592 logger.info(
593 "Decoding %s failed. Probably because of comments in the json file" % json_path)
594 orig_dict = {}
595 except FileNotFoundError:
596 orig_dict = {}
597
598 # Add or update the presets for the recipe and keep other presets
599 for k, v in preset_dict.items():
600 if isinstance(v, list):
601 update_preset = v[0]
602 preset_added = False
603 if k in orig_dict:
604 for index, orig_preset in enumerate(orig_dict[k]):
605 if 'name' in orig_preset:
606 if orig_preset['name'] == update_preset['name']:
607 logger.debug("Updating preset: %s" %
608 orig_preset['name'])
609 orig_dict[k][index] = update_preset
610 preset_added = True
611 break
612 else:
613 logger.debug("keeping preset: %s" %
614 orig_preset['name'])
615 else:
616 logger.warn("preset without a name found")
617 if not preset_added:
618 if not k in orig_dict:
619 orig_dict[k] = []
620 orig_dict[k].append(update_preset)
621 logger.debug("Added preset: %s" %
622 update_preset['name'])
623 else:
624 orig_dict[k] = v
625
626 with open(json_path, 'w') as f:
627 json.dump(orig_dict, f, indent=4)
628
629 def gen_meson_wrapper(self):
630 """Generate a wrapper script to call meson with the cross environment"""
631 bb.utils.mkdirhier(self.ide_sdk_scripts_dir)
632 meson_wrapper = os.path.join(self.ide_sdk_scripts_dir, 'meson')
633 meson_real = os.path.join(
634 self.recipe_sysroot_native, 'usr', 'bin', 'meson.real')
635 with open(meson_wrapper, 'w') as mwrap:
636 mwrap.write("#!/bin/sh" + os.linesep)
637 for var, val in self.exported_vars.items():
638 mwrap.write('export %s="%s"' % (var, val) + os.linesep)
639 mwrap.write("unset CC CXX CPP LD AR NM STRIP" + os.linesep)
640 private_temp = os.path.join(self.b, "meson-private", "tmp")
641 mwrap.write('mkdir -p "%s"' % private_temp + os.linesep)
642 mwrap.write('export TMPDIR="%s"' % private_temp + os.linesep)
643 mwrap.write('exec "%s" "$@"' % meson_real + os.linesep)
644 st = os.stat(meson_wrapper)
645 os.chmod(meson_wrapper, st.st_mode | stat.S_IEXEC)
646 self.meson_wrapper = meson_wrapper
647 self.cmd_compile = meson_wrapper + " compile -C " + self.b
648
649 def which(self, executable):
650 bin_path = shutil.which(executable, path=self.path)
651 if not bin_path:
652 raise DevtoolError(
653 'Cannot find %s. Probably the recipe %s is not built yet.' % (executable, self.bpn))
654 return bin_path
655
656 @staticmethod
657 def is_elf_file(file_path):
658 with open(file_path, "rb") as f:
659 data = f.read(4)
660 if data == b'\x7fELF':
661 return True
662 return False
663
664 def find_installed_binaries(self):
665 """find all executable elf files in the image directory"""
666 binaries = []
667 d_len = len(self.d)
668 re_so = re.compile(r'.*\.so[.0-9]*$')
669 for root, _, files in os.walk(self.d, followlinks=False):
670 for file in files:
671 if os.path.islink(file):
672 continue
673 if re_so.match(file):
674 continue
675 abs_name = os.path.join(root, file)
676 if os.access(abs_name, os.X_OK) and RecipeModified.is_elf_file(abs_name):
677 binaries.append(abs_name[d_len:])
678 return sorted(binaries)
679
680 def gen_deploy_target_script(self, args):
681 """Generate a script which does what devtool deploy-target does
682
683 This script is much quicker than devtool target-deploy. Because it
684 does not need to start a bitbake server. All information from tinfoil
685 is hard-coded in the generated script.
686 """
687 cmd_lines = ['#!%s' % str(sys.executable)]
688 cmd_lines.append('import sys')
689 cmd_lines.append('devtool_sys_path = %s' % str(sys.path))
690 cmd_lines.append('devtool_sys_path.reverse()')
691 cmd_lines.append('for p in devtool_sys_path:')
692 cmd_lines.append(' if p not in sys.path:')
693 cmd_lines.append(' sys.path.insert(0, p)')
694 cmd_lines.append('from devtool.deploy import deploy_no_d')
695 args_filter = ['debug', 'dry_run', 'key', 'no_check_space', 'no_host_check',
696 'no_preserve', 'port', 'show_status', 'ssh_exec', 'strip', 'target']
697 filtered_args_dict = {key: value for key, value in vars(
698 args).items() if key in args_filter}
699 cmd_lines.append('filtered_args_dict = %s' % str(filtered_args_dict))
700 cmd_lines.append('class Dict2Class(object):')
701 cmd_lines.append(' def __init__(self, my_dict):')
702 cmd_lines.append(' for key in my_dict:')
703 cmd_lines.append(' setattr(self, key, my_dict[key])')
704 cmd_lines.append('filtered_args = Dict2Class(filtered_args_dict)')
705 cmd_lines.append(
706 'setattr(filtered_args, "recipename", "%s")' % self.bpn)
707 cmd_lines.append('deploy_no_d("%s", "%s", "%s", "%s", "%s", "%s", %d, "%s", "%s", filtered_args)' %
708 (self.d, self.workdir, self.path, self.strip_cmd,
709 self.libdir, self.base_libdir, self.max_process,
710 self.fakerootcmd, self.fakerootenv))
711 return self.write_script(cmd_lines, 'deploy_target')
712
713 def gen_install_deploy_script(self, args):
714 """Generate a script which does install and deploy"""
715 cmd_lines = ['#!/bin/sh']
716
717 # . oe-init-build-env $BUILDDIR $BITBAKEDIR
718 # Using 'set' to pass the build directory to oe-init-build-env in sh syntax
719 cmd_lines.append('cd "%s" || { echo "cd %s failed"; exit 1; }' % (
720 self.oe_init_dir, self.oe_init_dir))
721 cmd_lines.append('set %s %s' % (self.topdir, self.bitbakepath.rstrip('/bin')))
722 cmd_lines.append('. "%s" || { echo ". %s %s failed"; exit 1; }' % (
723 self.oe_init_build_env, self.oe_init_build_env, self.topdir))
724
725 # bitbake -c install
726 cmd_lines.append(
727 'bitbake %s -c install --force || { echo "bitbake %s -c install --force failed"; exit 1; }' % (self.bpn, self.bpn))
728
729 # Self contained devtool deploy-target
730 cmd_lines.append(self.gen_deploy_target_script(args))
731
732 return self.write_script(cmd_lines, 'install_and_deploy')
733
734 def write_script(self, cmd_lines, script_name):
735 bb.utils.mkdirhier(self.ide_sdk_scripts_dir)
736 script_name_arch = script_name + '_' + self.recipe_id
737 script_file = os.path.join(self.ide_sdk_scripts_dir, script_name_arch)
738 with open(script_file, 'w') as script_f:
739 script_f.write(os.linesep.join(cmd_lines))
740 st = os.stat(script_file)
741 os.chmod(script_file, st.st_mode | stat.S_IEXEC)
742 return script_file
743
744 @property
745 def oe_init_build_env(self):
746 """Find the oe-init-build-env used for this setup"""
747 oe_init_dir = self.oe_init_dir
748 if oe_init_dir:
749 return os.path.join(oe_init_dir, RecipeModified.OE_INIT_BUILD_ENV)
750 return None
751
752 @property
753 def oe_init_dir(self):
754 """Find the directory where the oe-init-build-env is located
755
756 Assumption: There might be a layer with higher priority than poky
757 which provides to oe-init-build-env in the layer's toplevel folder.
758 """
759 if not self.__oe_init_dir:
760 for layer in reversed(self.bblayers):
761 result = subprocess.run(
762 ['git', 'rev-parse', '--show-toplevel'], cwd=layer, capture_output=True)
763 if result.returncode == 0:
764 oe_init_dir = result.stdout.decode('utf-8').strip()
765 oe_init_path = os.path.join(
766 oe_init_dir, RecipeModified.OE_INIT_BUILD_ENV)
767 if os.path.exists(oe_init_path):
768 logger.debug("Using %s from: %s" % (
769 RecipeModified.OE_INIT_BUILD_ENV, oe_init_path))
770 self.__oe_init_dir = oe_init_dir
771 break
772 if not self.__oe_init_dir:
773 logger.error("Cannot find the bitbake top level folder")
774 return self.__oe_init_dir
775
776
777def ide_setup(args, config, basepath, workspace):
778 """Generate the IDE configuration for the workspace"""
779
780 # Explicitely passing some special recipes does not make sense
781 for recipe in args.recipenames:
782 if recipe in ['meta-ide-support', 'build-sysroots']:
783 raise DevtoolError("Invalid recipe: %s." % recipe)
784
785 # Collect information about tasks which need to be bitbaked
786 bootstrap_tasks = []
787 bootstrap_tasks_late = []
788 tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
789 try:
790 # define mode depending on recipes which need to be processed
791 recipes_image_names = []
792 recipes_modified_names = []
793 recipes_other_names = []
794 for recipe in args.recipenames:
795 try:
796 check_workspace_recipe(
797 workspace, recipe, bbclassextend=True)
798 recipes_modified_names.append(recipe)
799 except DevtoolError:
800 recipe_d = parse_recipe(
801 config, tinfoil, recipe, appends=True, filter_workspace=False)
802 if not recipe_d:
803 raise DevtoolError("Parsing recipe %s failed" % recipe)
804 if bb.data.inherits_class('image', recipe_d):
805 recipes_image_names.append(recipe)
806 else:
807 recipes_other_names.append(recipe)
808
809 invalid_params = False
810 if args.mode == DevtoolIdeMode.shared:
811 if len(recipes_modified_names):
812 logger.error("In shared sysroots mode modified recipes %s cannot be handled." % str(
813 recipes_modified_names))
814 invalid_params = True
815 if args.mode == DevtoolIdeMode.modified:
816 if len(recipes_other_names):
817 logger.error("Only in shared sysroots mode not modified recipes %s can be handled." % str(
818 recipes_other_names))
819 invalid_params = True
820 if len(recipes_image_names) != 1:
821 logger.error(
822 "One image recipe is required as the rootfs for the remote development.")
823 invalid_params = True
824 for modified_recipe_name in recipes_modified_names:
825 if modified_recipe_name.startswith('nativesdk-') or modified_recipe_name.endswith('-native'):
826 logger.error(
827 "Only cross compiled recipes are support. %s is not cross." % modified_recipe_name)
828 invalid_params = True
829
830 if invalid_params:
831 raise DevtoolError("Invalid parameters are passed.")
832
833 # For the shared sysroots mode, add all dependencies of all the images to the sysroots
834 # For the modified mode provide one rootfs and the corresponding debug symbols via rootfs-dbg
835 recipes_images = []
836 for recipes_image_name in recipes_image_names:
837 logger.info("Using image: %s" % recipes_image_name)
838 recipe_image = RecipeImage(recipes_image_name)
839 recipe_image.initialize(config, tinfoil)
840 bootstrap_tasks += recipe_image.bootstrap_tasks
841 recipes_images.append(recipe_image)
842
843 # Provide a Direct SDK with shared sysroots
844 recipes_not_modified = []
845 if args.mode == DevtoolIdeMode.shared:
846 ide_support = RecipeMetaIdeSupport()
847 ide_support.initialize(config, tinfoil)
848 bootstrap_tasks += ide_support.bootstrap_tasks
849
850 logger.info("Adding %s to the Direct SDK sysroots." %
851 str(recipes_other_names))
852 for recipe_name in recipes_other_names:
853 recipe_not_modified = RecipeNotModified(recipe_name)
854 bootstrap_tasks += recipe_not_modified.bootstrap_tasks
855 recipes_not_modified.append(recipe_not_modified)
856
857 build_sysroots = RecipeBuildSysroots()
858 build_sysroots.initialize(config, tinfoil)
859 bootstrap_tasks_late += build_sysroots.bootstrap_tasks
860 shared_env = SharedSysrootsEnv()
861 shared_env.initialize(ide_support, build_sysroots)
862
863 recipes_modified = []
864 if args.mode == DevtoolIdeMode.modified:
865 logger.info("Setting up workspaces for modified recipe: %s" %
866 str(recipes_modified_names))
867 gdbs_cross = {}
868 for recipe_name in recipes_modified_names:
869 recipe_modified = RecipeModified(recipe_name)
870 recipe_modified.initialize(config, workspace, tinfoil)
871 bootstrap_tasks += recipe_modified.bootstrap_tasks
872 recipes_modified.append(recipe_modified)
873
874 if recipe_modified.target_arch not in gdbs_cross:
875 target_device = TargetDevice(args)
876 gdb_cross = RecipeGdbCross(
877 args, recipe_modified.target_arch, target_device)
878 gdb_cross.initialize(config, workspace, tinfoil)
879 bootstrap_tasks += gdb_cross.bootstrap_tasks
880 gdbs_cross[recipe_modified.target_arch] = gdb_cross
881 recipe_modified.gdb_cross = gdbs_cross[recipe_modified.target_arch]
882
883 finally:
884 tinfoil.shutdown()
885
886 if not args.skip_bitbake:
887 bb_cmd = 'bitbake '
888 if args.bitbake_k:
889 bb_cmd += "-k "
890 bb_cmd_early = bb_cmd + ' '.join(bootstrap_tasks)
891 exec_build_env_command(
892 config.init_path, basepath, bb_cmd_early, watch=True)
893 if bootstrap_tasks_late:
894 bb_cmd_late = bb_cmd + ' '.join(bootstrap_tasks_late)
895 exec_build_env_command(
896 config.init_path, basepath, bb_cmd_late, watch=True)
897
898 for recipe_image in recipes_images:
899 if (recipe_image.gdbserver_missing):
900 logger.warning(
901 "gdbserver not installed in image %s. Remote debugging will not be available" % recipe_image)
902
903 if recipe_image.combine_dbg_image is False:
904 logger.warning(
905 'IMAGE_CLASSES += "image-combined-dbg" is missing for image %s. Remote debugging will not find debug symbols from rootfs-dbg.' % recipe_image)
906
907 # Instantiate the active IDE plugin
908 ide = ide_plugins[args.ide]()
909 if args.mode == DevtoolIdeMode.shared:
910 ide.setup_shared_sysroots(shared_env)
911 elif args.mode == DevtoolIdeMode.modified:
912 for recipe_modified in recipes_modified:
913 if recipe_modified.build_tool is BuildTool.CMAKE:
914 recipe_modified.cmake_preset()
915 if recipe_modified.build_tool is BuildTool.MESON:
916 recipe_modified.gen_meson_wrapper()
917 ide.setup_modified_recipe(
918 args, recipe_image, recipe_modified)
919
920 if recipe_modified.debug_build != '1':
921 logger.warn(
922 'Recipe %s is compiled with release build configuration. '
923 'You might want to add DEBUG_BUILD = "1" to %s. '
924 'Note that devtool modify --debug-build can do this automatically.',
925 recipe_modified.name, recipe_modified.bbappend)
926 else:
927 raise DevtoolError("Must not end up here.")
928
929
930def register_commands(subparsers, context):
931 """Register devtool subcommands from this plugin"""
932
933 # The ide-sdk command bootstraps the SDK from the bitbake environment before the IDE
934 # configuration is generated. In the case of the eSDK, the bootstrapping is performed
935 # during the installation of the eSDK installer. Running the ide-sdk plugin from an
936 # eSDK installer-based setup would require skipping the bootstrapping and probably
937 # taking some other differences into account when generating the IDE configurations.
938 # This would be possible. But it is not implemented.
939 if context.fixed_setup:
940 return
941
942 global ide_plugins
943
944 # Search for IDE plugins in all sub-folders named ide_plugins where devtool seraches for plugins.
945 pluginpaths = [os.path.join(path, 'ide_plugins')
946 for path in context.pluginpaths]
947 ide_plugin_modules = []
948 for pluginpath in pluginpaths:
949 scriptutils.load_plugins(logger, ide_plugin_modules, pluginpath)
950
951 for ide_plugin_module in ide_plugin_modules:
952 if hasattr(ide_plugin_module, 'register_ide_plugin'):
953 ide_plugin_module.register_ide_plugin(ide_plugins)
954 # Sort plugins according to their priority. The first entry is the default IDE plugin.
955 ide_plugins = dict(sorted(ide_plugins.items(),
956 key=lambda p: p[1].ide_plugin_priority(), reverse=True))
957
958 parser_ide_sdk = subparsers.add_parser('ide-sdk', group='working', order=50, formatter_class=RawTextHelpFormatter,
959 help='Setup the SDK and configure the IDE')
960 parser_ide_sdk.add_argument(
961 'recipenames', nargs='+', help='Generate an IDE configuration suitable to work on the given recipes.\n'
962 'Depending on the --mode parameter different types of SDKs and IDE configurations are generated.')
963 parser_ide_sdk.add_argument(
964 '-m', '--mode', type=DevtoolIdeMode, default=DevtoolIdeMode.modified,
965 help='Different SDK types are supported:\n'
966 '- "' + DevtoolIdeMode.modified.name + '" (default):\n'
967 ' devtool modify creates a workspace to work on the source code of a recipe.\n'
968 ' devtool ide-sdk builds the SDK and generates the IDE configuration(s) in the workspace directorie(s)\n'
969 ' Usage example:\n'
970 ' devtool modify cmake-example\n'
971 ' devtool ide-sdk cmake-example core-image-minimal\n'
972 ' Start the IDE in the workspace folder\n'
973 ' At least one devtool modified recipe plus one image recipe are required:\n'
974 ' The image recipe is used to generate the target image and the remote debug configuration.\n'
975 '- "' + DevtoolIdeMode.shared.name + '":\n'
976 ' Usage example:\n'
977 ' devtool ide-sdk -m ' + DevtoolIdeMode.shared.name + ' recipe(s)\n'
978 ' This command generates a cross-toolchain as well as the corresponding shared sysroot directories.\n'
979 ' To use this tool-chain the environment-* file found in the deploy..image folder needs to be sourced into a shell.\n'
980 ' In case of VSCode and cmake the tool-chain is also exposed as a cmake-kit')
981 default_ide = list(ide_plugins.keys())[0]
982 parser_ide_sdk.add_argument(
983 '-i', '--ide', choices=ide_plugins.keys(), default=default_ide,
984 help='Setup the configuration for this IDE (default: %s)' % default_ide)
985 parser_ide_sdk.add_argument(
986 '-t', '--target', default='root@192.168.7.2',
987 help='Live target machine running an ssh server: user@hostname.')
988 parser_ide_sdk.add_argument(
989 '-G', '--gdbserver-port-start', default="1234", help='port where gdbserver is listening.')
990 parser_ide_sdk.add_argument(
991 '-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
992 parser_ide_sdk.add_argument(
993 '-e', '--ssh-exec', help='Executable to use in place of ssh')
994 parser_ide_sdk.add_argument(
995 '-P', '--port', help='Specify ssh port to use for connection to the target')
996 parser_ide_sdk.add_argument(
997 '-I', '--key', help='Specify ssh private key for connection to the target')
998 parser_ide_sdk.add_argument(
999 '--skip-bitbake', help='Generate IDE configuration but skip calling bitbake to update the SDK', action='store_true')
1000 parser_ide_sdk.add_argument(
1001 '-k', '--bitbake-k', help='Pass -k parameter to bitbake', action='store_true')
1002 parser_ide_sdk.add_argument(
1003 '--no-strip', help='Do not strip executables prior to deploy', dest='strip', action='store_false')
1004 parser_ide_sdk.add_argument(
1005 '-n', '--dry-run', help='List files to be undeployed only', action='store_true')
1006 parser_ide_sdk.add_argument(
1007 '-s', '--show-status', help='Show progress/status output', action='store_true')
1008 parser_ide_sdk.add_argument(
1009 '-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
1010 parser_ide_sdk.add_argument(
1011 '--no-check-space', help='Do not check for available space before deploying', action='store_true')
1012 parser_ide_sdk.set_defaults(func=ide_setup)