2e3f8a5f98
Support `zephyr-app-commands` being provided with a `snippets`, option, to specify building with the listed snippets. Signed-off-by: Jordan Yates <jordan.yates@data61.csiro.au>
476 lines
18 KiB
Python
476 lines
18 KiB
Python
# Copyright (c) 2017 Open Source Foundries Limited.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
'''Sphinx extensions related to managing Zephyr applications.'''
|
|
|
|
from docutils import nodes
|
|
from docutils.parsers.rst import Directive
|
|
from docutils.parsers.rst import directives
|
|
|
|
|
|
# TODO: extend and modify this for Windows.
|
|
#
|
|
# This could be as simple as generating a couple of sets of instructions, one
|
|
# for Unix environments, and another for Windows.
|
|
class ZephyrAppCommandsDirective(Directive):
|
|
r'''
|
|
This is a Zephyr directive for generating consistent documentation
|
|
of the shell commands needed to manage (build, flash, etc.) an application.
|
|
|
|
For example, to generate commands to build samples/hello_world for
|
|
qemu_x86 use::
|
|
|
|
.. zephyr-app-commands::
|
|
:zephyr-app: samples/hello_world
|
|
:board: qemu_x86
|
|
:goals: build
|
|
|
|
Directive options:
|
|
|
|
\:tool:
|
|
which tool to use. Valid options are currently 'cmake', 'west' and 'all'.
|
|
The default is 'west'.
|
|
|
|
\:app:
|
|
path to the application to build.
|
|
|
|
\:zephyr-app:
|
|
path to the application to build, this is an app present in the upstream
|
|
zephyr repository. Mutually exclusive with \:app:.
|
|
|
|
\:cd-into:
|
|
if set, build instructions are given from within the \:app: folder,
|
|
instead of outside of it.
|
|
|
|
\:generator:
|
|
which build system to generate. Valid options are
|
|
currently 'ninja' and 'make'. The default is 'ninja'. This option
|
|
is not case sensitive.
|
|
|
|
\:host-os:
|
|
which host OS the instructions are for. Valid options are
|
|
'unix', 'win' and 'all'. The default is 'all'.
|
|
|
|
\:board:
|
|
if set, the application build will target the given board.
|
|
|
|
\:shield:
|
|
if set, the application build will target the given shield.
|
|
|
|
\:conf:
|
|
if set, the application build will use the given configuration
|
|
file. If multiple conf files are provided, enclose the
|
|
space-separated list of files with quotes, e.g., "a.conf b.conf".
|
|
|
|
\:gen-args:
|
|
if set, additional arguments to the CMake invocation
|
|
|
|
\:build-args:
|
|
if set, additional arguments to the build invocation
|
|
|
|
\:snippets:
|
|
if set, indicates the application should be compiled with the listed snippets.
|
|
Multiple snippets can be provided in a comma separated list.
|
|
|
|
\:build-dir:
|
|
if set, the application build directory will *APPEND* this
|
|
(relative, Unix-separated) path to the standard build directory. This is
|
|
mostly useful for distinguishing builds for one application within a
|
|
single page.
|
|
|
|
\:build-dir-fmt:
|
|
if set, assume that "west config build.dir-fmt" has been set to this
|
|
path. Exclusive with 'build-dir' and depends on 'tool=west'.
|
|
|
|
\:goals:
|
|
a whitespace-separated list of what to do with the app (in
|
|
'build', 'flash', 'debug', 'debugserver', 'run'). Commands to accomplish
|
|
these tasks will be generated in the right order.
|
|
|
|
\:maybe-skip-config:
|
|
if set, this indicates the reader may have already
|
|
created a build directory and changed there, and will tweak the text to
|
|
note that doing so again is not necessary.
|
|
|
|
\:compact:
|
|
if set, the generated output is a single code block with no
|
|
additional comment lines
|
|
|
|
\:west-args:
|
|
if set, additional arguments to the west invocation (ignored for CMake)
|
|
|
|
\:flash-args:
|
|
if set, additional arguments to the flash invocation
|
|
|
|
'''
|
|
has_content = False
|
|
required_arguments = 0
|
|
optional_arguments = 0
|
|
final_argument_whitespace = False
|
|
option_spec = {
|
|
'tool': directives.unchanged,
|
|
'app': directives.unchanged,
|
|
'zephyr-app': directives.unchanged,
|
|
'cd-into': directives.flag,
|
|
'generator': directives.unchanged,
|
|
'host-os': directives.unchanged,
|
|
'board': directives.unchanged,
|
|
'shield': directives.unchanged,
|
|
'conf': directives.unchanged,
|
|
'gen-args': directives.unchanged,
|
|
'build-args': directives.unchanged,
|
|
'snippets': directives.unchanged,
|
|
'build-dir': directives.unchanged,
|
|
'build-dir-fmt': directives.unchanged,
|
|
'goals': directives.unchanged_required,
|
|
'maybe-skip-config': directives.flag,
|
|
'compact': directives.flag,
|
|
'west-args': directives.unchanged,
|
|
'flash-args': directives.unchanged,
|
|
}
|
|
|
|
TOOLS = ['cmake', 'west', 'all']
|
|
GENERATORS = ['make', 'ninja']
|
|
HOST_OS = ['unix', 'win', 'all']
|
|
IN_TREE_STR = '# From the root of the zephyr repository'
|
|
|
|
def run(self):
|
|
# Re-run on the current document if this directive's source changes.
|
|
self.state.document.settings.env.note_dependency(__file__)
|
|
|
|
# Parse directive options. Don't use os.path.sep or os.path.join here!
|
|
# That would break if building the docs on Windows.
|
|
tool = self.options.get('tool', 'west').lower()
|
|
app = self.options.get('app', None)
|
|
zephyr_app = self.options.get('zephyr-app', None)
|
|
cd_into = 'cd-into' in self.options
|
|
generator = self.options.get('generator', 'ninja').lower()
|
|
host_os = self.options.get('host-os', 'all').lower()
|
|
board = self.options.get('board', None)
|
|
shield = self.options.get('shield', None)
|
|
conf = self.options.get('conf', None)
|
|
gen_args = self.options.get('gen-args', None)
|
|
build_args = self.options.get('build-args', None)
|
|
snippets = self.options.get('snippets', None)
|
|
build_dir_append = self.options.get('build-dir', '').strip('/')
|
|
build_dir_fmt = self.options.get('build-dir-fmt', None)
|
|
goals = self.options.get('goals').split()
|
|
skip_config = 'maybe-skip-config' in self.options
|
|
compact = 'compact' in self.options
|
|
west_args = self.options.get('west-args', None)
|
|
flash_args = self.options.get('flash-args', None)
|
|
|
|
if tool not in self.TOOLS:
|
|
raise self.error('Unknown tool {}; choose from: {}'.format(
|
|
tool, self.TOOLS))
|
|
|
|
if app and zephyr_app:
|
|
raise self.error('Both app and zephyr-app options were given.')
|
|
|
|
if build_dir_append != '' and build_dir_fmt:
|
|
raise self.error('Both build-dir and build-dir-fmt options were given.')
|
|
|
|
if build_dir_fmt and tool != 'west':
|
|
raise self.error('build-dir-fmt is only supported for the west build tool.')
|
|
|
|
if generator not in self.GENERATORS:
|
|
raise self.error('Unknown generator {}; choose from: {}'.format(
|
|
generator, self.GENERATORS))
|
|
|
|
if host_os not in self.HOST_OS:
|
|
raise self.error('Unknown host-os {}; choose from: {}'.format(
|
|
host_os, self.HOST_OS))
|
|
|
|
if compact and skip_config:
|
|
raise self.error('Both compact and maybe-skip-config options were given.')
|
|
|
|
app = app or zephyr_app
|
|
in_tree = self.IN_TREE_STR if zephyr_app else None
|
|
# Allow build directories which are nested.
|
|
build_dir = ('build' + '/' + build_dir_append).rstrip('/')
|
|
|
|
# Create host_os array
|
|
host_os = [host_os] if host_os != "all" else [v for v in self.HOST_OS
|
|
if v != 'all']
|
|
# Create tools array
|
|
tools = [tool] if tool != "all" else [v for v in self.TOOLS
|
|
if v != 'all']
|
|
|
|
# Create snippet array
|
|
snippet_list = snippets.split(',') if snippets is not None else None
|
|
|
|
# Build the command content as a list, then convert to string.
|
|
content = []
|
|
tool_comment = None
|
|
if len(tools) > 1:
|
|
tool_comment = 'Using {}:'
|
|
|
|
run_config = {
|
|
'host_os': host_os,
|
|
'app': app,
|
|
'in_tree': in_tree,
|
|
'cd_into': cd_into,
|
|
'board': board,
|
|
'shield': shield,
|
|
'conf': conf,
|
|
'gen_args': gen_args,
|
|
'build_args': build_args,
|
|
'snippets': snippet_list,
|
|
'build_dir': build_dir,
|
|
'build_dir_fmt': build_dir_fmt,
|
|
'goals': goals,
|
|
'compact': compact,
|
|
'skip_config': skip_config,
|
|
'generator': generator,
|
|
'west_args': west_args,
|
|
'flash_args': flash_args,
|
|
}
|
|
|
|
if 'west' in tools:
|
|
w = self._generate_west(**run_config)
|
|
if tool_comment:
|
|
paragraph = nodes.paragraph()
|
|
paragraph += nodes.Text(tool_comment.format('west'))
|
|
content.append(paragraph)
|
|
content.append(self._lit_block(w))
|
|
else:
|
|
content.extend(w)
|
|
|
|
if 'cmake' in tools:
|
|
c = self._generate_cmake(**run_config)
|
|
if tool_comment:
|
|
paragraph = nodes.paragraph()
|
|
paragraph += nodes.Text(tool_comment.format(
|
|
'CMake and {}'.format(generator)))
|
|
content.append(paragraph)
|
|
content.append(self._lit_block(c))
|
|
else:
|
|
content.extend(c)
|
|
|
|
if not tool_comment:
|
|
content = [self._lit_block(content)]
|
|
|
|
return content
|
|
|
|
def _lit_block(self, content):
|
|
content = '\n'.join(content)
|
|
|
|
# Create the nodes.
|
|
literal = nodes.literal_block(content, content)
|
|
self.add_name(literal)
|
|
literal['language'] = 'shell'
|
|
return literal
|
|
|
|
|
|
def _generate_west(self, **kwargs):
|
|
content = []
|
|
generator = kwargs['generator']
|
|
board = kwargs['board']
|
|
app = kwargs['app']
|
|
in_tree = kwargs['in_tree']
|
|
goals = kwargs['goals']
|
|
cd_into = kwargs['cd_into']
|
|
build_dir = kwargs['build_dir']
|
|
build_dir_fmt = kwargs['build_dir_fmt']
|
|
compact = kwargs['compact']
|
|
snippets = kwargs['snippets']
|
|
west_args = kwargs['west_args']
|
|
flash_args = kwargs['flash_args']
|
|
kwargs['board'] = None
|
|
# west always defaults to ninja
|
|
gen_arg = ' -G\'Unix Makefiles\'' if generator == 'make' else ''
|
|
cmake_args = gen_arg + self._cmake_args(**kwargs)
|
|
cmake_args = ' --{}'.format(cmake_args) if cmake_args != '' else ''
|
|
west_args = ' {}'.format(west_args) if west_args else ''
|
|
flash_args = ' {}'.format(flash_args) if flash_args else ''
|
|
snippet_args = ''.join(f' -S {s}' for s in snippets) if snippets else ''
|
|
# ignore zephyr_app since west needs to run within
|
|
# the installation. Instead rely on relative path.
|
|
src = ' {}'.format(app) if app and not cd_into else ''
|
|
|
|
if build_dir_fmt is None:
|
|
dst = ' -d {}'.format(build_dir) if build_dir != 'build' else ''
|
|
build_dst = dst
|
|
else:
|
|
app_name = app.split('/')[-1]
|
|
build_dir_formatted = build_dir_fmt.format(app=app_name, board=board, source_dir=app)
|
|
dst = ' -d {}'.format(build_dir_formatted)
|
|
build_dst = ''
|
|
|
|
if in_tree and not compact:
|
|
content.append(in_tree)
|
|
|
|
if cd_into and app:
|
|
content.append('cd {}'.format(app))
|
|
|
|
# We always have to run west build.
|
|
#
|
|
# FIXME: doing this unconditionally essentially ignores the
|
|
# maybe-skip-config option if set.
|
|
#
|
|
# This whole script and its users from within the
|
|
# documentation needs to be overhauled now that we're
|
|
# defaulting to west.
|
|
#
|
|
# For now, this keeps the resulting commands working.
|
|
content.append('west build -b {}{}{}{}{}{}'.
|
|
format(board, west_args, snippet_args, build_dst, src, cmake_args))
|
|
|
|
# If we're signing, we want to do that next, so that flashing
|
|
# etc. commands can use the signed file which must be created
|
|
# in this step.
|
|
if 'sign' in goals:
|
|
content.append('west sign{}'.format(dst))
|
|
|
|
for goal in goals:
|
|
if goal in {'build', 'sign'}:
|
|
continue
|
|
elif goal == 'flash':
|
|
content.append('west flash{}{}'.format(flash_args, dst))
|
|
elif goal == 'debug':
|
|
content.append('west debug{}'.format(dst))
|
|
elif goal == 'debugserver':
|
|
content.append('west debugserver{}'.format(dst))
|
|
elif goal == 'attach':
|
|
content.append('west attach{}'.format(dst))
|
|
else:
|
|
content.append('west build -t {}{}'.format(goal, dst))
|
|
|
|
return content
|
|
|
|
@staticmethod
|
|
def _mkdir(mkdir, build_dir, host_os, skip_config):
|
|
content = []
|
|
if skip_config:
|
|
content.append("# If you already made a build directory ({}) and ran cmake, just 'cd {}' instead.".format(build_dir, build_dir)) # noqa: E501
|
|
if host_os == 'all':
|
|
content.append('mkdir {} && cd {}'.format(build_dir, build_dir))
|
|
if host_os == "unix":
|
|
content.append('{} {} && cd {}'.format(mkdir, build_dir, build_dir))
|
|
elif host_os == "win":
|
|
build_dir = build_dir.replace('/', '\\')
|
|
content.append('mkdir {} & cd {}'.format(build_dir, build_dir))
|
|
return content
|
|
|
|
@staticmethod
|
|
def _cmake_args(**kwargs):
|
|
board = kwargs['board']
|
|
shield = kwargs['shield']
|
|
conf = kwargs['conf']
|
|
gen_args = kwargs['gen_args']
|
|
board_arg = ' -DBOARD={}'.format(board) if board else ''
|
|
shield_arg = ' -DSHIELD={}'.format(shield) if shield else ''
|
|
conf_arg = ' -DCONF_FILE={}'.format(conf) if conf else ''
|
|
gen_args = ' {}'.format(gen_args) if gen_args else ''
|
|
|
|
return '{}{}{}{}'.format(board_arg, shield_arg, conf_arg, gen_args)
|
|
|
|
def _cd_into(self, mkdir, **kwargs):
|
|
app = kwargs['app']
|
|
host_os = kwargs['host_os']
|
|
compact = kwargs['compact']
|
|
build_dir = kwargs['build_dir']
|
|
skip_config = kwargs['skip_config']
|
|
content = []
|
|
os_comment = None
|
|
if len(host_os) > 1:
|
|
os_comment = '# On {}'
|
|
num_slashes = build_dir.count('/')
|
|
if not app and mkdir and num_slashes == 0:
|
|
# When there's no app and a single level deep build dir,
|
|
# simplify output
|
|
content.extend(self._mkdir(mkdir, build_dir, 'all',
|
|
skip_config))
|
|
if not compact:
|
|
content.append('')
|
|
return content
|
|
for host in host_os:
|
|
if host == "unix":
|
|
if os_comment:
|
|
content.append(os_comment.format('Linux/macOS'))
|
|
if app:
|
|
content.append('cd {}'.format(app))
|
|
elif host == "win":
|
|
if os_comment:
|
|
content.append(os_comment.format('Windows'))
|
|
if app:
|
|
backslashified = app.replace('/', '\\')
|
|
content.append('cd {}'.format(backslashified))
|
|
if mkdir:
|
|
content.extend(self._mkdir(mkdir, build_dir, host, skip_config))
|
|
if not compact:
|
|
content.append('')
|
|
return content
|
|
|
|
def _generate_cmake(self, **kwargs):
|
|
generator = kwargs['generator']
|
|
cd_into = kwargs['cd_into']
|
|
app = kwargs['app']
|
|
in_tree = kwargs['in_tree']
|
|
build_dir = kwargs['build_dir']
|
|
build_args = kwargs['build_args']
|
|
snippets = kwargs['snippets']
|
|
skip_config = kwargs['skip_config']
|
|
goals = kwargs['goals']
|
|
compact = kwargs['compact']
|
|
|
|
content = []
|
|
|
|
if in_tree and not compact:
|
|
content.append(in_tree)
|
|
|
|
if cd_into:
|
|
num_slashes = build_dir.count('/')
|
|
mkdir = 'mkdir' if num_slashes == 0 else 'mkdir -p'
|
|
content.extend(self._cd_into(mkdir, **kwargs))
|
|
# Prepare cmake/ninja/make variables
|
|
source_dir = ' ' + '/'.join(['..' for i in range(num_slashes + 1)])
|
|
cmake_build_dir = ''
|
|
tool_build_dir = ''
|
|
else:
|
|
source_dir = ' {}'.format(app) if app else ' .'
|
|
cmake_build_dir = ' -B{}'.format(build_dir)
|
|
tool_build_dir = ' -C{}'.format(build_dir)
|
|
|
|
# Now generate the actual cmake and make/ninja commands
|
|
gen_arg = ' -GNinja' if generator == 'ninja' else ''
|
|
build_args = ' {}'.format(build_args) if build_args else ''
|
|
snippet_args = ' -DSNIPPET="{}"'.format(';'.join(snippets)) if snippets else ''
|
|
cmake_args = self._cmake_args(**kwargs)
|
|
|
|
if not compact:
|
|
if not cd_into and skip_config:
|
|
content.append("# If you already ran cmake with -B{}, you " \
|
|
"can skip this step and run {} directly.".
|
|
format(build_dir, generator)) # noqa: E501
|
|
else:
|
|
content.append('# Use cmake to configure a {}-based build' \
|
|
'system:'.format(generator.capitalize())) # noqa: E501
|
|
|
|
content.append('cmake{}{}{}{}{}'.format(cmake_build_dir, gen_arg,
|
|
cmake_args, snippet_args, source_dir))
|
|
if not compact:
|
|
content.extend(['',
|
|
'# Now run the build tool on the generated build system:'])
|
|
|
|
if 'build' in goals:
|
|
content.append('{}{}{}'.format(generator, tool_build_dir,
|
|
build_args))
|
|
for goal in goals:
|
|
if goal == 'build':
|
|
continue
|
|
content.append('{}{} {}'.format(generator, tool_build_dir, goal))
|
|
|
|
return content
|
|
|
|
|
|
def setup(app):
|
|
app.add_directive('zephyr-app-commands', ZephyrAppCommandsDirective)
|
|
|
|
return {
|
|
'version': '1.0',
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True
|
|
}
|