| # 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 |
| from subprocess import CalledProcessError |
| import tempfile |
| import textwrap |
| import traceback |
| |
| from west import cmake |
| from west import log |
| from west import util |
| from build_helpers import find_build_dir, is_zephyr_build, \ |
| FIND_BUILD_DIR_DESCRIPTION |
| from west.commands import CommandError |
| from west.configuration import config |
| |
| from runners import get_runner_cls, ZephyrBinaryRunner, MissingProgram |
| |
| from zephyr_ext_common import cached_runner_config |
| |
| # 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 add_parser_common(parser_adder, command): |
| parser = parser_adder.add_parser( |
| command.name, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| help=command.help, |
| description=command.description) |
| |
| # Remember to update scripts/west-completion.bash if you add or remove |
| # flags |
| |
| parser.add_argument('-H', '--context', action='store_true', |
| help='''Rebuild application and print context-sensitive |
| help; this may be combined with --runner to restrict |
| output to a given runner.''') |
| |
| group = parser.add_argument_group(title='General Options') |
| |
| group.add_argument('-d', '--build-dir', |
| help='Build directory to obtain runner information ' + |
| 'from. ' + FIND_BUILD_DIR_DESCRIPTION) |
| group.add_argument('-c', '--cmake-cache', |
| help='''Path to CMake cache file containing runner |
| configuration (this is generated by the Zephyr |
| build system when compiling binaries); |
| default: {}. |
| |
| If this is a relative path, it is assumed relative to |
| the build directory. An absolute path can also be |
| given instead.'''.format(cmake.DEFAULT_CACHE)) |
| group.add_argument('-r', '--runner', |
| help='''If given, overrides any cached {} |
| runner.'''.format(command.name)) |
| group.add_argument('--skip-rebuild', action='store_true', |
| help='''If given, do not rebuild the application |
| before running {} commands.'''.format(command.name)) |
| |
| group = parser.add_argument_group( |
| title='Configuration overrides', |
| description=textwrap.dedent('''\ |
| These values usually come from the Zephyr build system itself |
| as stored in the CMake cache; providing these options |
| overrides those settings.''')) |
| |
| # Important: |
| # |
| # 1. The destination variables of these options must match |
| # the RunnerConfig slots. |
| # 2. The default values for all of these must be None. |
| # |
| # This is how we detect if the user provided them or not when |
| # overriding values from the cached configuration. |
| |
| command_verb = "flash" if command == "flash" else "debug" |
| |
| group.add_argument('--board-dir', |
| help='Zephyr board directory') |
| group.add_argument('--elf-file', |
| help='Path to elf file to {0}'.format(command_verb)) |
| group.add_argument('--hex-file', |
| help='Path to hex file to {0}'.format(command_verb)) |
| group.add_argument('--bin-file', |
| help='Path to binary file to {0}'.format(command_verb)) |
| group.add_argument('--gdb', |
| help='Path to GDB, if applicable') |
| group.add_argument('--openocd', |
| help='Path to OpenOCD, if applicable') |
| group.add_argument( |
| '--openocd-search', |
| help='Path to add to OpenOCD search path, if applicable') |
| |
| return parser |
| |
| |
| def desc_common(command_name): |
| return textwrap.dedent('''\ |
| Any options not recognized by this command are passed to the |
| back-end {command} runner (run "west {command} --context" |
| for help on available runner-specific options). |
| |
| If you need to pass an option to a runner which has the |
| same name as one recognized by this command, you can |
| end argument parsing with a '--', like so: |
| |
| west {command} --{command}-arg=value -- --runner-arg=value2 |
| '''.format(**{'command': command_name})) |
| |
| |
| def _override_config_from_namespace(cfg, namespace): |
| '''Override a RunnerConfig's contents with command-line values.''' |
| for var in cfg.__slots__: |
| if var in namespace: |
| val = getattr(namespace, var) |
| if val is not None: |
| setattr(cfg, var, val) |
| |
| |
| def _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 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) |
| |
| def do_run_common(command, args, runner_args, cached_runner_var): |
| if args.context: |
| _dump_context(command, args, runner_args, cached_runner_var) |
| return |
| |
| command_name = command.name |
| build_dir = _build_dir(args) |
| |
| if not args.skip_rebuild: |
| _banner('west {}: rebuilding'.format(command_name)) |
| try: |
| cmake.run_build(build_dir) |
| except CalledProcessError: |
| if args.build_dir: |
| log.die('cannot run {}, build in {} failed'.format( |
| command_name, args.build_dir)) |
| else: |
| log.die('cannot run {}; no --build-dir given and build in ' |
| 'current directory {} failed'.format(command_name, |
| build_dir)) |
| |
| # Runner creation, phase 1. |
| # |
| # Get the default runner name from the cache, allowing a command |
| # line override. Get the ZephyrBinaryRunner class by name, and |
| # make sure it supports the command. |
| |
| cache_file = path.join(build_dir, args.cmake_cache or cmake.DEFAULT_CACHE) |
| try: |
| cache = cmake.CMakeCache(cache_file) |
| except FileNotFoundError: |
| log.die('no CMake cache found (expected one at {})'.format(cache_file)) |
| board = cache['CACHED_BOARD'] |
| available = cache.get_list('ZEPHYR_RUNNERS') |
| if not available: |
| log.wrn('No cached runners are available in', cache_file) |
| runner = args.runner or cache.get(cached_runner_var) |
| |
| if runner is None: |
| log.die('No', command_name, 'runner available for board', board, |
| '({} is not in the cache).'.format(cached_runner_var), |
| "Check your board's documentation for instructions.") |
| |
| _banner('west {}: using runner {}'.format(command_name, runner)) |
| if runner not in available: |
| log.wrn('Runner {} is not configured for use with {}, ' |
| 'this may not work'.format(runner, board)) |
| runner_cls = get_runner_cls(runner) |
| if command_name not in runner_cls.capabilities().commands: |
| log.die('Runner {} does not support command {}'.format( |
| runner, command_name)) |
| |
| # Runner creation, phase 2. |
| # |
| # At this point, the common options above are already parsed in |
| # 'args', and unrecognized arguments are in 'runner_args'. |
| # |
| # - Set up runner logging to delegate to west. |
| # - Pull the RunnerConfig out of the cache |
| # - Override cached values with applicable command-line options |
| |
| logger = logging.getLogger('runners') |
| logger.setLevel(LOG_LEVEL) |
| logger.addHandler(WestLogHandler()) |
| cfg = cached_runner_config(build_dir, cache) |
| _override_config_from_namespace(cfg, args) |
| |
| # Runner creation, phase 3. |
| # |
| # - Pull out cached runner arguments, and append command-line |
| # values (which should override the cache) |
| # - Construct a runner-specific argument parser to handle cached |
| # values plus overrides given in runner_args |
| # - Parse arguments and create runner instance from final |
| # RunnerConfig and parsed arguments. |
| |
| cached_runner_args = cache.get_list( |
| 'ZEPHYR_RUNNER_ARGS_{}'.format(cmake.make_c_identifier(runner))) |
| assert isinstance(runner_args, list), runner_args |
| # 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 runner_args if arg != '--'] |
| final_runner_args = cached_runner_args + runner_args |
| parser = argparse.ArgumentParser(prog=runner) |
| runner_cls.add_parser(parser) |
| parsed_args, unknown = parser.parse_known_args(args=final_runner_args) |
| if unknown: |
| log.die('Runner', runner, 'received unknown arguments:', unknown) |
| runner = runner_cls.create(cfg, parsed_args) |
| try: |
| 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') |
| |
| |
| # |
| # Context-specific help |
| # |
| |
| def _dump_context(command, args, runner_args, cached_runner_var): |
| build_dir = _build_dir(args, die_if_none=False) |
| |
| # Try to figure out the CMake cache file based on the build |
| # directory or an explicit argument. |
| if build_dir is not None: |
| cache_file = path.abspath( |
| path.join(build_dir, args.cmake_cache or cmake.DEFAULT_CACHE)) |
| elif args.cmake_cache: |
| cache_file = path.abspath(args.cmake_cache) |
| else: |
| cache_file = None |
| |
| # Load the cache itself, if possible. |
| if cache_file is None: |
| log.wrn('No build directory (--build-dir) or CMake cache ' |
| '(--cmake-cache) given or found; output will be limited') |
| cache = None |
| else: |
| try: |
| cache = cmake.CMakeCache(cache_file) |
| except Exception: |
| log.die('Cannot load cache {}.'.format(cache_file)) |
| |
| # If we have a build directory, try to ensure build artifacts are |
| # up to date. If that doesn't work, still try to print information |
| # on a best-effort basis. |
| if build_dir and not args.skip_rebuild: |
| try: |
| cmake.run_build(build_dir) |
| except CalledProcessError: |
| msg = 'Failed re-building application; cannot load context. ' |
| if args.build_dir: |
| msg += 'Is {} the right --build-dir?'.format(args.build_dir) |
| else: |
| msg += textwrap.dedent('''\ |
| Use --build-dir (-d) to specify a build directory; the one |
| used was {}.'''.format(build_dir)) |
| log.die('\n'.join(textwrap.wrap(msg, initial_indent='', |
| subsequent_indent=INDENT, |
| break_on_hyphens=False))) |
| |
| if cache is None: |
| _dump_no_context_info(command, args) |
| if not args.runner: |
| return |
| |
| if args.runner: |
| # Just information on one runner was requested. |
| _dump_one_runner_info(cache, args, build_dir, INDENT) |
| return |
| |
| board = cache['CACHED_BOARD'] |
| |
| all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if |
| command.name in cls.capabilities().commands} |
| available = [r for r in cache.get_list('ZEPHYR_RUNNERS') if r in all_cls] |
| available_cls = {r: all_cls[r] for r in available if r in all_cls} |
| |
| default_runner = cache.get(cached_runner_var) |
| cfg = cached_runner_config(build_dir, cache) |
| |
| log.inf('All Zephyr runners which support {}:'.format(command.name), |
| colorize=True) |
| for line in util.wrap(', '.join(all_cls.keys()), INDENT): |
| log.inf(line) |
| log.inf('(Not all may work with this build, see available runners below.)', |
| colorize=True) |
| |
| if cache is None: |
| log.wrn('Missing or invalid CMake cache; there is no context.', |
| 'Use --build-dir to specify the build directory.') |
| return |
| |
| log.inf('Build directory:', colorize=True) |
| log.inf(INDENT + build_dir) |
| log.inf('Board:', colorize=True) |
| log.inf(INDENT + board) |
| log.inf('CMake cache:', colorize=True) |
| log.inf(INDENT + cache_file) |
| |
| if not available: |
| # Bail with a message if no runners are available. |
| msg = ('No runners available for {}. ' |
| 'Consult the documentation for instructions on how to run ' |
| 'binaries on this target.').format(board) |
| for line in util.wrap(msg, ''): |
| log.inf(line, colorize=True) |
| return |
| |
| log.inf('Available {} runners:'.format(command.name), colorize=True) |
| log.inf(INDENT + ', '.join(available)) |
| log.inf('Additional options for available', command.name, 'runners:', |
| colorize=True) |
| for runner in available: |
| _dump_runner_opt_help(runner, all_cls[runner]) |
| log.inf('Default {} runner:'.format(command.name), colorize=True) |
| log.inf(INDENT + default_runner) |
| _dump_runner_config(cfg, '', INDENT) |
| log.inf('Runner-specific information:', colorize=True) |
| for runner in available: |
| log.inf('{}{}:'.format(INDENT, runner), colorize=True) |
| _dump_runner_cached_opts(cache, runner, INDENT * 2, INDENT * 3) |
| _dump_runner_caps(available_cls[runner], INDENT * 2) |
| |
| if len(available) > 1: |
| log.inf('(Add -r RUNNER to just print information about one runner.)', |
| colorize=True) |
| |
| |
| def _dump_no_context_info(command, args): |
| 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) |
| for line in util.wrap(', '.join(all_cls.keys()), INDENT): |
| log.inf(line) |
| if not args.runner: |
| log.inf('Add -r RUNNER to print more information about any runner.', |
| colorize=True) |
| |
| |
| def _dump_one_runner_info(cache, args, build_dir, indent): |
| runner = args.runner |
| cls = get_runner_cls(runner) |
| |
| if cache is None: |
| _dump_runner_opt_help(runner, cls) |
| _dump_runner_caps(cls, '') |
| return |
| |
| available = runner in cache.get_list('ZEPHYR_RUNNERS') |
| cfg = cached_runner_config(build_dir, cache) |
| |
| log.inf('Build directory:', colorize=True) |
| log.inf(INDENT + build_dir) |
| log.inf('Board:', colorize=True) |
| log.inf(INDENT + cache['CACHED_BOARD']) |
| log.inf('CMake cache:', colorize=True) |
| log.inf(INDENT + cache.cache_file) |
| log.inf(runner, 'is available:', 'yes' if available else 'no', |
| colorize=True) |
| _dump_runner_opt_help(runner, cls) |
| _dump_runner_config(cfg, '', indent) |
| if available: |
| _dump_runner_cached_opts(cache, runner, '', indent) |
| _dump_runner_caps(cls, '') |
| if not available: |
| log.wrn('Runner', runner, 'is not configured in this build.') |
| |
| |
| def _dump_runner_caps(cls, base_indent): |
| log.inf('{}Capabilities:'.format(base_indent), colorize=True) |
| log.inf('{}{}'.format(base_indent + INDENT, cls.capabilities())) |
| |
| |
| def _dump_runner_opt_help(runner, cls): |
| # Construct and print the usage text |
| 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 = '\n'.join(formatter.format_help().splitlines()[1:]) |
| |
| log.inf('{} options:'.format(runner), colorize=True) |
| log.inf(runner_help) |
| |
| |
| def _dump_runner_config(cfg, initial_indent, subsequent_indent): |
| log.inf('{}Cached common runner configuration:'.format(initial_indent), |
| colorize=True) |
| for var in cfg.__slots__: |
| log.inf('{}--{}={}'.format(subsequent_indent, var, getattr(cfg, var))) |
| |
| |
| def _dump_runner_cached_opts(cache, runner, initial_indent, subsequent_indent): |
| runner_args = _get_runner_args(cache, runner) |
| if not runner_args: |
| return |
| |
| log.inf('{}Cached runner-specific options:'.format(initial_indent), |
| colorize=True) |
| for arg in runner_args: |
| log.inf('{}{}'.format(subsequent_indent, arg)) |
| |
| |
| def _get_runner_args(cache, runner): |
| runner_ident = cmake.make_c_identifier(runner) |
| args_var = 'ZEPHYR_RUNNER_ARGS_{}'.format(runner_ident) |
| return cache.get_list(args_var) |