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