• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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