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