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