1# Copyright 2023 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"""Auto-generate the Kconfig reference in //docs/os/zephyr/kconfig.rst""" 15 16 17import os 18import re 19from typing import Iterable 20 21import docutils 22from docutils.core import publish_doctree 23from sphinx.application import Sphinx 24from sphinx.addnodes import document 25 26 27try: 28 import kconfiglib # type: ignore 29 30 KCONFIGLIB_AVAILABLE = True 31except ImportError: 32 KCONFIGLIB_AVAILABLE = False 33 34 35def rst_to_doctree(rst: str) -> Iterable[docutils.nodes.Node]: 36 """Convert raw reStructuredText into doctree nodes.""" 37 # TODO: b/288127315 - Properly resolve references within the rst so that 38 # links are generated more robustly. 39 while ':ref:`module-' in rst: 40 rst = re.sub( 41 r':ref:`module-(.*?)`', r'`\1 <https://pigweed.dev/\1>`_', rst 42 ) 43 doctree = publish_doctree(rst) 44 return doctree.children 45 46 47def create_source_paragraph(name_and_loc: str) -> Iterable[docutils.nodes.Node]: 48 """Convert kconfiglib's name and location string into a source code link.""" 49 start = name_and_loc.index('pw_') 50 end = name_and_loc.index(':') 51 file_path = name_and_loc[start:end] 52 url = f'https://cs.opensource.google/pigweed/pigweed/+/main:{file_path}' 53 link = f'`//{file_path} <{url}>`_' 54 return rst_to_doctree(f'Source: {link}') 55 56 57def process_node( 58 node: kconfiglib.MenuNode, parent: docutils.nodes.Node 59) -> None: 60 """Recursively generate documentation for the Kconfig nodes.""" 61 while node: 62 if node.item == kconfiglib.MENU: 63 name = node.prompt[0] 64 # All auto-generated sections must have an ID or else the 65 # get_secnumber() function in Sphinx's HTML5 writer throws an 66 # IndexError. 67 menu_section = docutils.nodes.section(ids=[name]) 68 menu_section += docutils.nodes.title(text=f'{name} options') 69 if node.list: 70 process_node(node.list, menu_section) 71 parent += menu_section 72 elif isinstance(node.item, kconfiglib.Symbol): 73 name = f'CONFIG_{node.item.name}' 74 symbol_section = docutils.nodes.section(ids=[name]) 75 symbol_section += docutils.nodes.title(text=name) 76 symbol_section += docutils.nodes.paragraph( 77 text=f'Type: {kconfiglib.TYPE_TO_STR[node.item.type]}' 78 ) 79 if node.item.defaults: 80 try: 81 default_value = node.item.defaults[0][0].str_value 82 symbol_section += docutils.nodes.paragraph( 83 text=f'Default value: {default_value}' 84 ) 85 # If the data wasn't found, just contine trying to process 86 # rest of the documentation for the node. 87 except IndexError: 88 pass 89 if node.item.ranges: 90 try: 91 low = node.item.ranges[0][0].str_value 92 high = node.item.ranges[0][1].str_value 93 symbol_section += docutils.nodes.paragraph( 94 text=f'Range of valid values: {low} to {high}' 95 ) 96 except IndexError: 97 pass 98 if node.prompt: 99 try: 100 symbol_section += docutils.nodes.paragraph( 101 text=f'Description: {node.prompt[0]}' 102 ) 103 except IndexError: 104 pass 105 if node.help: 106 symbol_section += rst_to_doctree(node.help) 107 if node.list: 108 process_node(node.list, symbol_section) 109 symbol_section += create_source_paragraph(node.item.name_and_loc) 110 parent += symbol_section 111 # TODO: b/288127315 - Render choices? 112 # elif isinstance(node.item, kconfiglib.Choice): 113 node = node.next 114 115 116def generate_kconfig_reference(_, doctree: document, docname: str) -> None: 117 """Parse the Kconfig and kick off the doc generation process.""" 118 if 'docs/os/zephyr/kconfig' not in docname: 119 return 120 # Assume that the new content should be appended to the last section 121 # in the doctree. 122 for child in doctree.children: 123 if isinstance(child, docutils.nodes.section): 124 root = child 125 pw_root = os.environ['PW_ROOT'] 126 file_path = f'{pw_root}/Kconfig.zephyr' 127 kconfig = kconfiglib.Kconfig(file_path) 128 # There's no need to render kconfig.top_node (the main menu) or 129 # kconfig.top_node.list (ZEPHYR_PIGWEED_MODULE). 130 process_node(kconfig.top_node.list.next, root) 131 132 133def setup(app: Sphinx) -> dict[str, bool]: 134 """Initialize the Sphinx extension.""" 135 if KCONFIGLIB_AVAILABLE: 136 app.connect('doctree-resolved', generate_kconfig_reference) 137 return {'parallel_read_safe': True, 'parallel_write_safe': True} 138