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