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