Skip to content

Commit 38e8f06

Browse files
Marti Bolivarnashif
authored andcommitted
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 <[email protected]>
1 parent 68e5933 commit 38e8f06

File tree

1 file changed

+217
-6
lines changed

1 file changed

+217
-6
lines changed

scripts/meta/west/cmd/run_common.py

Lines changed: 217 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,31 @@
88
import argparse
99
from os import getcwd, path
1010
from subprocess import CalledProcessError
11-
from textwrap import dedent
11+
import textwrap
1212

1313
from .. import cmake
1414
from .. import log
15-
from ..runner import get_runner_cls
15+
from .. import util
16+
from ..runner import get_runner_cls, ZephyrBinaryRunner
1617
from ..runner.core import RunnerConfig
1718
from . import CommandContextError
1819

20+
# Context-sensitive help indentation.
21+
# Don't change this, or output from argparse won't match up.
22+
INDENT = ' ' * 2
23+
1924

2025
def add_parser_common(parser_adder, command):
2126
parser = parser_adder.add_parser(
2227
command.name,
2328
formatter_class=argparse.RawDescriptionHelpFormatter,
2429
description=command.description)
2530

31+
parser.add_argument('-H', '--context', action='store_true',
32+
help='''Rebuild application and print context-sensitive
33+
help; this may be combined with --runner to restrict
34+
output to a given runner.''')
35+
2636
group = parser.add_argument_group(title='General Options')
2737

2838
group.add_argument('-d', '--build-dir',
@@ -46,7 +56,7 @@ def add_parser_common(parser_adder, command):
4656

4757
group = parser.add_argument_group(
4858
title='Configuration overrides',
49-
description=dedent('''\
59+
description=textwrap.dedent('''\
5060
These values usually come from the Zephyr build system itself
5161
as stored in the CMake cache; providing these options
5262
overrides those settings.'''))
@@ -79,9 +89,10 @@ def add_parser_common(parser_adder, command):
7989

8090

8191
def desc_common(command_name):
82-
return dedent('''\
92+
return textwrap.dedent('''\
8393
Any options not recognized by this command are passed to the
84-
back-end {command} runner.
94+
back-end {command} runner (run "west {command} --context"
95+
for help on available runner-specific options).
8596
8697
If you need to pass an option to a runner which has the
8798
same name as one recognized by this command, you can
@@ -117,6 +128,10 @@ def _override_config_from_namespace(cfg, namespace):
117128

118129

119130
def do_run_common(command, args, runner_args, cached_runner_var):
131+
if args.context:
132+
_dump_context(command, args, runner_args, cached_runner_var)
133+
return
134+
120135
command_name = command.name
121136
build_dir = args.build_dir or getcwd()
122137

@@ -147,7 +162,7 @@ def do_run_common(command, args, runner_args, cached_runner_var):
147162
runner = args.runner or cache.get(cached_runner_var)
148163

149164
if runner is None:
150-
raise CommandContextError(dedent("""
165+
raise CommandContextError(textwrap.dedent("""
151166
No {} runner available for {}. Please either specify one
152167
manually, or check your board's documentation for
153168
alternative instructions.""".format(command_name, board)))
@@ -196,3 +211,199 @@ def do_run_common(command, args, runner_args, cached_runner_var):
196211
'received unknown arguments', unknown)
197212
runner = runner_cls.create(cfg, parsed_args)
198213
runner.run(command_name)
214+
215+
216+
#
217+
# Context-specific help
218+
#
219+
220+
def _dump_context(command, args, runner_args, cached_runner_var):
221+
build_dir = args.build_dir or getcwd()
222+
223+
# If the cache is a file, try to ensure build artifacts are up to
224+
# date. If that doesn't work, still try to print information on a
225+
# best-effort basis.
226+
cache_file = path.abspath(path.join(build_dir, args.cmake_cache))
227+
cache = None
228+
229+
if path.isfile(cache_file):
230+
have_cache_file = True
231+
else:
232+
have_cache_file = False
233+
if args.build_dir:
234+
msg = textwrap.dedent('''\
235+
CMake cache {}: no such file or directory, --build-dir {}
236+
is invalid'''.format(cache_file, args.build_dir))
237+
log.die('\n'.join(textwrap.wrap(msg, initial_indent='',
238+
subsequent_indent=INDENT,
239+
break_on_hyphens=False)))
240+
else:
241+
msg = textwrap.dedent('''\
242+
No cache file {} found; is this a build directory?
243+
(Use --build-dir to set one if not, otherwise, output will be
244+
limited.)'''.format(cache_file))
245+
log.wrn('\n'.join(textwrap.wrap(msg, initial_indent='',
246+
subsequent_indent=INDENT,
247+
break_on_hyphens=False)))
248+
249+
if have_cache_file and not args.skip_rebuild:
250+
try:
251+
cmake.run_build(build_dir)
252+
except CalledProcessError:
253+
msg = 'Failed re-building application; cannot load context. '
254+
if args.build_dir:
255+
msg += 'Is {} the right --build-dir?'.format(args.build_dir)
256+
else:
257+
msg += textwrap.dedent('''\
258+
Use --build-dir (-d) to specify a build directory; the default
259+
is the current directory, {}.'''.format(build_dir))
260+
log.die('\n'.join(textwrap.wrap(msg, initial_indent='',
261+
subsequent_indent=INDENT,
262+
break_on_hyphens=False)))
263+
264+
if have_cache_file:
265+
try:
266+
cache = cmake.CMakeCache(cache_file)
267+
except Exception:
268+
log.die('Cannot load cache {}.'.format(cache_file))
269+
270+
if cache is None:
271+
_dump_no_context_info(command, args)
272+
if not args.runner:
273+
return
274+
275+
if args.runner:
276+
# Just information on one runner was requested.
277+
_dump_one_runner_info(cache, args, build_dir, INDENT)
278+
return
279+
280+
board = cache['CACHED_BOARD']
281+
282+
all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if
283+
command.name in cls.capabilities().commands}
284+
available = [r for r in cache.get_list('ZEPHYR_RUNNERS') if r in all_cls]
285+
available_cls = {r: all_cls[r] for r in available if r in all_cls}
286+
287+
default_runner = cache.get(cached_runner_var)
288+
cfg = cached_runner_config(build_dir, cache)
289+
290+
log.inf('All Zephyr runners which support {}:'.format(command.name))
291+
for line in util.wrap(', '.join(all_cls.keys()), INDENT):
292+
log.inf(line)
293+
log.inf('(Not all may work with this build, see available runners below.)')
294+
295+
if cache is None:
296+
log.warn('Missing or invalid CMake cache {}; there is no context.',
297+
'Use --build-dir to specify the build directory.')
298+
return
299+
300+
log.inf('Build directory:', build_dir)
301+
log.inf('Board:', board)
302+
log.inf('CMake cache:', cache_file)
303+
304+
if not available:
305+
# Bail with a message if no runners are available.
306+
msg = ('No runners available for {}. '
307+
'Consult the documentation for instructions on how to run '
308+
'binaries on this target.').format(board)
309+
for line in util.wrap(msg, ''):
310+
log.inf(line)
311+
return
312+
313+
log.inf('Available {} runners:'.format(command.name), ', '.join(available))
314+
log.inf('Additional options for available', command.name, 'runners:')
315+
for runner in available:
316+
_dump_runner_opt_help(runner, all_cls[runner])
317+
log.inf('Default {} runner: {}'.format(command.name, default_runner))
318+
_dump_runner_config(cfg, '', INDENT)
319+
log.inf('Runner-specific information:')
320+
for runner in available:
321+
log.inf('{}{}:'.format(INDENT, runner))
322+
_dump_runner_cached_opts(cache, runner, INDENT * 2, INDENT * 3)
323+
_dump_runner_caps(available_cls[runner], INDENT * 2)
324+
325+
if len(available) > 1:
326+
log.inf('(Add -r RUNNER to just print information about one runner.)')
327+
328+
329+
def _dump_no_context_info(command, args):
330+
all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if
331+
command.name in cls.capabilities().commands}
332+
log.inf('All Zephyr runners which support {}:'.format(command.name))
333+
for line in util.wrap(', '.join(all_cls.keys()), INDENT):
334+
log.inf(line)
335+
if not args.runner:
336+
log.inf('Add -r RUNNER to print more information about any runner.')
337+
338+
339+
def _dump_one_runner_info(cache, args, build_dir, indent):
340+
runner = args.runner
341+
cls = get_runner_cls(runner)
342+
343+
if cache is None:
344+
_dump_runner_opt_help(runner, cls)
345+
_dump_runner_caps(cls, '')
346+
return
347+
348+
available = runner in cache.get_list('ZEPHYR_RUNNERS')
349+
cfg = cached_runner_config(build_dir, cache)
350+
351+
log.inf('Build directory:', build_dir)
352+
log.inf('Board:', cache['CACHED_BOARD'])
353+
log.inf('CMake cache:', cache.cache_file)
354+
log.inf(runner, 'is available:', 'yes' if available else 'no')
355+
_dump_runner_opt_help(runner, cls)
356+
_dump_runner_config(cfg, '', indent)
357+
if available:
358+
_dump_runner_cached_opts(cache, runner, '', indent)
359+
_dump_runner_caps(cls, '')
360+
if not available:
361+
log.wrn('Runner', runner, 'is not configured in this build.')
362+
363+
364+
def _dump_runner_caps(cls, base_indent):
365+
log.inf('{}Capabilities:'.format(base_indent))
366+
log.inf('{}{}'.format(base_indent + INDENT, cls.capabilities()))
367+
368+
369+
def _dump_runner_opt_help(runner, cls):
370+
# Construct and print the usage text
371+
dummy_parser = argparse.ArgumentParser(prog='', add_help=False)
372+
cls.add_parser(dummy_parser)
373+
formatter = dummy_parser._get_formatter()
374+
for group in dummy_parser._action_groups:
375+
# Break the abstraction to filter out the 'flash', 'debug', etc.
376+
# TODO: come up with something cleaner (may require changes
377+
# in the runner core).
378+
actions = group._group_actions
379+
if len(actions) == 1 and actions[0].dest == 'command':
380+
# This is the lone positional argument. Skip it.
381+
continue
382+
formatter.start_section('{} option help'.format(runner))
383+
formatter.add_text(group.description)
384+
formatter.add_arguments(actions)
385+
formatter.end_section()
386+
log.inf(formatter.format_help())
387+
388+
389+
def _dump_runner_config(cfg, initial_indent, subsequent_indent):
390+
log.inf('{}Cached common runner configuration:'.format(initial_indent))
391+
for var in cfg.__slots__:
392+
log.inf('{}--{}={}'.format(subsequent_indent, var, getattr(cfg, var)))
393+
394+
395+
def _dump_runner_cached_opts(cache, runner, initial_indent, subsequent_indent):
396+
runner_args = _get_runner_args(cache, runner)
397+
if not runner_args:
398+
return
399+
400+
log.inf('{}Cached runner-specific options:'.format(
401+
initial_indent))
402+
for arg in runner_args:
403+
log.inf('{}{}'.format(subsequent_indent, arg))
404+
405+
406+
def _get_runner_args(cache, runner):
407+
runner_ident = cmake.make_c_identifier(runner)
408+
args_var = 'ZEPHYR_RUNNER_ARGS_{}'.format(runner_ident)
409+
return cache.get_list(args_var)

0 commit comments

Comments
 (0)