From 24fd126ada05966fdb4a6100a0d6cca453317ad5 Mon Sep 17 00:00:00 2001 From: Jack Rosenthal Date: Tue, 11 Jan 2022 21:37:15 -0700 Subject: zephyr: zmake: Add a README.md file generated from command structure Generate a readme file from the command structure and arguments. This file gets checked that it matches the generated contents in the commit queue using a new command "zmake generate-readme --diff". Right now this file only has the commands, but could possibly be extended in the future, for example, to document `BUILD.py` functions. BUG=b:180609783 BRANCH=none TEST=View readme in gitiles TEST=Added unit tests Signed-off-by: Jack Rosenthal Change-Id: Ie8ed39b30ce2a58c91b2cf8e48b0426fb9b4f2e5 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/ec/+/3382480 Reviewed-by: Jeremy Bettis --- zephyr/zmake/README.md | 144 +++++++++++++++++++++++++++++ zephyr/zmake/run_tests.sh | 3 + zephyr/zmake/tests/test_generate_readme.py | 55 +++++++++++ zephyr/zmake/zmake/__main__.py | 25 ++++- zephyr/zmake/zmake/generate_readme.py | 123 ++++++++++++++++++++++++ zephyr/zmake/zmake/zmake.py | 37 ++++++++ 6 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 zephyr/zmake/README.md create mode 100644 zephyr/zmake/tests/test_generate_readme.py create mode 100644 zephyr/zmake/zmake/generate_readme.py diff --git a/zephyr/zmake/README.md b/zephyr/zmake/README.md new file mode 100644 index 0000000000..986cc8a506 --- /dev/null +++ b/zephyr/zmake/README.md @@ -0,0 +1,144 @@ +# Zmake + + + +[TOC] + +## Usage + +**Usage:** `zmake [-h] [--checkout CHECKOUT] [-D] [-j JOBS] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-L] [--log-label] [--modules-dir MODULES_DIR] [--zephyr-base ZEPHYR_BASE] subcommand ...` + +Chromium OS's meta-build tool for Zephyr + +#### Positional Arguments + +| | | +|---|---| +| `subcommand` | Subcommand to run | + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | +| `--checkout CHECKOUT` | Path to ChromiumOS checkout | +| `-D`, `--debug` | Turn on debug features (e.g., stack trace, verbose logging) | +| `-j JOBS`, `--jobs JOBS` | Degree of multiprogramming to use | +| `-l LOG_LEVEL`, `--log-level LOG_LEVEL` | Set the logging level (default=INFO) | +| `-L`, `--no-log-label` | Turn off logging labels | +| `--log-label` | Turn on logging labels | +| `--modules-dir MODULES_DIR` | The path to a directory containing all modules needed. If unspecified, zmake will assume you have a Chrome OS checkout and try locating them in the checkout. | +| `--zephyr-base ZEPHYR_BASE` | Path to Zephyr OS repository | + +## Subcommands + +### zmake configure + +**Usage:** `zmake configure [-h] [-t TOOLCHAIN] [--bringup] [--allow-warnings] [-B BUILD_DIR] [-b] [--test] project_name_or_dir [-c]` + +#### Positional Arguments + +| | | +|---|---| +| `project_name_or_dir` | Path to the project to build | + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | +| `-t TOOLCHAIN`, `--toolchain TOOLCHAIN` | Name of toolchain to use | +| `--bringup` | Enable bringup debugging features | +| `--allow-warnings` | Do not treat warnings as errors | +| `-B BUILD_DIR`, `--build-dir BUILD_DIR` | Build directory | +| `-b`, `--build` | Run the build after configuration | +| `--test` | Test the .elf file after configuration | +| `-c`, `--coverage` | Enable CONFIG_COVERAGE Kconfig. | + +### zmake build + +**Usage:** `zmake build [-h] build_dir [-w]` + +#### Positional Arguments + +| | | +|---|---| +| `build_dir` | The build directory used during configuration | + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | +| `-w`, `--fail-on-warnings` | Exit with code 2 if warnings are detected | + +### zmake list-projects + +**Usage:** `zmake list-projects [-h] [--format FORMAT] [search_dir]` + +#### Positional Arguments + +| | | +|---|---| +| `search_dir` | Optional directory to search for BUILD.py files in. | + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | +| `--format FORMAT` | Output format to print projects (str.format(config=project.config) is called on this for each project). | + +### zmake test + +**Usage:** `zmake test [-h] build_dir` + +#### Positional Arguments + +| | | +|---|---| +| `build_dir` | The build directory used during configuration | + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | + +### zmake testall + +**Usage:** `zmake testall [-h]` + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | + +### zmake coverage + +**Usage:** `zmake coverage [-h] build_dir` + +#### Positional Arguments + +| | | +|---|---| +| `build_dir` | The build directory used during configuration | + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | + +### zmake generate-readme + +**Usage:** `zmake generate-readme [-h] [-o OUTPUT_FILE] [--diff]` + +#### Optional Arguments + +| | | +|---|---| +| `-h`, `--help` | show this help message and exit | +| `-o OUTPUT_FILE`, `--output-file OUTPUT_FILE` | File to write to. It will only be written if changed. | +| `--diff` | If specified, diff the README with the expected contents instead of writing out. | diff --git a/zephyr/zmake/run_tests.sh b/zephyr/zmake/run_tests.sh index 60e93cdf1a..4796704440 100755 --- a/zephyr/zmake/run_tests.sh +++ b/zephyr/zmake/run_tests.sh @@ -33,3 +33,6 @@ black --check --diff . # Check flake8 reports no issues. flake8 . + +# Check auto-generated README.md is as expected. +python -m zmake generate-readme --diff diff --git a/zephyr/zmake/tests/test_generate_readme.py b/zephyr/zmake/tests/test_generate_readme.py new file mode 100644 index 0000000000..2a05de9cce --- /dev/null +++ b/zephyr/zmake/tests/test_generate_readme.py @@ -0,0 +1,55 @@ +# Copyright 2022 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import pytest + +import zmake.generate_readme as gen_readme +import zmake.zmake as zm + + +def test_generate_readme_contents(): + readme = gen_readme.generate_readme() + + # Look for a string we know should appear in the README. + assert "### zmake testall\n" in readme + + +@pytest.mark.parametrize( + ["expected_contents", "actual_contents", "return_code"], + [ + ("abc\ndef\nghi\n", "abc\nghi\n", 1), + ("abc\ndef\nghi\n", "abc\ndef\nghi\n", 0), + ("abc\ndef\nghi\n", None, 1), + ], +) +def test_generate_readme_diff( + monkeypatch, tmp_path, expected_contents, actual_contents, return_code +): + def generate_readme(): + return expected_contents + + monkeypatch.setattr(gen_readme, "generate_readme", generate_readme) + + readme_file = tmp_path / "README.md" + if actual_contents is not None: + readme_file.write_text(actual_contents) + + zmk = zm.Zmake() + assert zmk.generate_readme(readme_file, diff=True) == return_code + + +@pytest.mark.parametrize("exist", [False, True]) +def test_generate_readme_file(monkeypatch, tmp_path, exist): + def generate_readme(): + return "hello\n" + + monkeypatch.setattr(gen_readme, "generate_readme", generate_readme) + + readme_file = tmp_path / "README.md" + if exist: + readme_file.write_text("some existing contents\n") + + zmk = zm.Zmake() + assert zmk.generate_readme(readme_file) == 0 + assert readme_file.read_text() == "hello\n" diff --git a/zephyr/zmake/zmake/__main__.py b/zephyr/zmake/zmake/__main__.py index 136831a319..28ec232148 100644 --- a/zephyr/zmake/zmake/__main__.py +++ b/zephyr/zmake/zmake/__main__.py @@ -97,7 +97,7 @@ def get_argparser(): """Get the argument parser. Returns: - An argparse.ArgumentParser. + A two tuple, the argument parser, and the subcommand action. """ parser = argparse.ArgumentParser( prog="zmake", @@ -269,7 +269,26 @@ def get_argparser(): help="The build directory used during configuration", ) - return parser + generate_readme = sub.add_parser( + "generate-readme", + help="Update the auto-generated markdown documentation", + ) + generate_readme.add_argument( + "-o", + "--output-file", + default=pathlib.Path(__file__).parent.parent / "README.md", + help="File to write to. It will only be written if changed.", + ) + generate_readme.add_argument( + "--diff", + action="store_true", + help=( + "If specified, diff the README with the expected contents instead of " + "writing out." + ), + ) + + return parser, sub def main(argv=None): @@ -286,7 +305,7 @@ def main(argv=None): maybe_reexec(argv) - parser = get_argparser() + parser, _ = get_argparser() opts = parser.parse_args(argv) # Default logging diff --git a/zephyr/zmake/zmake/generate_readme.py b/zephyr/zmake/zmake/generate_readme.py new file mode 100644 index 0000000000..d21fd19f7c --- /dev/null +++ b/zephyr/zmake/zmake/generate_readme.py @@ -0,0 +1,123 @@ +# Copyright 2022 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Code for auto-generating README.md.""" + +import argparse +import io + + +class MarkdownHelpFormatter(argparse.HelpFormatter): + """Callbacks to format help output as Markdown.""" + + def __init__(self, prog): + self._prog = prog + self._section_title = None + self._section_contents = [] + self._paragraphs = [] + super().__init__(prog=prog) + + def add_text(self, text): + if text and text is not argparse.SUPPRESS: + lst = self._paragraphs + if self._section_title: + lst = self._section_contents + lst.append(text) + + def start_section(self, title): + self._section_title = title.title() + self._section_contents = [] + + def end_section(self): + if self._section_contents: + self._paragraphs.append(f"#### {self._section_title}") + self._paragraphs.extend(self._section_contents) + self._section_title = None + + def add_usage(self, usage, actions, groups): + if not usage: + usage = self._prog + self.add_text( + f"**Usage:** `{usage} {self._format_actions_usage(actions, groups)}`" + ) + + def add_arguments(self, actions): + def _get_metavar(action): + return action.metavar or action.dest + + def _format_invocation(action): + if action.option_strings: + parts = [] + for option_string in action.option_strings: + if action.nargs == 0: + parts.append(option_string) + else: + parts.append(f"{option_string} {_get_metavar(action).upper()}") + return ", ".join(f"`{part}`" for part in parts) + else: + return f"`{_get_metavar(action)}`" + + def _get_table_line(action): + return f"| {_format_invocation(action)} | {action.help} |" + + table_lines = [ + "| | |", + "|---|---|", + *( + _get_table_line(action) + for action in actions + if action.help is not argparse.SUPPRESS + ), + ] + + # Don't want a table with no rows. + if len(table_lines) > 2: + self.add_text("\n".join(table_lines)) + + def format_help(self): + return "\n\n".join(self._paragraphs) + + +def generate_readme(): + """Generate the README.md file. + + Returns: + A string with the README contents. + """ + # Deferred import position to avoid circular dependency. + # Normally, this would not be required, since we don't use from + # imports. But runpy's import machinery essentially does the + # equivalent of a from import on __main__.py. + import zmake.__main__ + + output = io.StringIO() + parser, sub_action = zmake.__main__.get_argparser() + + def _append(*args, **kwargs): + kwargs.setdefault("file", output) + print(*args, **kwargs) + + def _append_argparse_help(parser): + parser.formatter_class = MarkdownHelpFormatter + _append(parser.format_help()) + + _append("# Zmake") + _append() + _append('') + _append() + _append("[TOC]") + _append() + _append("## Usage") + _append() + _append_argparse_help(parser) + _append() + _append("## Subcommands") + + for sub_name, sub_parser in sub_action.choices.items(): + _append() + _append(f"### zmake {sub_name}") + _append() + _append_argparse_help(sub_parser) + + return output.getvalue() diff --git a/zephyr/zmake/zmake/zmake.py b/zephyr/zmake/zmake/zmake.py index a906181c47..abc41de855 100644 --- a/zephyr/zmake/zmake/zmake.py +++ b/zephyr/zmake/zmake/zmake.py @@ -3,6 +3,7 @@ # found in the LICENSE file. """Module encapsulating Zmake wrapper object.""" +import difflib import logging import os import pathlib @@ -12,6 +13,7 @@ import subprocess import tempfile import zmake.build_config +import zmake.generate_readme import zmake.jobserver import zmake.modules import zmake.multiproc @@ -828,3 +830,38 @@ class Zmake: print(format.format(config=project.config), end="") return 0 + + def generate_readme(self, output_file, diff=False): + """Re-generate the auto-generated README file. + + Args: + output_file: A pathlib.Path; to be written only if changed. + diff: Instead of writing out, report the diff. + """ + expected_contents = zmake.generate_readme.generate_readme() + + if output_file.is_file(): + current_contents = output_file.read_text() + if expected_contents == current_contents: + return 0 + if diff: + self.logger.error( + "The auto-generated README.md differs from the expected contents:" + ) + for line in difflib.unified_diff( + current_contents.splitlines(keepends=True), + expected_contents.splitlines(keepends=True), + str(output_file), + ): + self.logger.error(line.rstrip()) + self.logger.error('Run "zmake generate-readme" to fix this.') + return 1 + + if diff: + self.logger.error( + 'The README.md file does not exist. Run "zmake generate-readme".' + ) + return 1 + + output_file.write_text(expected_contents) + return 0 -- cgit v1.2.1