| # 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 |
| |
| \: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. |
| |
| \: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) |
| |
| ''' |
| 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, |
| 'build-dir': directives.unchanged, |
| 'goals': directives.unchanged_required, |
| 'maybe-skip-config': directives.flag, |
| 'compact': directives.flag, |
| 'west-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) |
| build_dir_append = self.options.get('build-dir', '').strip('/') |
| 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) |
| |
| 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 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'] |
| # 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, |
| 'build_dir': build_dir, |
| 'goals': goals, |
| 'compact': compact, |
| 'skip_config': skip_config, |
| 'generator': generator, |
| 'west_args': west_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'] = 'console' |
| 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'] |
| compact = kwargs['compact'] |
| west_args = kwargs['west_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 '' |
| # 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 '' |
| dst = ' -d {}'.format(build_dir) if build_dir != 'build' else '' |
| |
| 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, 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(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'] |
| 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 '' |
| 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, source_dir)) |
| if not compact: |
| content.extend(['', |
| '# Now run ninja 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 |
| } |