#!/usr/bin/env python3 """ Generates an alphabetical index of Kconfig symbols with links in index.rst, and a separate CONFIG_FOO.rst file for each Kconfig symbol. The generated symbol pages can be referenced in RST as :option:`foo`, and the generated index page as `configuration options`_. Optionally, the documentation can be split up based on where symbols are defined. See the --modules flag. """ import argparse import collections import errno from operator import attrgetter import os import pathlib import sys import textwrap import kconfiglib def rst_link(sc): # Returns an RST link (string) for the symbol/choice 'sc', or the normal # Kconfig expression format (e.g. just the name) for 'sc' if it can't be # turned into a link. if isinstance(sc, kconfiglib.Symbol): # Skip constant and undefined symbols by checking if expr.nodes is # empty if sc.nodes: # The "\ " avoids RST issues for !CONFIG_FOO -- see # http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#character-level-inline-markup return fr"\ :option:`{sc.name} `" elif isinstance(sc, kconfiglib.Choice): # Choices appear as dependencies of choice symbols. # # Use a :ref: instead of an :option:. With an :option:, we'd have to have # an '.. option::' in the choice reference page as well. That would make # the internal choice ID show up in the documentation. # # Note that the first pair of <...> is non-syntactic here. We just display # choices links within <> in the documentation. return fr"\ :ref:`<{choice_desc(sc)}> <{choice_id(sc)}>`" # Can't turn 'sc' into a link. Use the standard Kconfig format. return kconfiglib.standard_sc_expr_str(sc) def expr_str(expr): # Returns the Kconfig representation of 'expr', with symbols/choices turned # into RST links return kconfiglib.expr_str(expr, rst_link) def main(): init() write_index_pages() # Plural since there's more than one in --modules mode if os.getenv("KCONFIG_TURBO_MODE") == "1": write_dummy_syms_page() else: write_sym_pages() def init(): # Initializes these globals: # # kconf: # Kconfig instance for the configuration # # out_dir: # Output directory # # index_desc: # Set to the corresponding command-line arguments (or None if # missing) # # modules: # A list of (, <suffix>, <path>, <desc. path>) tuples. See the # --modules flag. Empty if --modules wasn't passed. # # <path> is an absolute pathlib.Path instance, which is handy for robust # path comparisons. # # separate_all_index: # True if --separate-all-index was passed # # strip_module_paths: # True unless --keep-module-paths was passed global kconf global out_dir global index_desc global modules global separate_all_index global strip_module_paths args = parse_args() kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) out_dir = args.out_dir index_desc = args.index_desc separate_all_index = args.separate_all_index strip_module_paths = args.strip_module_paths modules = [] for module_spec in args.modules: # Split on ',', but keep any ',,' as a literal ','. Temporarily # represent a literal comma with null. spec_parts = [part.replace("\0", ",") for part in module_spec.replace(",,", "\0").split(",")] if len(spec_parts) == 3: title, suffix, path_s = spec_parts desc_path = None elif len(spec_parts) == 4: title, suffix, path_s, desc_path = spec_parts else: sys.exit(f"error: --modules argument '{module_spec}' should have " "the format <title>,<suffix>,<path> or the format " "<title>,<suffix>,<path>,<index description filename>. " "A doubled ',,' in any part is treated as a literal " "comma.") abspath = pathlib.Path(path_s).resolve() if not abspath.exists(): sys.exit(f"error: path '{abspath}' in --modules argument does not exist") modules.append((title, suffix, abspath, desc_path)) def parse_args(): # Parses command-line arguments parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument( "--kconfig", metavar="KCONFIG", default="Kconfig", help="Top-level Kconfig file (default: Kconfig)") parser.add_argument( "--index-desc", metavar="RST_FILE", help="""\ Path to an RST file with description text for the top-level index.rst index page. If missing, a generic description will be used. Used both in --modules and non-modules mode. See <index description path> in the --modules description as well.""") parser.add_argument( "--modules", metavar="MODULE_SPECIFICATION", nargs="+", default=[], help="""\ Specifies that the documentation should be split into several index pages, based on where symbols are defined. Each MODULE_SPECIFICATION has the form <title>,<suffix>,<path>[,<index description path>] , where <index description path> is optional. To insert a literal comma into any of the parts, double it, e.g. 'My title,, with a comma'. A separate index-<suffix>.rst symbol index page is generated for each MODULE_SPECIFICATION, with links to all symbols that are defined inside <path> (possibly more than one level deep). The title of the index is "<title> Configuration Options", and a 'configuration_options_<suffix>' RST link target is inserted at the top of the index page. If <index description path> is given, it should be the path to an RST file. The contents of this file will appear under at the top of the symbol index page for the module, underneath the heading. If no <index description path> is given, a generic description is used instead. The top-level index.rst index page contains a TOC tree that links to the index-*.rst pages for any modules. It also includes a list of all symbols, including symbols that do not appear in any module. Pass --separate-all-index to use a separate index for the list of all symbols. If a symbol is defined in more than one module, it will be listed on several index pages. Passing --modules also tweaks how paths are displayed on symbol information pages, showing '<title>/path/within/module/Kconfig' for paths that fall within modules. This behavior can be disabled by passing --keep-module-paths.""") parser.add_argument( "--separate-all-index", action="store_true", help="""\ Instead of listing all symbols in index.rst, use a separate index-all.rst index page, which is linked from index.rst. Probably only useful in combination with --modules. index-all.rst has a 'configuration_options_all' RST link target. This option can make the documentation build orders of magnitude faster when the index.rst generated by this script is the top-level page, because Sphinx currently runs into a bottleneck with large top-level pages with some themes. See https://github.com/sphinx-doc/sphinx/issues/6909.""") parser.add_argument( "--keep-module-paths", dest="strip_module_paths", action="store_false", help="Do not rewrite paths that fall within modules. See --modules.") parser.add_argument( "out_dir", metavar="OUTPUT_DIRECTORY", help="Directory to write .rst output files to") return parser.parse_args() def write_index_pages(): # Writes all index pages. --modules will give more than one. # Implementation note: Functions used here add any newlines they want # before their output themselves. Try to keep this consistent if you change # things. write_main_index_page() write_module_index_pages() def write_main_index_page(): # Writes the main index page, which lists all symbols. In --modules mode, # links to the module index pages are included. If --separate-all-index was # passed, a separate index-all.rst index is generated as well. rst = index_header(title="Configuration Options", link="configuration_options", desc_path=index_desc) if separate_all_index: rst += """ This index page lists all symbols, regardless of where they are defined: .. toctree:: :maxdepth: 1 index-all.rst\ """ write_if_updated("index-all.rst", index_header(title="All Configuration Options", link="configuration_options_all", desc_path=None) + sym_table_rst("Configuration Options", kconf.unique_defined_syms)) if modules: rst += """ These index pages only list symbols defined within a particular subsystem: .. toctree:: :maxdepth: 1 """ + "\n".join(" index-" + suffix for _, suffix, _, _, in modules) if not separate_all_index: # Put index of all symbols in index.rst rst += sym_table_rst("All configuration options", kconf.unique_defined_syms) write_if_updated("index.rst", rst) def write_module_index_pages(): # Writes index index-<suffix>.rst index pages for all modules # Maps each module title to a set of Symbols in the module module2syms = collections.defaultdict(set) for sym in kconf.unique_defined_syms: # Loop over all definition locations for node in sym.nodes: mod_title = path2module(node.filename) if mod_title is not None: module2syms[mod_title].add(node.item) # Iterate 'modules' instead of 'module2syms' so that an index page gets # written even if the module has no symbols for title, suffix, _, desc_path in modules: rst = index_header(title=title + " Configuration Options", link="configuration_options_" + suffix, desc_path=desc_path) rst += sym_table_rst("Configuration Options", module2syms[title]) write_if_updated(f"index-{suffix}.rst", rst) def sym_table_rst(title, syms): # Returns RST for the list of symbols on index pages. 'title' is the # heading to use. rst = f""" {title} {len(title)*'*'} .. list-table:: :header-rows: 1 :widths: auto * - Symbol name - Help/prompt """ for sym in sorted(syms, key=attrgetter("name")): rst += f"""\ * - :option:`CONFIG_{sym.name}` - {sym_index_desc(sym)} """ return rst def sym_index_desc(sym): # Returns the description used for 'sym' on the index page # Use the first help text, if available for node in sym.nodes: if node.help is not None: return node.help.replace("\n", "\n ") # If there's no help, use the first prompt string for node in sym.nodes: if node.prompt: return node.prompt[0] # No help text or prompt return "" def index_header(title, link, desc_path): # Returns the RST for the beginning of a symbol index page. # # title: # Page title # # link: # Link target string # # desc_path: # Path to file with RST to put at the of the page, underneath the # heading. If None, a generic description is used. if desc_path is None: desc = DEFAULT_INDEX_DESC else: try: with open(desc_path, encoding="utf-8") as f: desc = f.read() except OSError as e: sys.exit("error: failed to open index description file " f"'{desc_path}': {e}") return f"""\ .. _{link}: {title} {len(title)*'='} {desc} This documentation is generated automatically from the :file:`Kconfig` files by the :file:`{os.path.basename(__file__)}` script. Click on symbols for more information.""" DEFAULT_INDEX_DESC = """\ :file:`Kconfig` files describe build-time configuration options (called symbols in Kconfig-speak), how they're grouped into menus and sub-menus, and dependencies between them that determine what configurations are valid. :file:`Kconfig` files appear throughout the directory tree. For example, :file:`subsys/power/Kconfig` defines power-related options.\ """ def write_sym_pages(): # Writes all symbol and choice pages for sym in kconf.unique_defined_syms: write_sym_page(sym) for choice in kconf.unique_choices: write_choice_page(choice) def write_dummy_syms_page(): # Writes a dummy page that just has targets for all symbol links so that # they can be referenced from elsewhere in the documentation. This speeds # up builds when we don't need the Kconfig symbol documentation. rst = ":orphan:\n\nDummy symbols page for turbo mode.\n\n" for sym in kconf.unique_defined_syms: rst += f".. option:: CONFIG_{sym.name}\n" write_if_updated("dummy-syms.rst", rst) def write_sym_page(sym): # Writes documentation for 'sym' to <out_dir>/CONFIG_<sym.name>.rst write_if_updated(f"CONFIG_{sym.name}.rst", sym_header_rst(sym) + help_rst(sym) + direct_deps_rst(sym) + defaults_rst(sym) + select_imply_rst(sym) + selecting_implying_rst(sym) + kconfig_definition_rst(sym)) def write_choice_page(choice): # Writes documentation for 'choice' to <out_dir>/choice_<n>.rst, where <n> # is the index of the choice in kconf.choices (where choices appear in the # same order as in the Kconfig files) write_if_updated(choice_id(choice) + ".rst", choice_header_rst(choice) + help_rst(choice) + direct_deps_rst(choice) + defaults_rst(choice) + choice_syms_rst(choice) + kconfig_definition_rst(choice)) def sym_header_rst(sym): # Returns RST that appears at the top of symbol reference pages # - :orphan: suppresses warnings for the symbol RST files not being # included in any toctree # # - '.. title::' sets the title of the document (e.g. <title>). This seems # to be poorly documented at the moment. return ":orphan:\n\n" \ f".. title:: {sym.name}\n\n" \ f".. option:: CONFIG_{sym.name}\n\n" \ f"{prompt_rst(sym)}\n\n" \ f"Type: ``{kconfiglib.TYPE_TO_STR[sym.type]}``\n\n" def choice_header_rst(choice): # Returns RST that appears at the top of choice reference pages return ":orphan:\n\n" \ f".. title:: {choice_desc(choice)}\n\n" \ f".. _{choice_id(choice)}:\n\n" \ f".. describe:: {choice_desc(choice)}\n\n" \ f"{prompt_rst(choice)}\n\n" \ f"Type: ``{kconfiglib.TYPE_TO_STR[choice.type]}``\n\n" def prompt_rst(sc): # Returns RST that lists the prompts of 'sc' (symbol or choice) return "\n\n".join(f"*{node.prompt[0]}*" for node in sc.nodes if node.prompt) \ or "*(No prompt -- not directly user assignable.)*" def help_rst(sc): # Returns RST that lists the help text(s) of 'sc' (symbol or choice). # Symbols and choices with multiple definitions can have multiple help # texts. rst = "" for node in sc.nodes: if node.help is not None: rst += "Help\n" \ "====\n\n" \ f"{node.help}\n\n" return rst def direct_deps_rst(sc): # Returns RST that lists the direct dependencies of 'sc' (symbol or choice) if sc.direct_dep is sc.kconfig.y: return "" return "Direct dependencies\n" \ "===================\n\n" \ f"{expr_str(sc.direct_dep)}\n\n" \ "*(Includes any dependencies from ifs and menus.)*\n\n" def defaults_rst(sc): # Returns RST that lists the 'default' properties of 'sc' (symbol or # choice) if isinstance(sc, kconfiglib.Symbol) and sc.choice: # 'default's on choice symbols have no effect (and generate a warning). # The implicit value hint below would be misleading as well. return "" heading = "Default" if len(sc.defaults) != 1: heading += "s" rst = f"{heading}\n{len(heading)*'='}\n\n" if sc.defaults: for value, cond in sc.orig_defaults: rst += "- " + expr_str(value) if cond is not sc.kconfig.y: rst += " if " + expr_str(cond) rst += "\n" else: rst += "No defaults. Implicitly defaults to " if isinstance(sc, kconfiglib.Choice): rst += "the first (visible) choice option.\n" elif sc.orig_type in (kconfiglib.BOOL, kconfiglib.TRISTATE): rst += "``n``.\n" else: # This is accurate even for int/hex symbols, though an active # 'range' might clamp the value (which is then treated as zero) rst += "the empty string.\n" return rst + "\n" def choice_syms_rst(choice): # Returns RST that lists the symbols contained in the choice if not choice.syms: return "" rst = "Choice options\n" \ "==============\n\n" for sym in choice.syms: # Generates a link rst += f"- {expr_str(sym)}\n" return rst + "\n" def select_imply_rst(sym): # Returns RST that lists the symbols 'select'ed or 'imply'd by the symbol rst = "" def add_select_imply_rst(type_str, lst): # Adds RST that lists the selects/implies from 'lst', which holds # (<symbol>, <condition>) tuples, if any. Also adds a heading derived # from 'type_str' if there any selects/implies. nonlocal rst if lst: heading = f"Symbols {type_str} by this symbol" rst += f"{heading}\n{len(heading)*'='}\n\n" for select, cond in lst: rst += "- " + rst_link(select) if cond is not sym.kconfig.y: rst += " if " + expr_str(cond) rst += "\n" rst += "\n" add_select_imply_rst("selected", sym.orig_selects) add_select_imply_rst("implied", sym.orig_implies) return rst def selecting_implying_rst(sym): # Returns RST that lists the symbols that are 'select'ing or 'imply'ing the # symbol rst = "" def add_selecting_implying_rst(type_str, expr): # Writes a link for each symbol that selects the symbol (if 'expr' is # sym.rev_dep) or each symbol that imply's the symbol (if 'expr' is # sym.weak_rev_dep). Also adds a heading at the top derived from # type_str ("select"/"imply"), if there are any selecting/implying # symbols. nonlocal rst if expr is not sym.kconfig.n: heading = f"Symbols that {type_str} this symbol" rst += f"{heading}\n{len(heading)*'='}\n\n" # The reverse dependencies from each select/imply are ORed together for select in kconfiglib.split_expr(expr, kconfiglib.OR): # - 'select/imply A if B' turns into A && B # - 'select/imply A' just turns into A # # In both cases, we can split on AND and pick the first # operand. rst += "- {}\n".format(rst_link( kconfiglib.split_expr(select, kconfiglib.AND)[0])) rst += "\n" add_selecting_implying_rst("select", sym.rev_dep) add_selecting_implying_rst("imply", sym.weak_rev_dep) return rst def kconfig_definition_rst(sc): # Returns RST that lists the Kconfig definition location, include path, # menu path, and Kconfig definition for each node (definition location) of # 'sc' (symbol or choice) # Fancy Unicode arrow. Added in '93, so ought to be pretty safe. arrow = " \N{RIGHTWARDS ARROW} " def include_path(node): if not node.include_path: # In the top-level Kconfig file return "" return "Included via {}\n\n".format( arrow.join(f"``{strip_module_path(filename)}:{linenr}``" for filename, linenr in node.include_path)) def menu_path(node): path = "" while node.parent is not node.kconfig.top_node: node = node.parent # Promptless choices can show up as parents, e.g. when people # define choices in multiple locations to add symbols. Use # standard_sc_expr_str() to show them. That way they show up as # '<choice (name if any)>'. path = arrow + \ (node.prompt[0] if node.prompt else kconfiglib.standard_sc_expr_str(node.item)) + \ path return "(Top)" + path heading = "Kconfig definition" if len(sc.nodes) > 1: heading += "s" rst = f"{heading}\n{len(heading)*'='}\n\n" rst += ".. highlight:: kconfig" for node in sc.nodes: rst += "\n\n" \ f"At ``{strip_module_path(node.filename)}:{node.linenr}``\n\n" \ f"{include_path(node)}" \ f"Menu path: {menu_path(node)}\n\n" \ ".. parsed-literal::\n\n" \ f"{textwrap.indent(node.custom_str(rst_link), 4*' ')}" # Not the last node? if node is not sc.nodes[-1]: # Add a horizontal line between multiple definitions rst += "\n\n----" rst += "\n\n*(The 'depends on' condition includes propagated " \ "dependencies from ifs and menus.)*" return rst def choice_id(choice): # Returns "choice_<n>", where <n> is the index of the choice in the Kconfig # files. The choice that appears first has index 0, the next one index 1, # etc. # # This gives each choice a unique ID, which is used to generate its RST # filename and in cross-references. Choices (usually) don't have names, so # we can't use that, and the prompt isn't guaranteed to be unique. # Pretty slow, but fast enough return f"choice_{choice.kconfig.unique_choices.index(choice)}" def choice_desc(choice): # Returns a description of the choice, used as the title of choice # reference pages and in link texts. The format is # "choice <name, if any>: <prompt text>" desc = "choice" if choice.name: desc += " " + choice.name # The choice might be defined in multiple locations. Use the prompt from # the first location that has a prompt. for node in choice.nodes: if node.prompt: desc += ": " + node.prompt[0] break return desc def path2module(path): # Returns the name of module that 'path' appears in, or None if it does not # appear in a module. 'path' is assumed to be relative to 'srctree'. # Have to be careful here so that e.g. foo/barbaz/qaz isn't assumed to be # part of a module with path foo/bar/. Play it safe with pathlib. abspath = pathlib.Path(kconf.srctree).joinpath(path).resolve() for name, _, mod_path, _ in modules: try: abspath.relative_to(mod_path) except ValueError: # Not within the module continue return name return None def strip_module_path(path): # If 'path' is within a module, strips the module path from it, and adds a # '<module name>/' prefix. Otherwise, returns 'path' unchanged. 'path' is # assumed to be relative to 'srctree'. if strip_module_paths: abspath = pathlib.Path(kconf.srctree).joinpath(path).resolve() for title, _, mod_path, _ in modules: try: relpath = abspath.relative_to(mod_path) except ValueError: # Not within the module continue return f"<{title}>{os.path.sep}{relpath}" return path def write_if_updated(filename, s): # Writes 's' as the contents of <out_dir>/<filename>, but only if it # differs from the current contents of the file. This avoids unnecessary # timestamp updates, which trigger documentation rebuilds. path = os.path.join(out_dir, filename) try: with open(path, "r", encoding="utf-8") as f: if s == f.read(): return except OSError as e: if e.errno != errno.ENOENT: raise with open(path, "w", encoding="utf-8") as f: f.write(s) if __name__ == "__main__": main()