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