blob: d382ef1d25fa62279b0a8768c56d1f5ad7746800 [file] [edit]
# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Auto-generate the Kconfig reference in //docs/sphinx/os/zephyr/kconfig.rst"""
import re
from typing import Iterable
import docutils
from docutils.core import publish_doctree
from sphinx.application import Sphinx
from sphinx.addnodes import document
try:
import kconfiglib # type: ignore
KCONFIGLIB_AVAILABLE = True
except ImportError:
KCONFIGLIB_AVAILABLE = False
# Determine whether the docs are being built with Bazel or GN.
is_bazel_build = True
try:
# pylint: disable=unused-import
from python.runfiles import runfiles # type: ignore
# pylint: enable=unused-import
except ImportError:
is_bazel_build = False
class MissingKconfigRoot(Exception):
pass
def rst_to_doctree(rst: str) -> Iterable[docutils.nodes.Node]:
"""Convert raw reStructuredText into doctree nodes."""
# TODO: b/288127315 - Properly resolve references within the rst so that
# links are generated more robustly.
while ':ref:`module-' in rst:
rst = re.sub(
r':ref:`module-(.*?)`', r'`\1 <https://pigweed.dev/\1>`_', rst
)
doctree = publish_doctree(rst)
return doctree.children
def create_source_paragraph(name_and_loc: str) -> Iterable[docutils.nodes.Node]:
"""Convert kconfiglib's name and location string into a source code link."""
if not is_bazel_build:
name_and_loc = name_and_loc.replace('pw_docgen_tree/', '')
start = name_and_loc.index('pw_')
end = name_and_loc.index(':')
file_path = name_and_loc[start:end]
url = f'https://cs.opensource.google/pigweed/pigweed/+/main:{file_path}'
link = f'`//{file_path} <{url}>`_'
return rst_to_doctree(f'Source: {link}')
def process_node(
node: kconfiglib.MenuNode, parent: docutils.nodes.Node
) -> None:
"""Recursively generate documentation for the Kconfig nodes."""
while node:
if node.item == kconfiglib.MENU:
name = node.prompt[0]
# All auto-generated sections must have an ID or else the
# get_secnumber() function in Sphinx's HTML5 writer throws an
# IndexError.
menu_section = docutils.nodes.section(ids=[name])
menu_section += docutils.nodes.title(text=f'{name} options')
if node.list:
process_node(node.list, menu_section)
parent += menu_section
elif isinstance(node.item, kconfiglib.Symbol):
name = f'CONFIG_{node.item.name}'
symbol_section = docutils.nodes.section(ids=[name])
symbol_section += docutils.nodes.title(text=name)
symbol_section += docutils.nodes.paragraph(
text=f'Type: {kconfiglib.TYPE_TO_STR[node.item.type]}'
)
if node.item.defaults:
try:
default_value = node.item.defaults[0][0].str_value
symbol_section += docutils.nodes.paragraph(
text=f'Default value: {default_value}'
)
# If the data wasn't found, just contine trying to process
# rest of the documentation for the node.
except IndexError:
pass
if node.item.ranges:
try:
low = node.item.ranges[0][0].str_value
high = node.item.ranges[0][1].str_value
symbol_section += docutils.nodes.paragraph(
text=f'Range of valid values: {low} to {high}'
)
except IndexError:
pass
if node.prompt:
try:
symbol_section += docutils.nodes.paragraph(
text=f'Description: {node.prompt[0]}'
)
except IndexError:
pass
if node.help:
symbol_section += rst_to_doctree(node.help)
if node.list:
process_node(node.list, symbol_section)
symbol_section += create_source_paragraph(node.item.name_and_loc)
parent += symbol_section
# TODO: b/288127315 - Render choices?
# elif isinstance(node.item, kconfiglib.Choice):
node = node.next
def generate_kconfig_reference(
app: Sphinx, doctree: document, docname: str
) -> None:
"""Parse the Kconfig and kick off the doc generation process."""
# Only run this logic on one specific page.
if 'docs/os/zephyr/kconfig' not in docname:
return
root = None
# Get the last `section` node in the doc. This is where we'll append the
# auto-generated content.
for child in doctree.children:
if isinstance(child, docutils.nodes.section):
root = child
if not root:
raise MissingKconfigRoot
file_path = f'{app.confdir}/Kconfig.zephyr'
kconfig = kconfiglib.Kconfig(file_path)
# There's no need to process kconfig.top_node (the main menu) or
# kconfig.top_node.list (ZEPHYR_PIGWEED_MODULE) because they don't
# contain data that should be documented.
first_data_node = kconfig.top_node.list.next
process_node(first_data_node, root)
def setup(app: Sphinx) -> dict[str, bool]:
"""Initialize the Sphinx extension."""
if KCONFIGLIB_AVAILABLE:
app.connect('doctree-resolved', generate_kconfig_reference)
return {'parallel_read_safe': True, 'parallel_write_safe': True}