blob: 11f150fd303108bb86b488cde9c65350b6218ea7 [file] [log] [blame]
# Copyright (c) 2017 Open Source Foundries Limited.
#
# SPDX-License-Identifier: Apache-2.0
'''Sphinx extensions related to managing Zephyr applications.'''
from pathlib import Path
from docutils import nodes
from docutils.parsers.rst import Directive, directives
ZEPHYR_BASE = Path(__file__).parents[3]
# 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.
'''
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,
'debug-args': directives.unchanged,
'debugserver-args': directives.unchanged,
'attach-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)
debug_args = self.options.get('debug-args', None)
debugserver_args = self.options.get('debugserver-args', None)
attach_args = self.options.get('attach-args', None)
if tool not in self.TOOLS:
raise self.error(f'Unknown tool {tool}; choose from: {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(f'Unknown generator {generator}; choose from: {self.GENERATORS}')
if host_os not in self.HOST_OS:
raise self.error(f'Unknown host-os {host_os}; choose from: {self.HOST_OS}')
if compact and skip_config:
raise self.error('Both compact and maybe-skip-config options were given.')
# as folks might use "<...>" notation to indicate a variable portion of the path, we
# deliberately don't check for the validity of such paths.
if zephyr_app and not any([x in zephyr_app for x in ["<", ">"]]):
app_path = ZEPHYR_BASE / zephyr_app
if not app_path.is_dir():
raise self.error(
f"zephyr-app: {zephyr_app} is not a valid folder in the zephyr tree."
)
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('/')
# Prepare repeatable arguments
host_os = [host_os] if host_os != "all" else [v for v in self.HOST_OS if v != 'all']
tools = [tool] if tool != "all" else [v for v in self.TOOLS if v != 'all']
build_args_list = build_args.split(' ') if build_args is not None else None
snippet_list = snippets.split(',') if snippets is not None else None
shield_list = shield.split(',') if shield 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_list,
'conf': conf,
'gen_args': gen_args,
'build_args': build_args_list,
'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,
'debug_args': debug_args,
'debugserver_args': debugserver_args,
'attach_args': attach_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(f'CMake and {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']
shield = kwargs['shield']
snippets = kwargs['snippets']
build_args = kwargs["build_args"]
west_args = kwargs['west_args']
flash_args = kwargs['flash_args']
debug_args = kwargs['debug_args']
debugserver_args = kwargs['debugserver_args']
attach_args = kwargs['attach_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 = f' --{cmake_args}' if cmake_args != '' else ''
build_args = "".join(f" -o {b}" for b in build_args) if build_args else ""
west_args = f' {west_args}' if west_args else ''
flash_args = f' {flash_args}' if flash_args else ''
debug_args = f' {debug_args}' if debug_args else ''
debugserver_args = f' {debugserver_args}' if debugserver_args else ''
attach_args = f' {attach_args}' if attach_args else ''
snippet_args = ''.join(f' -S {s}' for s in snippets) if snippets else ''
shield_args = ''.join(f' --shield {s}' for s in shield) if shield else ''
# ignore zephyr_app since west needs to run within
# the installation. Instead rely on relative path.
src = f' {app}' if app and not cd_into else ''
if build_dir_fmt is None:
dst = f' -d {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 = f' -d {build_dir_formatted}'
build_dst = ''
if in_tree and not compact:
content.append(in_tree)
if cd_into and app:
content.append(f'cd {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(
f"west build -b {board}{build_args}{west_args}{snippet_args}"
f"{shield_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(f'west sign{dst}')
for goal in goals:
if goal in {'build', 'sign'}:
continue
elif goal == 'flash':
content.append(f'west flash{flash_args}{dst}')
elif goal == 'debug':
content.append(f'west debug{debug_args}{dst}')
elif goal == 'debugserver':
content.append(f'west debugserver{debugserver_args}{dst}')
elif goal == 'attach':
content.append(f'west attach{attach_args}{dst}')
else:
content.append(f'west build -t {goal}{dst}')
return content
@staticmethod
def _mkdir(mkdir, build_dir, host_os, skip_config):
content = []
if skip_config:
content.append(
f"# If you already made a build directory ({build_dir}) and ran cmake, "
f"just 'cd {build_dir}' instead."
)
if host_os == 'all':
content.append(f'mkdir {build_dir} && cd {build_dir}')
if host_os == "unix":
content.append(f'{mkdir} {build_dir} && cd {build_dir}')
elif host_os == "win":
build_dir = build_dir.replace('/', '\\')
content.append(f'mkdir {build_dir} & cd {build_dir}')
return content
@staticmethod
def _cmake_args(**kwargs):
board = kwargs['board']
conf = kwargs['conf']
gen_args = kwargs['gen_args']
board_arg = f' -DBOARD={board}' if board else ''
conf_arg = f' -DCONF_FILE={conf}' if conf else ''
gen_args = f' {gen_args}' if gen_args else ''
return f'{board_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(f'cd {app}')
elif host == "win":
if os_comment:
content.append(os_comment.format('Windows'))
if app:
backslashified = app.replace('/', '\\')
content.append(f'cd {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']
shield = kwargs['shield']
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 = f' {app}' if app else ' .'
cmake_build_dir = f' -B{build_dir}'
tool_build_dir = f' -C{build_dir}'
# Now generate the actual cmake and make/ninja commands
gen_arg = ' -GNinja' if generator == 'ninja' else ''
build_args = f' {build_args}' if build_args else ''
snippet_args = ' -DSNIPPET="{}"'.format(';'.join(snippets)) if snippets else ''
shield_args = ' -DSHIELD="{}"'.format(';'.join(shield)) if shield else ''
cmake_args = self._cmake_args(**kwargs)
if not compact:
if not cd_into and skip_config:
content.append(
f'# If you already ran cmake with -B{build_dir}, you '
f'can skip this step and run {generator} directly.'
)
else:
content.append(
f'# Use cmake to configure a {generator.capitalize()}-based buildsystem:'
)
content.append(
f'cmake{cmake_build_dir}{gen_arg}{cmake_args}{snippet_args}{shield_args}{source_dir}'
)
if not compact:
content.extend(['', '# Now run the build tool on the generated build system:'])
if 'build' in goals:
content.append(f'{generator}{tool_build_dir}{build_args}')
for goal in goals:
if goal == 'build':
continue
content.append(f'{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}