gen_kconfig_doc.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # gen_kconfig_doc - confgen.py support for generating ReST markup documentation
  5. #
  6. # For each option in the loaded Kconfig (e.g. 'FOO'), CONFIG_FOO link target is
  7. # generated, allowing options to be referenced in other documents
  8. # (using :ref:`CONFIG_FOO`)
  9. #
  10. # Copyright 2017-2020 Espressif Systems (Shanghai) PTE LTD
  11. #
  12. # Licensed under the Apache License, Version 2.0 (the "License");
  13. # you may not use this file except in compliance with the License.
  14. # You may obtain a copy of the License at
  15. #
  16. # http:#www.apache.org/licenses/LICENSE-2.0
  17. #
  18. # Unless required by applicable law or agreed to in writing, software
  19. # distributed under the License is distributed on an "AS IS" BASIS,
  20. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  21. # See the License for the specific language governing permissions and
  22. # limitations under the License.
  23. from __future__ import print_function
  24. import os
  25. import re
  26. import sys
  27. try:
  28. from . import kconfiglib
  29. except Exception:
  30. sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
  31. import kconfiglib
  32. # Indentation to be used in the generated file
  33. INDENT = ' '
  34. # Characters used when underlining section heading
  35. HEADING_SYMBOLS = '#*=-^"+'
  36. # Keep the heading level in sync with api-reference/kconfig.rst
  37. INITIAL_HEADING_LEVEL = 3
  38. MAX_HEADING_LEVEL = len(HEADING_SYMBOLS) - 1
  39. class ConfigTargetVisibility(object):
  40. """
  41. Determine the visibility of Kconfig options based on IDF targets. Note that other environment variables should not
  42. imply invisibility and neither dependencies on visible options with default disabled state. This difference makes
  43. it necessary to implement our own visibility and cannot use the visibility defined inside Kconfiglib.
  44. """
  45. def __init__(self, config, target):
  46. # target actually is not necessary here because kconfiglib.expr_value() will evaluate it internally
  47. self.config = config
  48. self.visibility = dict() # node name to (x, y) mapping where x is the visibility (True/False) and y is the
  49. # name of the config which implies the visibility
  50. self.target_env_var = 'IDF_TARGET'
  51. self.direct_eval_set = frozenset([kconfiglib.EQUAL, kconfiglib.UNEQUAL, kconfiglib.LESS, kconfiglib.LESS_EQUAL,
  52. kconfiglib.GREATER, kconfiglib.GREATER_EQUAL])
  53. def _implies_invisibility(self, item):
  54. if isinstance(item, tuple):
  55. if item[0] == kconfiglib.NOT:
  56. (invisibility, source) = self._implies_invisibility(item[1])
  57. if source is not None and source.startswith(self.target_env_var):
  58. return (not invisibility, source)
  59. else:
  60. # we want to be visible all configs which are not dependent on target variables,
  61. # e.g. "depends on XY" and "depends on !XY" as well
  62. return (False, None)
  63. elif item[0] == kconfiglib.AND:
  64. (invisibility, source) = self._implies_invisibility(item[1])
  65. if invisibility:
  66. return (True, source)
  67. (invisibility, source) = self._implies_invisibility(item[2])
  68. if invisibility:
  69. return (True, source)
  70. return (False, None)
  71. elif item[0] == kconfiglib.OR:
  72. implication_list = [self._implies_invisibility(item[1]), self._implies_invisibility(item[2])]
  73. if all([implies for (implies, _) in implication_list]):
  74. source_list = [s for (_, s) in implication_list if s.startswith(self.target_env_var)]
  75. if len(set(source_list)) != 1: # set removes the duplicates
  76. print('[WARNING] list contains targets: {}'.format(source_list))
  77. return (True, source_list[0])
  78. return (False, None)
  79. elif item[0] in self.direct_eval_set:
  80. def node_is_invisible(item):
  81. return all([node.prompt is None for node in item.nodes])
  82. if node_is_invisible(item[1]) or node_is_invisible(item[1]):
  83. # it makes no sense to call self._implies_invisibility() here because it won't generate any useful
  84. # "source"
  85. return (not kconfiglib.expr_value(item), None)
  86. else:
  87. # expressions with visible configs can be changed to make the item visible
  88. return (False, None)
  89. else:
  90. raise RuntimeError('Unimplemented operation in {}'.format(item))
  91. else: # Symbol or Choice
  92. vis_list = [self._visible(node) for node in item.nodes]
  93. if len(vis_list) > 0 and all([not visible for (visible, _) in vis_list]):
  94. source_list = [s for (_, s) in vis_list if s is not None and s.startswith(self.target_env_var)]
  95. if len(set(source_list)) != 1: # set removes the duplicates
  96. print('[WARNING] list contains targets: {}'.format(source_list))
  97. return (True, source_list[0])
  98. if item.name.startswith(self.target_env_var):
  99. return (not kconfiglib.expr_value(item), item.name)
  100. if len(vis_list) == 1:
  101. (visible, source) = vis_list[0]
  102. if visible:
  103. return (False, item.name) # item.name is important here in case the result will be inverted: if
  104. # the dependency is on another config then it can be still visible
  105. return (False, None)
  106. def _visible(self, node):
  107. if isinstance(node.item, kconfiglib.Symbol) or isinstance(node.item, kconfiglib.Choice):
  108. dependencies = node.item.direct_dep # "depends on" for configs
  109. name_id = node.item.name
  110. simple_def = len(node.item.nodes) <= 1 # defined only in one source file
  111. # Probably it is not necessary to check the default statements.
  112. else:
  113. dependencies = node.visibility # "visible if" for menu
  114. name_id = node.prompt[0]
  115. simple_def = False # menus can be defined with the same name at multiple locations and they don't know
  116. # about each other like configs through node.item.nodes. Therefore, they cannot be stored and have to be
  117. # re-evaluated always.
  118. try:
  119. (visib, source) = self.visibility[name_id]
  120. except KeyError:
  121. def invert_first_arg(_tuple):
  122. return (not _tuple[0], _tuple[1])
  123. (visib, source) = self._visible(node.parent) if node.parent else (True, None)
  124. if visib:
  125. (visib, source) = invert_first_arg(self._implies_invisibility(dependencies))
  126. if simple_def:
  127. # Configs defined at multiple places are not stored because they could have different visibility based
  128. # on different targets. kconfiglib.expr_value() will handle the visibility.
  129. self.visibility[name_id] = (visib, source)
  130. return (visib, source) # not used in "finally" block because failure messages from _implies_invisibility are
  131. # this way more understandable
  132. def visible(self, node):
  133. if not node.prompt:
  134. # don't store this in self.visibility because don't want to stop at invisible nodes when recursively
  135. # searching for invisible targets
  136. return False
  137. return self._visible(node)[0]
  138. def write_docs(config, visibility, filename):
  139. """ Note: writing .rst documentation ignores the current value
  140. of any items. ie the --config option can be ignored.
  141. (However at time of writing it still needs to be set to something...) """
  142. with open(filename, "w") as f:
  143. for node in config.node_iter():
  144. write_menu_item(f, node, visibility)
  145. def node_is_menu(node):
  146. try:
  147. return node.item == kconfiglib.MENU or node.is_menuconfig
  148. except AttributeError:
  149. return False # not all MenuNodes have is_menuconfig for some reason
  150. def get_breadcrumbs(node):
  151. # this is a bit wasteful as it recalculates each time, but still...
  152. result = []
  153. node = node.parent
  154. while node.parent:
  155. if node.prompt:
  156. result = [":ref:`%s`" % get_link_anchor(node)] + result
  157. node = node.parent
  158. return " > ".join(result)
  159. def get_link_anchor(node):
  160. try:
  161. return "CONFIG_%s" % node.item.name
  162. except AttributeError:
  163. assert(node_is_menu(node)) # only menus should have no item.name
  164. # for menus, build a link anchor out of the parents
  165. result = []
  166. while node.parent:
  167. if node.prompt:
  168. result = [re.sub(r"[^a-zA-z0-9]+", "-", node.prompt[0])] + result
  169. node = node.parent
  170. result = "-".join(result).lower()
  171. return result
  172. def get_heading_level(node):
  173. result = INITIAL_HEADING_LEVEL
  174. node = node.parent
  175. while node.parent:
  176. result += 1
  177. if result == MAX_HEADING_LEVEL:
  178. return MAX_HEADING_LEVEL
  179. node = node.parent
  180. return result
  181. def format_rest_text(text, indent):
  182. # Format an indented text block for use with ReST
  183. text = indent + text.replace('\n', '\n' + indent)
  184. # Escape some characters which are inline formatting in ReST
  185. text = text.replace("*", "\\*")
  186. text = text.replace("_", "\\_")
  187. # replace absolute links to documentation by relative ones
  188. text = re.sub(r'https://docs.espressif.com/projects/esp-idf/\w+/\w+/(.+)\.html', r':doc:`../\1`', text)
  189. text += '\n'
  190. return text
  191. def write_menu_item(f, node, visibility):
  192. def is_choice(node):
  193. """ Skip choice nodes, they are handled as part of the parent (see below) """
  194. return isinstance(node.parent.item, kconfiglib.Choice)
  195. if is_choice(node) or not visibility.visible(node):
  196. return
  197. try:
  198. name = node.item.name
  199. except AttributeError:
  200. name = None
  201. is_menu = node_is_menu(node)
  202. # Heading
  203. if name:
  204. title = 'CONFIG_%s' % name
  205. else:
  206. # if no symbol name, use the prompt as the heading
  207. title = node.prompt[0]
  208. f.write(".. _%s:\n\n" % get_link_anchor(node))
  209. f.write('%s\n' % title)
  210. f.write(HEADING_SYMBOLS[get_heading_level(node)] * len(title))
  211. f.write('\n\n')
  212. if name:
  213. f.write('%s%s\n\n' % (INDENT, node.prompt[0]))
  214. f.write('%s:emphasis:`Found in:` %s\n\n' % (INDENT, get_breadcrumbs(node)))
  215. try:
  216. if node.help:
  217. # Help text normally contains newlines, but spaces at the beginning of
  218. # each line are stripped by kconfiglib. We need to re-indent the text
  219. # to produce valid ReST.
  220. f.write(format_rest_text(node.help, INDENT))
  221. f.write('\n')
  222. except AttributeError:
  223. pass # No help
  224. if isinstance(node.item, kconfiglib.Choice):
  225. f.write('%sAvailable options:\n' % INDENT)
  226. choice_node = node.list
  227. while choice_node:
  228. # Format available options as a list
  229. f.write('%s- %-20s (%s)\n' % (INDENT * 2, choice_node.prompt[0], choice_node.item.name))
  230. if choice_node.help:
  231. HELP_INDENT = INDENT * 2
  232. fmt_help = format_rest_text(choice_node.help, ' ' + HELP_INDENT)
  233. f.write('%s \n%s\n' % (HELP_INDENT, fmt_help))
  234. choice_node = choice_node.next
  235. f.write('\n\n')
  236. if is_menu:
  237. # enumerate links to child items
  238. first = True
  239. child = node.list
  240. while child:
  241. try:
  242. if not is_choice(child) and child.prompt and visibility.visible(child):
  243. if first:
  244. f.write("Contains:\n\n")
  245. first = False
  246. f.write('- :ref:`%s`\n' % get_link_anchor(child))
  247. except AttributeError:
  248. pass
  249. child = child.next
  250. f.write('\n')
  251. if __name__ == '__main__':
  252. print("Run this via 'confgen.py --output doc FILENAME'")