scripts: west: add context-sensitive runner help

Though commands like "west flash -h" now have help for generic runner
configuration options, runner-specific context is not printed.

In order to print this information, we would ideally want to know the
currently available runners from a build directory. Otherwise, we
can't print the current cached configuration, and the user will likely
be overwhelmed by a giant list of options etc. available for all the
runners in the package.

However, we can't print that information out without re-doing the
build, which is not safe to do when the user just gives '--help'.

To provide more complete help without causing side effects in the
default help output, add a new -H/--context option, which explicitly
re-runs the build (unless --skip-rebuild was given), parses the cache,
and prints context-sensitive help. This can be combined with the -r
option to restrict help to a particular runner.

Examples:

- Print context for all available flash runners:

      west flash -H --build-dir build-frdm_k64f/

- Print context for just one runner:

      west flash -H --build-dir build-frdm_k64f/ -r jlink

- Print context for all available debug runners, if current
  working directory is a build directory:

      west debug -H

If no context is available because there is no CMake cache file, this
command can still be used to obtain generic information about
runners. It emits a warning in this case.

Signed-off-by: Marti Bolivar <marti@opensourcefoundries.com>
This commit is contained in:
Marti Bolivar 2018-05-10 17:20:10 -04:00 committed by Anas Nashif
commit 38e8f06fdb

View file

@ -8,14 +8,19 @@
import argparse
from os import getcwd, path
from subprocess import CalledProcessError
from textwrap import dedent
import textwrap
from .. import cmake
from .. import log
from ..runner import get_runner_cls
from .. import util
from ..runner import get_runner_cls, ZephyrBinaryRunner
from ..runner.core import RunnerConfig
from . import CommandContextError
# Context-sensitive help indentation.
# Don't change this, or output from argparse won't match up.
INDENT = ' ' * 2
def add_parser_common(parser_adder, command):
parser = parser_adder.add_parser(
@ -23,6 +28,11 @@ def add_parser_common(parser_adder, command):
formatter_class=argparse.RawDescriptionHelpFormatter,
description=command.description)
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',
@ -46,7 +56,7 @@ def add_parser_common(parser_adder, command):
group = parser.add_argument_group(
title='Configuration overrides',
description=dedent('''\
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.'''))
@ -79,9 +89,10 @@ def add_parser_common(parser_adder, command):
def desc_common(command_name):
return dedent('''\
return textwrap.dedent('''\
Any options not recognized by this command are passed to the
back-end {command} runner.
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
@ -117,6 +128,10 @@ def _override_config_from_namespace(cfg, namespace):
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 = args.build_dir or getcwd()
@ -147,7 +162,7 @@ def do_run_common(command, args, runner_args, cached_runner_var):
runner = args.runner or cache.get(cached_runner_var)
if runner is None:
raise CommandContextError(dedent("""
raise CommandContextError(textwrap.dedent("""
No {} runner available for {}. Please either specify one
manually, or check your board's documentation for
alternative instructions.""".format(command_name, board)))
@ -196,3 +211,199 @@ def do_run_common(command, args, runner_args, cached_runner_var):
'received unknown arguments', unknown)
runner = runner_cls.create(cfg, parsed_args)
runner.run(command_name)
#
# Context-specific help
#
def _dump_context(command, args, runner_args, cached_runner_var):
build_dir = args.build_dir or getcwd()
# If the cache is a file, try to ensure build artifacts are up to
# date. If that doesn't work, still try to print information on a
# best-effort basis.
cache_file = path.abspath(path.join(build_dir, args.cmake_cache))
cache = None
if path.isfile(cache_file):
have_cache_file = True
else:
have_cache_file = False
if args.build_dir:
msg = textwrap.dedent('''\
CMake cache {}: no such file or directory, --build-dir {}
is invalid'''.format(cache_file, args.build_dir))
log.die('\n'.join(textwrap.wrap(msg, initial_indent='',
subsequent_indent=INDENT,
break_on_hyphens=False)))
else:
msg = textwrap.dedent('''\
No cache file {} found; is this a build directory?
(Use --build-dir to set one if not, otherwise, output will be
limited.)'''.format(cache_file))
log.wrn('\n'.join(textwrap.wrap(msg, initial_indent='',
subsequent_indent=INDENT,
break_on_hyphens=False)))
if have_cache_file 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 default
is the current directory, {}.'''.format(build_dir))
log.die('\n'.join(textwrap.wrap(msg, initial_indent='',
subsequent_indent=INDENT,
break_on_hyphens=False)))
if have_cache_file:
try:
cache = cmake.CMakeCache(cache_file)
except Exception:
log.die('Cannot load cache {}.'.format(cache_file))
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))
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.)')
if cache is None:
log.warn('Missing or invalid CMake cache {}; there is no context.',
'Use --build-dir to specify the build directory.')
return
log.inf('Build directory:', build_dir)
log.inf('Board:', board)
log.inf('CMake cache:', 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)
return
log.inf('Available {} runners:'.format(command.name), ', '.join(available))
log.inf('Additional options for available', command.name, 'runners:')
for runner in available:
_dump_runner_opt_help(runner, all_cls[runner])
log.inf('Default {} runner: {}'.format(command.name, default_runner))
_dump_runner_config(cfg, '', INDENT)
log.inf('Runner-specific information:')
for runner in available:
log.inf('{}{}:'.format(INDENT, runner))
_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.)')
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))
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.')
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:', build_dir)
log.inf('Board:', cache['CACHED_BOARD'])
log.inf('CMake cache:', cache.cache_file)
log.inf(runner, 'is available:', 'yes' if available else 'no')
_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))
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('{} option help'.format(runner))
formatter.add_text(group.description)
formatter.add_arguments(actions)
formatter.end_section()
log.inf(formatter.format_help())
def _dump_runner_config(cfg, initial_indent, subsequent_indent):
log.inf('{}Cached common runner configuration:'.format(initial_indent))
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))
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)