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