esp-idf/tools/kconfig_new/gen_kconfig_doc.py
2022-08-12 15:36:11 +02:00

392 lines
17 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# gen_kconfig_doc - confgen.py support for generating ReST markup documentation
#
# For each option in the loaded Kconfig (e.g. 'FOO'), CONFIG_FOO link target is
# generated, allowing options to be referenced in other documents
# (using :ref:`CONFIG_FOO`)
#
# SPDX-FileCopyrightText: 2017-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from __future__ import print_function
import re
import kconfiglib
# Indentation to be used in the generated file
INDENT = ' '
# Characters used when underlining section heading
HEADING_SYMBOLS = '#*=-^"+'
# Keep the heading level in sync with api-reference/kconfig.rst
INITIAL_HEADING_LEVEL = 3
MAX_HEADING_LEVEL = len(HEADING_SYMBOLS) - 1
class ConfigTargetVisibility(object):
"""
Determine the visibility of Kconfig options based on IDF targets. Note that other environment variables should not
imply invisibility and neither dependencies on visible options with default disabled state. This difference makes
it necessary to implement our own visibility and cannot use the visibility defined inside Kconfiglib.
"""
def __init__(self, config, target):
# target actually is not necessary here because kconfiglib.expr_value() will evaluate it internally
self.config = config
self.visibility = dict() # node name to (x, y) mapping where x is the visibility (True/False) and y is the
# name of the config which implies the visibility
self.target_env_var = 'IDF_TARGET'
self.direct_eval_set = frozenset([kconfiglib.EQUAL, kconfiglib.UNEQUAL, kconfiglib.LESS, kconfiglib.LESS_EQUAL,
kconfiglib.GREATER, kconfiglib.GREATER_EQUAL])
def _implies_invisibility(self, item):
if isinstance(item, tuple):
if item[0] == kconfiglib.NOT:
(invisibility, source) = self._implies_invisibility(item[1])
if source is not None and source.startswith(self.target_env_var):
return (not invisibility, source)
else:
# we want to be visible all configs which are not dependent on target variables,
# e.g. "depends on XY" and "depends on !XY" as well
return (False, None)
elif item[0] == kconfiglib.AND:
(invisibility, source) = self._implies_invisibility(item[1])
if invisibility:
return (True, source)
(invisibility, source) = self._implies_invisibility(item[2])
if invisibility:
return (True, source)
return (False, None)
elif item[0] == kconfiglib.OR:
implication_list = [self._implies_invisibility(item[1]), self._implies_invisibility(item[2])]
if all([implies for (implies, _) in implication_list]):
source_list = [s for (_, s) in implication_list if s.startswith(self.target_env_var)]
# if source_list has more items then it should not matter which will imply the invisibility
return (True, source_list[0])
return (False, None)
elif item[0] in self.direct_eval_set:
def node_is_invisible(item):
return all([node.prompt is None for node in item.nodes])
if node_is_invisible(item[1]) or node_is_invisible(item[1]):
# it makes no sense to call self._implies_invisibility() here because it won't generate any useful
# "source"
return (not kconfiglib.expr_value(item), None)
else:
# expressions with visible configs can be changed to make the item visible
return (False, None)
else:
raise RuntimeError('Unimplemented operation in {}'.format(item))
else: # Symbol or Choice
vis_list = [self._visible(node) for node in item.nodes]
if len(vis_list) > 0 and all([not visible for (visible, _) in vis_list]):
source_list = [s for (_, s) in vis_list if s is not None and s.startswith(self.target_env_var)]
# if source_list has more items then it should not matter which will imply the invisibility
return (True, source_list[0])
if item.name.startswith(self.target_env_var):
return (not kconfiglib.expr_value(item), item.name)
if len(vis_list) == 1:
(visible, source) = vis_list[0]
if visible:
return (False, item.name) # item.name is important here in case the result will be inverted: if
# the dependency is on another config then it can be still visible
return (False, None)
def _visible(self, node):
if node.item == kconfiglib.COMMENT:
return (False, None)
if isinstance(node.item, kconfiglib.Symbol) or isinstance(node.item, kconfiglib.Choice):
dependencies = node.item.direct_dep # "depends on" for configs
name_id = node.item.name
simple_def = len(node.item.nodes) <= 1 # defined only in one source file
# Probably it is not necessary to check the default statements.
else:
dependencies = node.visibility # "visible if" for menu
name_id = node.prompt[0]
simple_def = False # menus can be defined with the same name at multiple locations and they don't know
# about each other like configs through node.item.nodes. Therefore, they cannot be stored and have to be
# re-evaluated always.
try:
(visib, source) = self.visibility[name_id]
except KeyError:
def invert_first_arg(_tuple):
return (not _tuple[0], _tuple[1])
(visib, source) = self._visible(node.parent) if node.parent else (True, None)
if visib:
(visib, source) = invert_first_arg(self._implies_invisibility(dependencies))
if simple_def:
# Configs defined at multiple places are not stored because they could have different visibility based
# on different targets. kconfiglib.expr_value() will handle the visibility.
self.visibility[name_id] = (visib, source)
return (visib, source) # not used in "finally" block because failure messages from _implies_invisibility are
# this way more understandable
def visible(self, node):
if not node.prompt:
# don't store this in self.visibility because don't want to stop at invisible nodes when recursively
# searching for invisible targets
return False
return self._visible(node)[0]
def write_docs(config, visibility, filename):
""" Note: writing .rst documentation ignores the current value
of any items. ie the --config option can be ignored.
(However at time of writing it still needs to be set to something...) """
with open(filename, 'w') as f:
for node in config.node_iter():
write_menu_item(f, node, visibility)
def node_is_menu(node):
try:
return node.item in [kconfiglib.MENU, kconfiglib.COMMENT] or node.is_menuconfig
except AttributeError:
return False # not all MenuNodes have is_menuconfig for some reason
def get_breadcrumbs(node):
# this is a bit wasteful as it recalculates each time, but still...
result = []
node = node.parent
while node.parent:
if node.prompt:
result = [':ref:`%s`' % get_link_anchor(node)] + result
node = node.parent
return ' > '.join(result)
def get_link_anchor(node):
try:
return 'CONFIG_%s' % node.item.name
except AttributeError:
assert node_is_menu(node) # only menus should have no item.name
# for menus, build a link anchor out of the parents
result = []
while node.parent:
if node.prompt:
result = [re.sub(r'[^a-zA-z0-9]+', '-', node.prompt[0])] + result
node = node.parent
result = '-'.join(result).lower()
return result
def get_heading_level(node):
result = INITIAL_HEADING_LEVEL
node = node.parent
while node.parent:
result += 1
if result == MAX_HEADING_LEVEL:
return MAX_HEADING_LEVEL
node = node.parent
return result
def format_rest_text(text, indent):
# Format an indented text block for use with ReST
text = indent + text.replace('\n', '\n' + indent)
# Escape some characters which are inline formatting in ReST
text = text.replace('*', '\\*')
text = text.replace('_', '\\_')
# replace absolute links to documentation by relative ones
text = re.sub(r'https://docs.espressif.com/projects/esp-idf/\w+/\w+/(.+)\.html', r':doc:`../\1`', text)
text += '\n'
return text
def _minimize_expr(expr, visibility):
def expr_nodes_invisible(e):
return hasattr(e, 'nodes') and len(e.nodes) > 0 and all(not visibility.visible(i) for i in e.nodes)
if isinstance(expr, tuple):
if expr[0] == kconfiglib.NOT:
new_expr = _minimize_expr(expr[1], visibility)
return kconfiglib.Kconfig.y if new_expr == kconfiglib.Kconfig.n else new_expr
else:
new_expr1 = _minimize_expr(expr[1], visibility)
new_expr2 = _minimize_expr(expr[2], visibility)
if expr[0] == kconfiglib.AND:
if new_expr1 == kconfiglib.Kconfig.n or new_expr2 == kconfiglib.Kconfig.n:
return kconfiglib.Kconfig.n
if new_expr1 == kconfiglib.Kconfig.y:
return new_expr2
if new_expr2 == kconfiglib.Kconfig.y:
return new_expr1
elif expr[0] == kconfiglib.OR:
if new_expr1 == kconfiglib.Kconfig.y or new_expr2 == kconfiglib.Kconfig.y:
return kconfiglib.Kconfig.y
if new_expr1 == kconfiglib.Kconfig.n:
return new_expr2
if new_expr2 == kconfiglib.Kconfig.n:
return new_expr1
elif expr[0] == kconfiglib.EQUAL:
if not isinstance(new_expr1, type(new_expr2)):
return kconfiglib.Kconfig.n
if new_expr1 == new_expr2:
return kconfiglib.Kconfig.y
elif expr[0] == kconfiglib.UNEQUAL:
if not isinstance(new_expr1, type(new_expr2)):
return kconfiglib.Kconfig.y
if new_expr1 != new_expr2:
return kconfiglib.Kconfig.n
else: # <, <=, >, >=
if not isinstance(new_expr1, type(new_expr2)):
return kconfiglib.Kconfig.n # e.g "True < 2"
if expr_nodes_invisible(new_expr1) or expr_nodes_invisible(new_expr2):
return kconfiglib.Kconfig.y if kconfiglib.expr_value(expr) else kconfiglib.Kconfig.n
return (expr[0], new_expr1, new_expr2)
if (not kconfiglib.expr_value(expr) and len(expr.config_string) == 0 and expr_nodes_invisible(expr)):
# nodes which are invisible
# len(expr.nodes) > 0 avoids constant symbols without actual node definitions, e.g. integer constants
# len(expr.config_string) == 0 avoids hidden configs which reflects the values of choices
return kconfiglib.Kconfig.n
if (kconfiglib.expr_value(expr) and len(expr.config_string) > 0 and expr_nodes_invisible(expr)):
# hidden config dependencies which will be written to sdkconfig as enabled ones.
return kconfiglib.Kconfig.y
if any(node.item.name.startswith(visibility.target_env_var) for node in expr.nodes):
# We know the actual values for IDF_TARGETs
return kconfiglib.Kconfig.y if kconfiglib.expr_value(expr) else kconfiglib.Kconfig.n
return expr
def write_menu_item(f, node, visibility):
def is_choice(node):
""" Skip choice nodes, they are handled as part of the parent (see below) """
return isinstance(node.parent.item, kconfiglib.Choice)
if is_choice(node) or not visibility.visible(node):
return
try:
name = node.item.name
except AttributeError:
name = None
is_menu = node_is_menu(node)
# Heading
if name:
title = 'CONFIG_%s' % name
else:
# if no symbol name, use the prompt as the heading
title = node.prompt[0]
f.write('.. _%s:\n\n' % get_link_anchor(node))
f.write('%s\n' % title)
f.write(HEADING_SYMBOLS[get_heading_level(node)] * len(title))
f.write('\n\n')
if name:
f.write('%s%s\n\n' % (INDENT, node.prompt[0]))
f.write('%s:emphasis:`Found in:` %s\n\n' % (INDENT, get_breadcrumbs(node)))
try:
if node.help:
# Help text normally contains newlines, but spaces at the beginning of
# each line are stripped by kconfiglib. We need to re-indent the text
# to produce valid ReST.
f.write(format_rest_text(node.help, INDENT))
f.write('\n')
except AttributeError:
pass # No help
if isinstance(node.item, kconfiglib.Choice):
f.write('%sAvailable options:\n' % INDENT)
choice_node = node.list
while choice_node:
# Format available options as a list
f.write('%s- %-20s (%s)\n' % (INDENT * 2, choice_node.prompt[0], choice_node.item.name))
if choice_node.help:
HELP_INDENT = INDENT * 2
fmt_help = format_rest_text(choice_node.help, ' ' + HELP_INDENT)
f.write('%s \n%s\n' % (HELP_INDENT, fmt_help))
choice_node = choice_node.next
f.write('\n\n')
if isinstance(node.item, kconfiglib.Symbol):
def _expr_str(sc):
if sc.is_constant or not sc.nodes or sc.choice:
return '{}'.format(sc.name)
return ':ref:`%s%s`' % (sc.kconfig.config_prefix, sc.name)
range_strs = []
for low, high, cond in node.item.ranges:
cond = _minimize_expr(cond, visibility)
if cond == kconfiglib.Kconfig.n:
continue
if not isinstance(cond, tuple) and cond != kconfiglib.Kconfig.y:
if len(cond.nodes) > 0 and all(not visibility.visible(i) for i in cond.nodes):
if not kconfiglib.expr_value(cond):
continue
range_str = '%s- from %s to %s' % (INDENT * 2, low.str_value, high.str_value)
if cond != kconfiglib.Kconfig.y and not kconfiglib.expr_value(cond):
range_str += ' if %s' % kconfiglib.expr_str(cond, _expr_str)
range_strs.append(range_str)
if len(range_strs) > 0:
f.write('%sRange:\n' % INDENT)
f.write('\n'.join(range_strs))
f.write('\n\n')
default_strs = []
for default, cond in node.item.defaults:
cond = _minimize_expr(cond, visibility)
if cond == kconfiglib.Kconfig.n:
continue
if not isinstance(cond, tuple) and cond != kconfiglib.Kconfig.y:
if len(cond.nodes) > 0 and all(not visibility.visible(i) for i in cond.nodes):
if not kconfiglib.expr_value(cond):
continue
# default.type is mostly UNKNOWN so it cannot be used reliably for detecting the type
d = default.str_value
if d in ['y', 'Y']:
d = 'Yes (enabled)'
elif d in ['n', 'N']:
d = 'No (disabled)'
elif re.search(r'[^0-9a-fA-F]', d): # simple string detection: if it not a valid number
d = '"%s"' % d
default_str = '%s- %s' % (INDENT * 2, d)
if cond != kconfiglib.Kconfig.y and not kconfiglib.expr_value(cond):
default_str += ' if %s' % kconfiglib.expr_str(cond, _expr_str)
default_strs.append(default_str)
if len(default_strs) > 0:
f.write('%sDefault value:\n' % INDENT)
f.write('\n'.join(default_strs))
f.write('\n\n')
if is_menu:
# enumerate links to child items
child_list = []
child = node.list
while child:
if not is_choice(child) and child.prompt and visibility.visible(child):
child_list.append((child.prompt[0], get_link_anchor(child)))
child = child.next
if len(child_list) > 0:
f.write('Contains:\n\n')
sorted_child_list = sorted(child_list, key=lambda pair: pair[0].lower())
ref_list = ['- :ref:`{}`'.format(anchor) for _, anchor in sorted_child_list]
f.write('\n'.join(ref_list))
f.write('\n\n')
if __name__ == '__main__':
print("Run this via 'confgen.py --output doc FILENAME'")