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