| # Copyright (c) 2018 Open Source Foundries Limited. |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| '''Common code used by commands which execute runners. |
| ''' |
| |
| import argparse |
| import logging |
| from os import close, getcwd, path, fspath |
| from pathlib import Path |
| from subprocess import CalledProcessError |
| import sys |
| import tempfile |
| import textwrap |
| import traceback |
| |
| from west import log |
| from build_helpers import find_build_dir, is_zephyr_build, load_domains, \ |
| FIND_BUILD_DIR_DESCRIPTION |
| from west.commands import CommandError |
| from west.configuration import config |
| import yaml |
| |
| from zephyr_ext_common import ZEPHYR_SCRIPTS |
| |
| # Runners depend on edtlib. Make sure the copy in the tree is |
| # available to them before trying to import any. |
| sys.path.insert(0, str(ZEPHYR_SCRIPTS / 'dts' / 'python-devicetree' / 'src')) |
| |
| from runners import get_runner_cls, ZephyrBinaryRunner, MissingProgram |
| from runners.core import RunnerConfig |
| import zcmake |
| |
| # Context-sensitive help indentation. |
| # Don't change this, or output from argparse won't match up. |
| INDENT = ' ' * 2 |
| |
| if log.VERBOSE >= log.VERBOSE_NORMAL: |
| # Using level 1 allows sub-DEBUG levels of verbosity. The |
| # west.log module decides whether or not to actually print the |
| # message. |
| # |
| # https://docs.python.org/3.7/library/logging.html#logging-levels. |
| LOG_LEVEL = 1 |
| else: |
| LOG_LEVEL = logging.INFO |
| |
| def _banner(msg): |
| log.inf('-- ' + msg, colorize=True) |
| |
| class WestLogFormatter(logging.Formatter): |
| |
| def __init__(self): |
| super().__init__(fmt='%(name)s: %(message)s') |
| |
| class WestLogHandler(logging.Handler): |
| |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self.setFormatter(WestLogFormatter()) |
| self.setLevel(LOG_LEVEL) |
| |
| def emit(self, record): |
| fmt = self.format(record) |
| lvl = record.levelno |
| if lvl > logging.CRITICAL: |
| log.die(fmt) |
| elif lvl >= logging.ERROR: |
| log.err(fmt) |
| elif lvl >= logging.WARNING: |
| log.wrn(fmt) |
| elif lvl >= logging.INFO: |
| _banner(fmt) |
| elif lvl >= logging.DEBUG: |
| log.dbg(fmt) |
| else: |
| log.dbg(fmt, level=log.VERBOSE_EXTREME) |
| |
| def command_verb(command): |
| return "flash" if command.name == "flash" else "debug" |
| |
| def add_parser_common(command, parser_adder=None, parser=None): |
| if parser_adder is not None: |
| parser = parser_adder.add_parser( |
| command.name, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| help=command.help, |
| description=command.description) |
| |
| # Remember to update west-completion.bash if you add or remove |
| # flags |
| |
| group = parser.add_argument_group('general options', |
| FIND_BUILD_DIR_DESCRIPTION) |
| |
| group.add_argument('-d', '--build-dir', metavar='DIR', |
| help='application build directory') |
| # still supported for backwards compatibility, but questionably |
| # useful now that we do everything with runners.yaml |
| group.add_argument('-c', '--cmake-cache', metavar='FILE', |
| help=argparse.SUPPRESS) |
| group.add_argument('-r', '--runner', |
| help='override default runner from --build-dir') |
| group.add_argument('--skip-rebuild', action='store_true', |
| help='do not refresh cmake dependencies first') |
| group.add_argument('--domain', action='append', |
| help='execute runner only for given domain') |
| |
| group = parser.add_argument_group( |
| 'runner configuration', |
| textwrap.dedent(f'''\ |
| =================================================================== |
| IMPORTANT: |
| Individual runners support additional options not printed here. |
| =================================================================== |
| |
| Run "west {command.name} --context" for runner-specific options. |
| |
| If a build directory is found, --context also prints per-runner |
| settings found in that build directory's runners.yaml file. |
| |
| Use "west {command.name} --context -r RUNNER" to limit output to a |
| specific RUNNER. |
| |
| Some runner settings also can be overridden with options like |
| --hex-file. However, this depends on the runner: not all runners |
| respect --elf-file / --hex-file / --bin-file, nor use gdb or openocd, |
| etc.''')) |
| group.add_argument('-H', '--context', action='store_true', |
| help='print runner- and build-specific help') |
| # Options used to override RunnerConfig values in runners.yaml. |
| # TODO: is this actually useful? |
| group.add_argument('--board-dir', metavar='DIR', help='board directory') |
| # FIXME: we should just have a single --file argument. The variation |
| # between runners is confusing people. |
| group.add_argument('--elf-file', metavar='FILE', help='path to zephyr.elf') |
| group.add_argument('--hex-file', metavar='FILE', help='path to zephyr.hex') |
| group.add_argument('--bin-file', metavar='FILE', help='path to zephyr.bin') |
| # FIXME: these are runner-specific and should be moved to where --context |
| # can find them instead. |
| group.add_argument('--gdb', help='path to GDB') |
| group.add_argument('--openocd', help='path to openocd') |
| group.add_argument( |
| '--openocd-search', metavar='DIR', action='append', |
| help='path to add to openocd search path, if applicable') |
| |
| return parser |
| |
| def do_run_common(command, user_args, user_runner_args, domains=None): |
| # This is the main routine for all the "west flash", "west debug", |
| # etc. commands. |
| |
| if user_args.context: |
| dump_context(command, user_args, user_runner_args) |
| return |
| |
| build_dir = get_build_dir(user_args) |
| if not user_args.skip_rebuild: |
| rebuild(command, build_dir, user_args) |
| |
| if domains is None: |
| if user_args.domain is None: |
| # No domains are passed down and no domains specified by the user. |
| # So default domain will be used. |
| domains = [load_domains(build_dir).get_default_domain()] |
| else: |
| # No domains are passed down, but user has specified domains to use. |
| # Get the user specified domains. |
| domains = load_domains(build_dir).get_domains(user_args.domain) |
| |
| if len(domains) > 1 and len(user_runner_args) > 0: |
| log.wrn("Specifying runner options for multiple domains is experimental.\n" |
| "If problems are experienced, please specify a single domain " |
| "using '--domain <domain>'") |
| |
| for d in domains: |
| do_run_common_image(command, user_args, user_runner_args, d.build_dir) |
| |
| def do_run_common_image(command, user_args, user_runner_args, build_dir=None): |
| command_name = command.name |
| if build_dir is None: |
| build_dir = get_build_dir(user_args) |
| cache = load_cmake_cache(build_dir, user_args) |
| board = cache['CACHED_BOARD'] |
| |
| # Load runners.yaml. |
| yaml_path = runners_yaml_path(build_dir, board) |
| runners_yaml = load_runners_yaml(yaml_path) |
| |
| # Get a concrete ZephyrBinaryRunner subclass to use based on |
| # runners.yaml and command line arguments. |
| runner_cls = use_runner_cls(command, board, user_args, runners_yaml, |
| cache) |
| runner_name = runner_cls.name() |
| |
| # Set up runner logging to delegate to west.log commands. |
| logger = logging.getLogger('runners') |
| logger.setLevel(LOG_LEVEL) |
| if not logger.hasHandlers(): |
| # Only add a runners log handler if none has been added already. |
| logger.addHandler(WestLogHandler()) |
| |
| # If the user passed -- to force the parent argument parser to stop |
| # parsing, it will show up here, and needs to be filtered out. |
| runner_args = [arg for arg in user_runner_args if arg != '--'] |
| |
| # Arguments in this order to allow specific to override general: |
| # |
| # - runner-specific runners.yaml arguments |
| # - user-provided command line arguments |
| final_argv = runners_yaml['args'][runner_name] + runner_args |
| |
| # 'user_args' contains parsed arguments which are: |
| # |
| # 1. provided on the command line, and |
| # 2. handled by add_parser_common(), and |
| # 3. *not* runner-specific |
| # |
| # 'final_argv' contains unparsed arguments from either: |
| # |
| # 1. runners.yaml, or |
| # 2. the command line |
| # |
| # We next have to: |
| # |
| # - parse 'final_argv' now that we have all the command line |
| # arguments |
| # - create a RunnerConfig using 'user_args' and the result |
| # of parsing 'final_argv' |
| parser = argparse.ArgumentParser(prog=runner_name) |
| add_parser_common(command, parser=parser) |
| runner_cls.add_parser(parser) |
| args, unknown = parser.parse_known_args(args=final_argv) |
| if unknown: |
| log.die(f'runner {runner_name} received unknown arguments: {unknown}') |
| |
| # Override args with any user_args. The latter must take |
| # precedence, or e.g. --hex-file on the command line would be |
| # ignored in favor of a board.cmake setting. |
| for a, v in vars(user_args).items(): |
| if v is not None: |
| setattr(args, a, v) |
| |
| # Create the RunnerConfig from runners.yaml and any command line |
| # overrides. |
| runner_config = get_runner_config(build_dir, yaml_path, runners_yaml, args) |
| log.dbg(f'runner_config: {runner_config}', level=log.VERBOSE_VERY) |
| |
| # Use that RunnerConfig to create the ZephyrBinaryRunner instance |
| # and call its run(). |
| try: |
| runner = runner_cls.create(runner_config, args) |
| runner.run(command_name) |
| except ValueError as ve: |
| log.err(str(ve), fatal=True) |
| dump_traceback() |
| raise CommandError(1) |
| except MissingProgram as e: |
| log.die('required program', e.filename, |
| 'not found; install it or add its location to PATH') |
| except RuntimeError as re: |
| if not user_args.verbose: |
| log.die(re) |
| else: |
| log.err('verbose mode enabled, dumping stack:', fatal=True) |
| raise |
| |
| def get_build_dir(args, die_if_none=True): |
| # Get the build directory for the given argument list and environment. |
| if args.build_dir: |
| return args.build_dir |
| |
| guess = config.get('build', 'guess-dir', fallback='never') |
| guess = guess == 'runners' |
| dir = find_build_dir(None, guess) |
| |
| if dir and is_zephyr_build(dir): |
| return dir |
| elif die_if_none: |
| msg = '--build-dir was not given, ' |
| if dir: |
| msg = msg + 'and neither {} nor {} are zephyr build directories.' |
| else: |
| msg = msg + ('{} is not a build directory and the default build ' |
| 'directory cannot be determined. Check your ' |
| 'build.dir-fmt configuration option') |
| log.die(msg.format(getcwd(), dir)) |
| else: |
| return None |
| |
| def load_cmake_cache(build_dir, args): |
| cache_file = path.join(build_dir, args.cmake_cache or zcmake.DEFAULT_CACHE) |
| try: |
| return zcmake.CMakeCache(cache_file) |
| except FileNotFoundError: |
| log.die(f'no CMake cache found (expected one at {cache_file})') |
| |
| def rebuild(command, build_dir, args): |
| _banner(f'west {command.name}: rebuilding') |
| try: |
| zcmake.run_build(build_dir) |
| except CalledProcessError: |
| if args.build_dir: |
| log.die(f're-build in {args.build_dir} failed') |
| else: |
| log.die(f're-build in {build_dir} failed (no --build-dir given)') |
| |
| def runners_yaml_path(build_dir, board): |
| ret = Path(build_dir) / 'zephyr' / 'runners.yaml' |
| if not ret.is_file(): |
| log.die(f'either a pristine build is needed, or board {board} ' |
| "doesn't support west flash/debug " |
| '(no ZEPHYR_RUNNERS_YAML in CMake cache)') |
| return ret |
| |
| def load_runners_yaml(path): |
| # Load runners.yaml and convert to Python object. |
| |
| try: |
| with open(path, 'r') as f: |
| content = yaml.safe_load(f.read()) |
| except FileNotFoundError: |
| log.die(f'runners.yaml file not found: {path}') |
| |
| if not content.get('runners'): |
| log.wrn(f'no pre-configured runners in {path}; ' |
| "this probably won't work") |
| |
| return content |
| |
| def use_runner_cls(command, board, args, runners_yaml, cache): |
| # Get the ZephyrBinaryRunner class from its name, and make sure it |
| # supports the command. Print a message about the choice, and |
| # return the class. |
| |
| runner = args.runner or runners_yaml.get(command.runner_key) |
| if runner is None: |
| log.die(f'no {command.name} runner available for board {board}. ' |
| "Check the board's documentation for instructions.") |
| |
| _banner(f'west {command.name}: using runner {runner}') |
| |
| available = runners_yaml.get('runners', []) |
| if runner not in available: |
| if 'BOARD_DIR' in cache: |
| board_cmake = Path(cache['BOARD_DIR']) / 'board.cmake' |
| else: |
| board_cmake = 'board.cmake' |
| log.err(f'board {board} does not support runner {runner}', |
| fatal=True) |
| log.inf(f'To fix, configure this runner in {board_cmake} and rebuild.') |
| sys.exit(1) |
| try: |
| runner_cls = get_runner_cls(runner) |
| except ValueError as e: |
| log.die(e) |
| if command.name not in runner_cls.capabilities().commands: |
| log.die(f'runner {runner} does not support command {command.name}') |
| |
| return runner_cls |
| |
| def get_runner_config(build_dir, yaml_path, runners_yaml, args=None): |
| # Get a RunnerConfig object for the current run. yaml_config is |
| # runners.yaml's config: map, and args are the command line arguments. |
| yaml_config = runners_yaml['config'] |
| yaml_dir = yaml_path.parent |
| if args is None: |
| args = argparse.Namespace() |
| |
| def output_file(filetype): |
| |
| from_args = getattr(args, f'{filetype}_file', None) |
| if from_args is not None: |
| return from_args |
| |
| from_yaml = yaml_config.get(f'{filetype}_file') |
| if from_yaml is not None: |
| # Output paths in runners.yaml are relative to the |
| # directory containing the runners.yaml file. |
| return fspath(yaml_dir / from_yaml) |
| |
| return None |
| |
| def config(attr, default=None): |
| return getattr(args, attr, None) or yaml_config.get(attr, default) |
| |
| return RunnerConfig(build_dir, |
| yaml_config['board_dir'], |
| output_file('elf'), |
| output_file('hex'), |
| output_file('bin'), |
| config('gdb'), |
| config('openocd'), |
| config('openocd_search', [])) |
| |
| def dump_traceback(): |
| # Save the current exception to a file and return its path. |
| fd, name = tempfile.mkstemp(prefix='west-exc-', suffix='.txt') |
| close(fd) # traceback has no use for the fd |
| with open(name, 'w') as f: |
| traceback.print_exc(file=f) |
| log.inf("An exception trace has been saved in", name) |
| |
| # |
| # west {command} --context |
| # |
| |
| def dump_context(command, args, unknown_args): |
| build_dir = get_build_dir(args, die_if_none=False) |
| if build_dir is None: |
| log.wrn('no --build-dir given or found; output will be limited') |
| runners_yaml = None |
| else: |
| cache = load_cmake_cache(build_dir, args) |
| board = cache['CACHED_BOARD'] |
| yaml_path = runners_yaml_path(build_dir, board) |
| runners_yaml = load_runners_yaml(yaml_path) |
| |
| # Re-build unless asked not to, to make sure the output is up to date. |
| if build_dir and not args.skip_rebuild: |
| rebuild(command, build_dir, args) |
| |
| if args.runner: |
| try: |
| cls = get_runner_cls(args.runner) |
| except ValueError: |
| log.die(f'invalid runner name {args.runner}; choices: ' + |
| ', '.join(cls.name() for cls in |
| ZephyrBinaryRunner.get_runners())) |
| else: |
| cls = None |
| |
| if runners_yaml is None: |
| dump_context_no_config(command, cls) |
| else: |
| log.inf(f'build configuration:', colorize=True) |
| log.inf(f'{INDENT}build directory: {build_dir}') |
| log.inf(f'{INDENT}board: {board}') |
| log.inf(f'{INDENT}runners.yaml: {yaml_path}') |
| if cls: |
| dump_runner_context(command, cls, runners_yaml) |
| else: |
| dump_all_runner_context(command, runners_yaml, board, build_dir) |
| |
| def dump_context_no_config(command, cls): |
| if not cls: |
| all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() |
| if command.name in cls.capabilities().commands} |
| log.inf('all Zephyr runners which support {}:'.format(command.name), |
| colorize=True) |
| dump_wrapped_lines(', '.join(all_cls.keys()), INDENT) |
| log.inf() |
| log.inf('Note: use -r RUNNER to limit information to one runner.') |
| else: |
| # This does the right thing with a None argument. |
| dump_runner_context(command, cls, None) |
| |
| def dump_runner_context(command, cls, runners_yaml, indent=''): |
| dump_runner_caps(cls, indent) |
| dump_runner_option_help(cls, indent) |
| |
| if runners_yaml is None: |
| return |
| |
| if cls.name() in runners_yaml['runners']: |
| dump_runner_args(cls.name(), runners_yaml, indent) |
| else: |
| log.wrn(f'support for runner {cls.name()} is not configured ' |
| f'in this build directory') |
| |
| def dump_runner_caps(cls, indent=''): |
| # Print RunnerCaps for the given runner class. |
| |
| log.inf(f'{indent}{cls.name()} capabilities:', colorize=True) |
| log.inf(f'{indent}{INDENT}{cls.capabilities()}') |
| |
| def dump_runner_option_help(cls, indent=''): |
| # Print help text for class-specific command line options for the |
| # given runner class. |
| |
| dummy_parser = argparse.ArgumentParser(prog='', add_help=False) |
| cls.add_parser(dummy_parser) |
| formatter = dummy_parser._get_formatter() |
| for group in dummy_parser._action_groups: |
| # Break the abstraction to filter out the 'flash', 'debug', etc. |
| # TODO: come up with something cleaner (may require changes |
| # in the runner core). |
| actions = group._group_actions |
| if len(actions) == 1 and actions[0].dest == 'command': |
| # This is the lone positional argument. Skip it. |
| continue |
| formatter.start_section('REMOVE ME') |
| formatter.add_text(group.description) |
| formatter.add_arguments(actions) |
| formatter.end_section() |
| # Get the runner help, with the "REMOVE ME" string gone |
| runner_help = f'\n{indent}'.join(formatter.format_help().splitlines()[1:]) |
| |
| log.inf(f'{indent}{cls.name()} options:', colorize=True) |
| log.inf(indent + runner_help) |
| |
| def dump_runner_args(group, runners_yaml, indent=''): |
| msg = f'{indent}{group} arguments from runners.yaml:' |
| args = runners_yaml['args'][group] |
| if args: |
| log.inf(msg, colorize=True) |
| for arg in args: |
| log.inf(f'{indent}{INDENT}{arg}') |
| else: |
| log.inf(f'{msg} (none)', colorize=True) |
| |
| def dump_all_runner_context(command, runners_yaml, board, build_dir): |
| all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if |
| command.name in cls.capabilities().commands} |
| available = runners_yaml['runners'] |
| available_cls = {r: all_cls[r] for r in available if r in all_cls} |
| default_runner = runners_yaml[command.runner_key] |
| yaml_path = runners_yaml_path(build_dir, board) |
| runners_yaml = load_runners_yaml(yaml_path) |
| |
| log.inf(f'zephyr runners which support "west {command.name}":', |
| colorize=True) |
| dump_wrapped_lines(', '.join(all_cls.keys()), INDENT) |
| log.inf() |
| dump_wrapped_lines('Note: not all may work with this board and build ' |
| 'directory. Available runners are listed below.', |
| INDENT) |
| |
| log.inf(f'available runners in runners.yaml:', |
| colorize=True) |
| dump_wrapped_lines(', '.join(available), INDENT) |
| log.inf(f'default runner in runners.yaml:', colorize=True) |
| log.inf(INDENT + default_runner) |
| log.inf('common runner configuration:', colorize=True) |
| runner_config = get_runner_config(build_dir, yaml_path, runners_yaml) |
| for field, value in zip(runner_config._fields, runner_config): |
| log.inf(f'{INDENT}- {field}: {value}') |
| log.inf('runner-specific context:', colorize=True) |
| for cls in available_cls.values(): |
| dump_runner_context(command, cls, runners_yaml, INDENT) |
| |
| if len(available) > 1: |
| log.inf() |
| log.inf('Note: use -r RUNNER to limit information to one runner.') |
| |
| def dump_wrapped_lines(text, indent): |
| for line in textwrap.wrap(text, initial_indent=indent, |
| subsequent_indent=indent, |
| break_on_hyphens=False, |
| break_long_words=False): |
| log.inf(line) |