1# Copyright (c) 2018 Open Source Foundries Limited. 2# Copyright (c) 2023 Nordic Semiconductor ASA 3# 4# SPDX-License-Identifier: Apache-2.0 5 6'''Common code used by commands which execute runners. 7''' 8 9import importlib.util 10import re 11import argparse 12import logging 13from collections import defaultdict 14from os import close, getcwd, path, fspath 15from pathlib import Path 16from subprocess import CalledProcessError 17import sys 18import tempfile 19import textwrap 20import traceback 21 22from dataclasses import dataclass 23from west import log 24from build_helpers import find_build_dir, is_zephyr_build, load_domains, \ 25 FIND_BUILD_DIR_DESCRIPTION 26from west.commands import CommandError 27from west.configuration import config 28from runners.core import FileType 29from runners.core import BuildConfiguration 30import yaml 31 32import zephyr_module 33from zephyr_ext_common import ZEPHYR_BASE, ZEPHYR_SCRIPTS 34 35# Runners depend on edtlib. Make sure the copy in the tree is 36# available to them before trying to import any. 37sys.path.insert(0, str(ZEPHYR_SCRIPTS / 'dts' / 'python-devicetree' / 'src')) 38 39from runners import get_runner_cls, ZephyrBinaryRunner, MissingProgram 40from runners.core import RunnerConfig 41import zcmake 42 43# Context-sensitive help indentation. 44# Don't change this, or output from argparse won't match up. 45INDENT = ' ' * 2 46 47IGNORED_RUN_ONCE_PRIORITY = -1 48SOC_FILE_RUN_ONCE_DEFAULT_PRIORITY = 0 49BOARD_FILE_RUN_ONCE_DEFAULT_PRIORITY = 10 50 51if log.VERBOSE >= log.VERBOSE_NORMAL: 52 # Using level 1 allows sub-DEBUG levels of verbosity. The 53 # west.log module decides whether or not to actually print the 54 # message. 55 # 56 # https://docs.python.org/3.7/library/logging.html#logging-levels. 57 LOG_LEVEL = 1 58else: 59 LOG_LEVEL = logging.INFO 60 61def _banner(msg): 62 log.inf('-- ' + msg, colorize=True) 63 64class WestLogFormatter(logging.Formatter): 65 66 def __init__(self): 67 super().__init__(fmt='%(name)s: %(message)s') 68 69class WestLogHandler(logging.Handler): 70 71 def __init__(self, *args, **kwargs): 72 super().__init__(*args, **kwargs) 73 self.setFormatter(WestLogFormatter()) 74 self.setLevel(LOG_LEVEL) 75 76 def emit(self, record): 77 fmt = self.format(record) 78 lvl = record.levelno 79 if lvl > logging.CRITICAL: 80 log.die(fmt) 81 elif lvl >= logging.ERROR: 82 log.err(fmt) 83 elif lvl >= logging.WARNING: 84 log.wrn(fmt) 85 elif lvl >= logging.INFO: 86 _banner(fmt) 87 elif lvl >= logging.DEBUG: 88 log.dbg(fmt) 89 else: 90 log.dbg(fmt, level=log.VERBOSE_EXTREME) 91 92@dataclass 93class UsedFlashCommand: 94 command: str 95 boards: list 96 runners: list 97 first: bool 98 ran: bool = False 99 100@dataclass 101class ImagesFlashed: 102 flashed: int = 0 103 total: int = 0 104 105@dataclass 106class SocBoardFilesProcessing: 107 filename: str 108 board: bool = False 109 priority: int = IGNORED_RUN_ONCE_PRIORITY 110 yaml: object = None 111 112def import_from_path(module_name, file_path): 113 spec = importlib.util.spec_from_file_location(module_name, file_path) 114 module = importlib.util.module_from_spec(spec) 115 sys.modules[module_name] = module 116 spec.loader.exec_module(module) 117 return module 118 119def command_verb(command): 120 return "flash" if command.name == "flash" else "debug" 121 122def add_parser_common(command, parser_adder=None, parser=None): 123 if parser_adder is not None: 124 parser = parser_adder.add_parser( 125 command.name, 126 formatter_class=argparse.RawDescriptionHelpFormatter, 127 help=command.help, 128 description=command.description) 129 130 # Remember to update west-completion.bash if you add or remove 131 # flags 132 133 group = parser.add_argument_group('general options', 134 FIND_BUILD_DIR_DESCRIPTION) 135 136 group.add_argument('-d', '--build-dir', metavar='DIR', 137 help='application build directory') 138 # still supported for backwards compatibility, but questionably 139 # useful now that we do everything with runners.yaml 140 group.add_argument('-c', '--cmake-cache', metavar='FILE', 141 help=argparse.SUPPRESS) 142 group.add_argument('-r', '--runner', 143 help='override default runner from --build-dir') 144 group.add_argument('--skip-rebuild', action='store_true', 145 help='do not refresh cmake dependencies first') 146 group.add_argument('--domain', action='append', 147 help='execute runner only for given domain') 148 149 group = parser.add_argument_group( 150 'runner configuration', 151 textwrap.dedent(f'''\ 152 =================================================================== 153 IMPORTANT: 154 Individual runners support additional options not printed here. 155 =================================================================== 156 157 Run "west {command.name} --context" for runner-specific options. 158 159 If a build directory is found, --context also prints per-runner 160 settings found in that build directory's runners.yaml file. 161 162 Use "west {command.name} --context -r RUNNER" to limit output to a 163 specific RUNNER. 164 165 Some runner settings also can be overridden with options like 166 --hex-file. However, this depends on the runner: not all runners 167 respect --elf-file / --hex-file / --bin-file, nor use gdb or openocd, 168 etc.''')) 169 group.add_argument('-H', '--context', action='store_true', 170 help='print runner- and build-specific help') 171 # Options used to override RunnerConfig values in runners.yaml. 172 # TODO: is this actually useful? 173 group.add_argument('--board-dir', metavar='DIR', help='board directory') 174 # FIXME: these are runner-specific and should be moved to where --context 175 # can find them instead. 176 group.add_argument('--gdb', help='path to GDB') 177 group.add_argument('--openocd', help='path to openocd') 178 group.add_argument( 179 '--openocd-search', metavar='DIR', action='append', 180 help='path to add to openocd search path, if applicable') 181 182 return parser 183 184def do_run_common(command, user_args, user_runner_args, domain_file=None): 185 # This is the main routine for all the "west flash", "west debug", 186 # etc. commands. 187 188 # Holds a list of run once commands, this is useful for sysbuild images 189 # whereby there are multiple images per board with flash commands that can 190 # interfere with other images if they run one per time an image is flashed. 191 used_cmds = [] 192 193 # Holds a set of processed board names for flash running information. 194 processed_boards = set() 195 196 # Holds a dictionary of board image flash counts, the first element is 197 # number of images flashed so far and second element is total number of 198 # images for a given board. 199 board_image_count = defaultdict(ImagesFlashed) 200 201 highest_priority = IGNORED_RUN_ONCE_PRIORITY 202 highest_entry = None 203 check_files = [] 204 205 if user_args.context: 206 dump_context(command, user_args, user_runner_args) 207 return 208 209 # Import external module runners 210 for module in zephyr_module.parse_modules(ZEPHYR_BASE, command.manifest): 211 runners_ext = module.meta.get("runners", []) 212 for runner in runners_ext: 213 import_from_path( 214 module.meta.get("name", "runners_ext"), Path(module.project) / runner["file"] 215 ) 216 217 build_dir = get_build_dir(user_args) 218 if not user_args.skip_rebuild: 219 rebuild(command, build_dir, user_args) 220 221 if domain_file is None: 222 if user_args.domain is None: 223 # No domains are passed down and no domains specified by the user. 224 # So default domain will be used. 225 domains = [load_domains(build_dir).get_default_domain()] 226 else: 227 # No domains are passed down, but user has specified domains to use. 228 # Get the user specified domains. 229 domains = load_domains(build_dir).get_domains(user_args.domain) 230 else: 231 domains = load_domains(build_dir).get_domains(user_args.domain, 232 default_flash_order=True) 233 234 if len(domains) > 1: 235 if len(user_runner_args) > 0: 236 log.wrn("Specifying runner options for multiple domains is experimental.\n" 237 "If problems are experienced, please specify a single domain " 238 "using '--domain <domain>'") 239 240 # Process all domains to load board names and populate flash runner 241 # parameters. 242 board_names = set() 243 for d in domains: 244 if d.build_dir is None: 245 build_dir = get_build_dir(user_args) 246 else: 247 build_dir = d.build_dir 248 249 cache = load_cmake_cache(build_dir, user_args) 250 build_conf = BuildConfiguration(build_dir) 251 board = build_conf.get('CONFIG_BOARD_TARGET') 252 board_names.add(board) 253 board_image_count[board].total += 1 254 255 # Load board flash runner configuration (if it exists) and store 256 # single-use commands in a dictionary so that they get executed 257 # once per unique board name. 258 for directory in cache.get_list('SOC_DIRECTORIES'): 259 if directory not in processed_boards: 260 check_files.append(SocBoardFilesProcessing(Path(directory) / 'soc.yml')) 261 processed_boards.add(directory) 262 263 for directory in cache.get_list('BOARD_DIRECTORIES'): 264 if directory not in processed_boards: 265 check_files.append(SocBoardFilesProcessing(Path(directory) / 'board.yml', True)) 266 processed_boards.add(directory) 267 268 for check in check_files: 269 try: 270 with open(check.filename, 'r') as f: 271 check.yaml = yaml.safe_load(f.read()) 272 273 if 'runners' not in check.yaml: 274 continue 275 elif check.board is False and 'run_once' not in check.yaml['runners']: 276 continue 277 278 if 'priority' in check.yaml['runners']: 279 check.priority = check.yaml['runners']['priority'] 280 else: 281 check.priority = BOARD_FILE_RUN_ONCE_DEFAULT_PRIORITY if check.board is True else SOC_FILE_RUN_ONCE_DEFAULT_PRIORITY 282 283 if check.priority == highest_priority: 284 log.die("Duplicate flash run once configuration found with equal priorities") 285 286 elif check.priority > highest_priority: 287 highest_priority = check.priority 288 highest_entry = check 289 290 except FileNotFoundError: 291 continue 292 293 if highest_entry is not None: 294 group_type = 'boards' if highest_entry.board is True else 'qualifiers' 295 296 for cmd in highest_entry.yaml['runners']['run_once']: 297 for data in highest_entry.yaml['runners']['run_once'][cmd]: 298 for group in data['groups']: 299 run_first = bool(data['run'] == 'first') 300 if group_type == 'qualifiers': 301 targets = [] 302 for target in group[group_type]: 303 # For SoC-based qualifiers, prepend to the beginning of the 304 # match to allow for matching any board name 305 targets.append('([^/]+)/' + target) 306 else: 307 targets = group[group_type] 308 309 used_cmds.append(UsedFlashCommand(cmd, targets, data['runners'], run_first)) 310 311 # Reduce entries to only those having matching board names (either exact or with regex) and 312 # remove any entries with empty board lists 313 for i, entry in enumerate(used_cmds): 314 for l, match in enumerate(entry.boards): 315 match_found = False 316 317 # Check if there is a matching board for this regex 318 for check in board_names: 319 if re.match(fr'^{match}$', check) is not None: 320 match_found = True 321 break 322 323 if not match_found: 324 del entry.boards[l] 325 326 if len(entry.boards) == 0: 327 del used_cmds[i] 328 329 prev_runner = None 330 for d in domains: 331 prev_runner = do_run_common_image(command, user_args, user_runner_args, used_cmds, 332 board_image_count, d.build_dir, prev_runner) 333 334 335def do_run_common_image(command, user_args, user_runner_args, used_cmds, 336 board_image_count, build_dir=None, prev_runner=None): 337 global re 338 command_name = command.name 339 if build_dir is None: 340 build_dir = get_build_dir(user_args) 341 cache = load_cmake_cache(build_dir, user_args) 342 build_conf = BuildConfiguration(build_dir) 343 board = build_conf.get('CONFIG_BOARD_TARGET') 344 345 if board_image_count is not None and board in board_image_count: 346 board_image_count[board].flashed += 1 347 348 # Load runners.yaml. 349 yaml_path = runners_yaml_path(build_dir, board) 350 runners_yaml = load_runners_yaml(yaml_path) 351 352 # Get a concrete ZephyrBinaryRunner subclass to use based on 353 # runners.yaml and command line arguments. 354 runner_cls = use_runner_cls(command, board, user_args, runners_yaml, 355 cache) 356 runner_name = runner_cls.name() 357 358 # Set up runner logging to delegate to west.log commands. 359 logger = logging.getLogger('runners') 360 logger.setLevel(LOG_LEVEL) 361 if not logger.hasHandlers(): 362 # Only add a runners log handler if none has been added already. 363 logger.addHandler(WestLogHandler()) 364 365 # If the user passed -- to force the parent argument parser to stop 366 # parsing, it will show up here, and needs to be filtered out. 367 runner_args = [arg for arg in user_runner_args if arg != '--'] 368 369 # Check if there are any commands that should only be ran once per board 370 # and if so, remove them for all but the first iteration of the flash 371 # runner per unique board name. 372 if len(used_cmds) > 0 and len(runner_args) > 0: 373 i = len(runner_args) - 1 374 while i >= 0: 375 for cmd in used_cmds: 376 if cmd.command == runner_args[i] and (runner_name in cmd.runners or 'all' in cmd.runners): 377 # Check if board is here 378 match_found = False 379 380 for match in cmd.boards: 381 # Check if there is a matching board for this regex 382 if re.match(fr'^{match}$', board) is not None: 383 match_found = True 384 break 385 386 if not match_found: 387 continue 388 389 # Check if this is a first or last run 390 if not cmd.first: 391 # For last run instances, we need to check that this really is the last 392 # image of all boards being flashed 393 for check in cmd.boards: 394 can_continue = False 395 396 for match in board_image_count: 397 if re.match(fr'^{check}$', match) is not None: 398 if board_image_count[match].flashed == board_image_count[match].total: 399 can_continue = True 400 break 401 402 if not can_continue: 403 continue 404 405 if not cmd.ran: 406 cmd.ran = True 407 else: 408 runner_args.pop(i) 409 410 break 411 412 i = i - 1 413 414 # If flashing multiple images, the runner supports reset after flashing and 415 # the board has enabled this functionality, check if the board should be 416 # reset or not. If this is not specified in the board/soc file, leave it up to 417 # the runner's default configuration to decide if a reset should occur. 418 if runner_cls.capabilities().reset: 419 if board_image_count is not None: 420 reset = True 421 422 for cmd in used_cmds: 423 if cmd.command == '--reset' and (runner_name in cmd.runners or 'all' in cmd.runners): 424 # Check if board is here 425 match_found = False 426 427 for match in cmd.boards: 428 if re.match(fr'^{match}$', board) is not None: 429 match_found = True 430 break 431 432 if not match_found: 433 continue 434 435 # Check if this is a first or last run 436 if cmd.first and cmd.ran: 437 reset = False 438 break 439 elif not cmd.first and not cmd.ran: 440 # For last run instances, we need to check that this really is the last 441 # image of all boards being flashed 442 for check in cmd.boards: 443 can_continue = False 444 445 for match in board_image_count: 446 if re.match(fr'^{check}$', match) is not None: 447 if board_image_count[match].flashed != board_image_count[match].total: 448 reset = False 449 break 450 451 if reset: 452 runner_args.append('--reset') 453 else: 454 runner_args.append('--no-reset') 455 456 # Arguments in this order to allow specific to override general: 457 # 458 # - runner-specific runners.yaml arguments 459 # - user-provided command line arguments 460 final_argv = runners_yaml['args'][runner_name] + runner_args 461 462 # 'user_args' contains parsed arguments which are: 463 # 464 # 1. provided on the command line, and 465 # 2. handled by add_parser_common(), and 466 # 3. *not* runner-specific 467 # 468 # 'final_argv' contains unparsed arguments from either: 469 # 470 # 1. runners.yaml, or 471 # 2. the command line 472 # 473 # We next have to: 474 # 475 # - parse 'final_argv' now that we have all the command line 476 # arguments 477 # - create a RunnerConfig using 'user_args' and the result 478 # of parsing 'final_argv' 479 parser = argparse.ArgumentParser(prog=runner_name, allow_abbrev=False) 480 add_parser_common(command, parser=parser) 481 runner_cls.add_parser(parser) 482 args, unknown = parser.parse_known_args(args=final_argv) 483 if unknown: 484 log.die(f'runner {runner_name} received unknown arguments: {unknown}') 485 486 # Propagate useful args from previous domain invocations 487 if prev_runner is not None: 488 runner_cls.args_from_previous_runner(prev_runner, args) 489 490 # Override args with any user_args. The latter must take 491 # precedence, or e.g. --hex-file on the command line would be 492 # ignored in favor of a board.cmake setting. 493 for a, v in vars(user_args).items(): 494 if v is not None: 495 setattr(args, a, v) 496 497 # Create the RunnerConfig from runners.yaml and any command line 498 # overrides. 499 runner_config = get_runner_config(build_dir, yaml_path, runners_yaml, args) 500 log.dbg(f'runner_config: {runner_config}', level=log.VERBOSE_VERY) 501 502 # Use that RunnerConfig to create the ZephyrBinaryRunner instance 503 # and call its run(). 504 try: 505 runner = runner_cls.create(runner_config, args) 506 runner.run(command_name) 507 except ValueError as ve: 508 log.err(str(ve), fatal=True) 509 dump_traceback() 510 raise CommandError(1) 511 except MissingProgram as e: 512 log.die('required program', e.filename, 513 'not found; install it or add its location to PATH') 514 except RuntimeError as re: 515 if not user_args.verbose: 516 log.die(re) 517 else: 518 log.err('verbose mode enabled, dumping stack:', fatal=True) 519 raise 520 return runner 521 522def get_build_dir(args, die_if_none=True): 523 # Get the build directory for the given argument list and environment. 524 if args.build_dir: 525 return args.build_dir 526 527 guess = config.get('build', 'guess-dir', fallback='never') 528 guess = guess == 'runners' 529 dir = find_build_dir(None, guess) 530 531 if dir and is_zephyr_build(dir): 532 return dir 533 elif die_if_none: 534 msg = '--build-dir was not given, ' 535 if dir: 536 msg = msg + 'and neither {} nor {} are zephyr build directories.' 537 else: 538 msg = msg + ('{} is not a build directory and the default build ' 539 'directory cannot be determined. Check your ' 540 'build.dir-fmt configuration option') 541 log.die(msg.format(getcwd(), dir)) 542 else: 543 return None 544 545def load_cmake_cache(build_dir, args): 546 cache_file = path.join(build_dir, args.cmake_cache or zcmake.DEFAULT_CACHE) 547 try: 548 return zcmake.CMakeCache(cache_file) 549 except FileNotFoundError: 550 log.die(f'no CMake cache found (expected one at {cache_file})') 551 552def rebuild(command, build_dir, args): 553 _banner(f'west {command.name}: rebuilding') 554 try: 555 zcmake.run_build(build_dir) 556 except CalledProcessError: 557 if args.build_dir: 558 log.die(f're-build in {args.build_dir} failed') 559 else: 560 log.die(f're-build in {build_dir} failed (no --build-dir given)') 561 562def runners_yaml_path(build_dir, board): 563 ret = Path(build_dir) / 'zephyr' / 'runners.yaml' 564 if not ret.is_file(): 565 log.die(f'no runners.yaml found in {build_dir}/zephyr. ' 566 f"Either board {board} doesn't support west flash/debug/simulate," 567 ' or a pristine build is needed.') 568 return ret 569 570def load_runners_yaml(path): 571 # Load runners.yaml and convert to Python object. 572 573 try: 574 with open(path, 'r') as f: 575 content = yaml.safe_load(f.read()) 576 except FileNotFoundError: 577 log.die(f'runners.yaml file not found: {path}') 578 579 if not content.get('runners'): 580 log.wrn(f'no pre-configured runners in {path}; ' 581 "this probably won't work") 582 583 return content 584 585def use_runner_cls(command, board, args, runners_yaml, cache): 586 # Get the ZephyrBinaryRunner class from its name, and make sure it 587 # supports the command. Print a message about the choice, and 588 # return the class. 589 590 runner = args.runner or runners_yaml.get(command.runner_key) 591 if runner is None: 592 log.die(f'no {command.name} runner available for board {board}. ' 593 "Check the board's documentation for instructions.") 594 595 _banner(f'west {command.name}: using runner {runner}') 596 597 available = runners_yaml.get('runners', []) 598 if runner not in available: 599 if 'BOARD_DIR' in cache: 600 board_cmake = Path(cache['BOARD_DIR']) / 'board.cmake' 601 else: 602 board_cmake = 'board.cmake' 603 log.err(f'board {board} does not support runner {runner}', 604 fatal=True) 605 log.inf(f'To fix, configure this runner in {board_cmake} and rebuild.') 606 sys.exit(1) 607 try: 608 runner_cls = get_runner_cls(runner) 609 except ValueError as e: 610 log.die(e) 611 if command.name not in runner_cls.capabilities().commands: 612 log.die(f'runner {runner} does not support command {command.name}') 613 614 return runner_cls 615 616def get_runner_config(build_dir, yaml_path, runners_yaml, args=None): 617 # Get a RunnerConfig object for the current run. yaml_config is 618 # runners.yaml's config: map, and args are the command line arguments. 619 yaml_config = runners_yaml['config'] 620 yaml_dir = yaml_path.parent 621 if args is None: 622 args = argparse.Namespace() 623 624 def output_file(filetype): 625 626 from_args = getattr(args, f'{filetype}_file', None) 627 if from_args is not None: 628 return from_args 629 630 from_yaml = yaml_config.get(f'{filetype}_file') 631 if from_yaml is not None: 632 # Output paths in runners.yaml are relative to the 633 # directory containing the runners.yaml file. 634 return fspath(yaml_dir / from_yaml) 635 636 return None 637 638 def config(attr, default=None): 639 return getattr(args, attr, None) or yaml_config.get(attr, default) 640 641 def filetype(attr): 642 ftype = str(getattr(args, attr, None)).lower() 643 if ftype == "hex": 644 return FileType.HEX 645 elif ftype == "bin": 646 return FileType.BIN 647 elif ftype == "elf": 648 return FileType.ELF 649 elif getattr(args, attr, None) is not None: 650 err = 'unknown --file-type ({}). Please use hex, bin or elf' 651 raise ValueError(err.format(ftype)) 652 653 # file-type not provided, try to get from filename 654 file = getattr(args, "file", None) 655 if file is not None: 656 ext = Path(file).suffix 657 if ext == ".hex": 658 return FileType.HEX 659 if ext == ".bin": 660 return FileType.BIN 661 if ext == ".elf": 662 return FileType.ELF 663 664 # we couldn't get the file-type, set to 665 # OTHER and let the runner deal with it 666 return FileType.OTHER 667 668 return RunnerConfig(build_dir, 669 yaml_config['board_dir'], 670 output_file('elf'), 671 output_file('exe'), 672 output_file('hex'), 673 output_file('bin'), 674 output_file('uf2'), 675 config('file'), 676 filetype('file_type'), 677 config('gdb'), 678 config('openocd'), 679 config('openocd_search', []), 680 config('rtt_address')) 681 682def dump_traceback(): 683 # Save the current exception to a file and return its path. 684 fd, name = tempfile.mkstemp(prefix='west-exc-', suffix='.txt') 685 close(fd) # traceback has no use for the fd 686 with open(name, 'w') as f: 687 traceback.print_exc(file=f) 688 log.inf("An exception trace has been saved in", name) 689 690# 691# west {command} --context 692# 693 694def dump_context(command, args, unknown_args): 695 build_dir = get_build_dir(args, die_if_none=False) 696 if build_dir is None: 697 log.wrn('no --build-dir given or found; output will be limited') 698 runners_yaml = None 699 else: 700 build_conf = BuildConfiguration(build_dir) 701 board = build_conf.get('CONFIG_BOARD_TARGET') 702 yaml_path = runners_yaml_path(build_dir, board) 703 runners_yaml = load_runners_yaml(yaml_path) 704 705 # Re-build unless asked not to, to make sure the output is up to date. 706 if build_dir and not args.skip_rebuild: 707 rebuild(command, build_dir, args) 708 709 if args.runner: 710 try: 711 cls = get_runner_cls(args.runner) 712 except ValueError: 713 log.die(f'invalid runner name {args.runner}; choices: ' + 714 ', '.join(cls.name() for cls in 715 ZephyrBinaryRunner.get_runners())) 716 else: 717 cls = None 718 719 if runners_yaml is None: 720 dump_context_no_config(command, cls) 721 else: 722 log.inf(f'build configuration:', colorize=True) 723 log.inf(f'{INDENT}build directory: {build_dir}') 724 log.inf(f'{INDENT}board: {board}') 725 log.inf(f'{INDENT}runners.yaml: {yaml_path}') 726 if cls: 727 dump_runner_context(command, cls, runners_yaml) 728 else: 729 dump_all_runner_context(command, runners_yaml, board, build_dir) 730 731def dump_context_no_config(command, cls): 732 if not cls: 733 all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() 734 if command.name in cls.capabilities().commands} 735 log.inf('all Zephyr runners which support {}:'.format(command.name), 736 colorize=True) 737 dump_wrapped_lines(', '.join(all_cls.keys()), INDENT) 738 log.inf() 739 log.inf('Note: use -r RUNNER to limit information to one runner.') 740 else: 741 # This does the right thing with a None argument. 742 dump_runner_context(command, cls, None) 743 744def dump_runner_context(command, cls, runners_yaml, indent=''): 745 dump_runner_caps(cls, indent) 746 dump_runner_option_help(cls, indent) 747 748 if runners_yaml is None: 749 return 750 751 if cls.name() in runners_yaml['runners']: 752 dump_runner_args(cls.name(), runners_yaml, indent) 753 else: 754 log.wrn(f'support for runner {cls.name()} is not configured ' 755 f'in this build directory') 756 757def dump_runner_caps(cls, indent=''): 758 # Print RunnerCaps for the given runner class. 759 760 log.inf(f'{indent}{cls.name()} capabilities:', colorize=True) 761 log.inf(f'{indent}{INDENT}{cls.capabilities()}') 762 763def dump_runner_option_help(cls, indent=''): 764 # Print help text for class-specific command line options for the 765 # given runner class. 766 767 dummy_parser = argparse.ArgumentParser(prog='', add_help=False, allow_abbrev=False) 768 cls.add_parser(dummy_parser) 769 formatter = dummy_parser._get_formatter() 770 for group in dummy_parser._action_groups: 771 # Break the abstraction to filter out the 'flash', 'debug', etc. 772 # TODO: come up with something cleaner (may require changes 773 # in the runner core). 774 actions = group._group_actions 775 if len(actions) == 1 and actions[0].dest == 'command': 776 # This is the lone positional argument. Skip it. 777 continue 778 formatter.start_section('REMOVE ME') 779 formatter.add_text(group.description) 780 formatter.add_arguments(actions) 781 formatter.end_section() 782 # Get the runner help, with the "REMOVE ME" string gone 783 runner_help = f'\n{indent}'.join(formatter.format_help().splitlines()[1:]) 784 785 log.inf(f'{indent}{cls.name()} options:', colorize=True) 786 log.inf(indent + runner_help) 787 788def dump_runner_args(group, runners_yaml, indent=''): 789 msg = f'{indent}{group} arguments from runners.yaml:' 790 args = runners_yaml['args'][group] 791 if args: 792 log.inf(msg, colorize=True) 793 for arg in args: 794 log.inf(f'{indent}{INDENT}{arg}') 795 else: 796 log.inf(f'{msg} (none)', colorize=True) 797 798def dump_all_runner_context(command, runners_yaml, board, build_dir): 799 all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if 800 command.name in cls.capabilities().commands} 801 available = runners_yaml['runners'] 802 available_cls = {r: all_cls[r] for r in available if r in all_cls} 803 default_runner = runners_yaml[command.runner_key] 804 yaml_path = runners_yaml_path(build_dir, board) 805 runners_yaml = load_runners_yaml(yaml_path) 806 807 log.inf(f'zephyr runners which support "west {command.name}":', 808 colorize=True) 809 dump_wrapped_lines(', '.join(all_cls.keys()), INDENT) 810 log.inf() 811 dump_wrapped_lines('Note: not all may work with this board and build ' 812 'directory. Available runners are listed below.', 813 INDENT) 814 815 log.inf(f'available runners in runners.yaml:', 816 colorize=True) 817 dump_wrapped_lines(', '.join(available), INDENT) 818 log.inf(f'default runner in runners.yaml:', colorize=True) 819 log.inf(INDENT + default_runner) 820 log.inf('common runner configuration:', colorize=True) 821 runner_config = get_runner_config(build_dir, yaml_path, runners_yaml) 822 for field, value in zip(runner_config._fields, runner_config): 823 log.inf(f'{INDENT}- {field}: {value}') 824 log.inf('runner-specific context:', colorize=True) 825 for cls in available_cls.values(): 826 dump_runner_context(command, cls, runners_yaml, INDENT) 827 828 if len(available) > 1: 829 log.inf() 830 log.inf('Note: use -r RUNNER to limit information to one runner.') 831 832def dump_wrapped_lines(text, indent): 833 for line in textwrap.wrap(text, initial_indent=indent, 834 subsequent_indent=indent, 835 break_on_hyphens=False, 836 break_long_words=False): 837 log.inf(line) 838