553 lines
20 KiB
Python
553 lines
20 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, 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)
|