xref: /aosp_15_r20/external/pigweed/pw_docgen/py/pw_docgen/docgen.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2019 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Renders HTML documentation using Sphinx."""
15
16# TODO(frolv): Figure out a solution for installing all library dependencies
17# to run Sphinx and build RTD docs.
18
19import argparse
20import collections
21import json
22import os
23import shutil
24import subprocess
25import sys
26
27from pathlib import Path
28
29SCRIPT_HEADER: str = '''
30██████╗ ██╗ ██████╗ ██╗    ██╗███████╗███████╗██████╗     ██████╗  ██████╗  ██████╗███████╗
31██╔══██╗██║██╔════╝ ██║    ██║██╔════╝██╔════╝██╔══██╗    ██╔══██╗██╔═══██╗██╔════╝██╔════╝
32██████╔╝██║██║  ███╗██║ █╗ ██║█████╗  █████╗  ██║  ██║    ██║  ██║██║   ██║██║     ███████╗
33██╔═══╝ ██║██║   ██║██║███╗██║██╔══╝  ██╔══╝  ██║  ██║    ██║  ██║██║   ██║██║     ╚════██║
34██║     ██║╚██████╔╝╚███╔███╔╝███████╗███████╗██████╔╝    ██████╔╝╚██████╔╝╚██████╗███████║
35╚═╝     ╚═╝ ╚═════╝  ╚══╝╚══╝ ╚══════╝╚══════╝╚═════╝     ╚═════╝  ╚═════╝  ╚═════╝╚══════╝
36'''
37
38
39def parse_args() -> argparse.Namespace:
40    """Parses command-line arguments."""
41
42    parser = argparse.ArgumentParser(description=__doc__)
43    parser.add_argument(
44        '--sphinx-build-dir',
45        required=True,
46        help='Directory in which to build docs',
47    )
48    parser.add_argument(
49        '--conf', required=True, help='Path to conf.py file for Sphinx'
50    )
51    parser.add_argument(
52        '-j',
53        '--parallel',
54        type=int,
55        default=os.cpu_count(),
56        help='Number of parallel processes to run',
57    )
58    parser.add_argument(
59        '--gn-root', required=True, help='Root of the GN build tree'
60    )
61    parser.add_argument(
62        '--gn-gen-root', required=True, help='Root of the GN gen tree'
63    )
64    parser.add_argument(
65        'sources', nargs='+', help='Paths to the root level rst source files'
66    )
67    parser.add_argument(
68        '--out-dir',
69        required=True,
70        help='Output directory for rendered HTML docs',
71    )
72    parser.add_argument(
73        '--metadata',
74        required=True,
75        type=argparse.FileType('r'),
76        help='Metadata JSON file',
77    )
78    parser.add_argument(
79        '--google-analytics-id',
80        const=None,
81        help='Enables Google Analytics with the provided ID',
82    )
83    return parser.parse_args()
84
85
86def build_docs(
87    src_dir: str,
88    dst_dir: str,
89    parallel: int,
90    google_analytics_id: str | None = None,
91) -> int:
92    """Runs Sphinx to render HTML documentation from a doc tree."""
93
94    # TODO(frolv): Specify the Sphinx script from a prebuilts path instead of
95    # requiring it in the tree.
96    command = [
97        'sphinx-build',
98        '-W',
99        '-j',
100        str(parallel),
101        '-b',
102        'html',
103        '-d',
104        f'{dst_dir}/help',
105    ]
106
107    if google_analytics_id is not None:
108        command.append(f'-Dgoogle_analytics_id={google_analytics_id}')
109
110    command.extend([src_dir, f'{dst_dir}/html'])
111
112    return subprocess.call(command)
113
114
115def copy_doc_tree(args: argparse.Namespace) -> None:
116    """Copies doc source and input files into a build tree."""
117
118    def build_path(path):
119        """Converts a source path to a filename in the build directory."""
120        if path.startswith(args.gn_root):
121            path = os.path.relpath(path, args.gn_root)
122        elif path.startswith(args.gn_gen_root):
123            path = os.path.relpath(path, args.gn_gen_root)
124
125        return os.path.join(args.sphinx_build_dir, path)
126
127    source_files = json.load(args.metadata)
128    copy_paths = [build_path(f) for f in source_files]
129
130    os.makedirs(args.sphinx_build_dir)
131    for source_path in args.sources:
132        os.link(
133            source_path, f'{args.sphinx_build_dir}/{Path(source_path).name}'
134        )
135    os.link(args.conf, f'{args.sphinx_build_dir}/conf.py')
136
137    # Map of directory path to list of source and destination file paths.
138    dirs: dict[str, list[tuple[str, str]]] = collections.defaultdict(list)
139
140    for source_file, copy_path in zip(source_files, copy_paths):
141        dirname = os.path.dirname(copy_path)
142        dirs[dirname].append((source_file, copy_path))
143
144    for directory, file_pairs in dirs.items():
145        os.makedirs(directory, exist_ok=True)
146        for src, dst in file_pairs:
147            os.link(src, dst)
148
149
150def main() -> int:
151    """Script entry point."""
152
153    args = parse_args()
154
155    # Clear out any existing docs for the target.
156    if os.path.exists(args.sphinx_build_dir):
157        shutil.rmtree(args.sphinx_build_dir)
158
159    # TODO: b/235349854 - Printing the header causes unicode problems on
160    # Windows. Disabled for now; re-enable once the root issue is fixed.
161    # print(SCRIPT_HEADER)
162    copy_doc_tree(args)
163
164    # Flush all script output before running Sphinx.
165    print('-' * 80, flush=True)
166
167    return build_docs(
168        args.sphinx_build_dir,
169        args.out_dir,
170        args.parallel,
171        args.google_analytics_id,
172    )
173
174
175if __name__ == '__main__':
176    sys.exit(main())
177