zephyr/scripts/west_commands/run_common.py
Torsten Rasmussen 13642fe013 cmake: west: west flash dependencies in sync with CMake flash target
This commit creates a list of a phony targets for each runner, that is:
`west_flash_depends`, `west_debug_depends`, and so on.
Those targets has identical dependencies as CMake runner target.
flash, debug, debugserver, attach targets.

As example `ninja flash` correctly ensures dependencies are taken into
consideration before calling `west flash`.

Unfortunately, calling `west flash` directly would not re-run the flash
dependencies, cause `west flash` would only build the default CMake
target.

Now, `west flash` calls the phony `west_flash_depends` target, ensuring
all deps are up-to-date before flashing (unless --skip-rebuild is given)

The same is true for the other mentioned runners.

Signed-off-by: Torsten Rasmussen <Torsten.Rasmussen@nordicsemi.no>
2020-07-01 07:58:10 -04:00

480 lines
18 KiB
Python

# 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 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, \
FIND_BUILD_DIR_DESCRIPTION
from west.commands import CommandError
from west.configuration import config
import yaml
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 scripts/west-completion.bash if you add or remove
# flags
parser.add_argument('-H', '--context', action='store_true',
help='print build directory specific help')
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 = parser.add_argument_group(
'runner configuration overrides',
textwrap.dedent(f'''\
Run "west {command.name} --context" for --build-dir specific options.
Not all runners respect --elf-file / --hex-file / --bin-file, nor use
gdb or openocd.'''))
# 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',
help='path to add to openocd search path, if applicable')
return parser
def do_run_common(command, user_args, user_runner_args):
# 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
command_name = command.name
build_dir = get_build_dir(user_args)
cache = load_cmake_cache(build_dir, user_args)
board = cache['CACHED_BOARD']
if not user_args.skip_rebuild:
rebuild(command, build_dir, user_args)
# Load runners.yaml.
runners_yaml = runners_yaml_path(cache)
runner_config = load_runners_yaml(runners_yaml, user_args)
# Get a concrete ZephyrBinaryRunner subclass to use based on
# runners.yaml and command line arguments.
runner_cls = use_runner_cls(command, board, user_args, runner_config,
cache)
runner_name = runner_cls.name()
# Set up runner logging to delegate to west.log commands.
logger = logging.getLogger('runners')
logger.setLevel(LOG_LEVEL)
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 are provided in this order to allow the specific to
# override the general:
#
# - common runners.yaml arguments
# - runner-specific runners.yaml arguments
# - command line arguments
final_argv = (runner_config['args']['common'] +
runner_config['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 the values assigned to common
# arguments. This is a hacky way to go about this; probably
# ZephyrBinaryRunner should define what it needs to make this
# happen by itself. That would be a larger refactoring of the
# runners package than there's time for right now, though.
#
# Use that RunnerConfig to create the ZephyrBinaryRunner instance
# and call its run().
try:
runner = runner_cls.create(runner_cfg_from_args(args, build_dir), 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')
extra_args = ['--target', 'west_' + command.name + '_depends']
try:
zcmake.run_build(build_dir, extra_args=extra_args)
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(cache):
try:
return cache['ZEPHYR_RUNNERS_YAML']
except KeyError:
board = cache.get('CACHED_BOARD')
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)')
def load_runners_yaml(path, args):
# Load runners.yaml using its location in the CMake cache,
# allowing a command line override for the cache file location.
try:
with open(path, 'r') as f:
config = yaml.safe_load(f.read())
except FileNotFoundError:
log.die(f'runners.yaml file not found: {path}')
if not config.get('runners'):
log.wrn(f'no pre-configured runners in {path}; '
"this probably won't work")
return config
def use_runner_cls(command, board, args, runner_config, 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 runner_config.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 = runner_config.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)
runner_cls = get_runner_cls(runner)
if command.name not in runner_cls.capabilities().commands:
log.die(f'runner {runner} does not support command {command.name}')
return runner_cls
def runner_cfg_from_args(args, build_dir):
return RunnerConfig(build_dir, args.board_dir, args.elf_file,
args.hex_file, args.bin_file,
gdb=args.gdb, openocd=args.openocd,
openocd_search=args.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')
runner_config = None
else:
cache = load_cmake_cache(build_dir, args)
board = cache['CACHED_BOARD']
runners_yaml = runners_yaml_path(cache)
runner_config = load_runners_yaml(runners_yaml, args)
# 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 runner_config 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: {runners_yaml}')
if cls:
dump_runner_context(command, cls, runner_config)
else:
dump_all_runner_context(command, runner_config, 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 runner_config=None.
dump_runner_context(command, cls, None)
def dump_runner_context(command, cls, runner_config, indent=''):
dump_runner_caps(cls, indent)
dump_runner_option_help(cls, indent)
if runner_config is None:
return
if cls.name() in runner_config['runners']:
dump_runner_args(cls.name(), runner_config, 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, runner_config, indent=''):
msg = f'{indent}{group} arguments from runners.yaml:'
args = runner_config['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, runner_config, board, build_dir):
all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if
command.name in cls.capabilities().commands}
available = runner_config['runners']
available_cls = {r: all_cls[r] for r in available if r in all_cls}
default_runner = runner_config[command.runner_key]
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)
dump_runner_args('common', runner_config)
log.inf('runner-specific context:', colorize=True)
for cls in available_cls.values():
dump_runner_context(command, cls, runner_config, 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)