""" Generates a Kconfig symbol reference in RST format, with a separate CONFIG_FOO.rst file for each symbol, and an alphabetical index with links in index.rst. 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 r"\ :option:`{0} `".format(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 r"\ :ref:`<{}> <{}>`" \ .format(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(): # Writes index.rst and the symbol RST files init() # Writes index page(s) if modules: write_module_index_pages() else: write_index_page(kconf.unique_defined_syms, None, None, desc_from_file(top_index_desc)) # Write symbol pages 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 # # non_module_title: # Title for index of non-module symbols, as passed via # --non-module-title # # top_index_desc/non_module_index_desc/all_index_desc: # Set to the corresponding command-line arguments (or None if # missing) # # modules: # A list of (, <suffix>, <path>, <index description>) tuples. See # the --modules argument. Empty if --modules wasn't passed. # # <path> is an absolute pathlib.Path instance, which is handy for robust # path comparisons. # # strip_module_paths: # True unless --keep-module-paths was passed global kconf global out_dir global non_module_title global top_index_desc global non_module_index_desc global all_index_desc global modules global strip_module_paths args = parse_args() kconf = kconfiglib.Kconfig(args.kconfig) out_dir = args.out_dir non_module_title = args.non_module_title top_index_desc = args.top_index_desc non_module_index_desc = args.non_module_index_desc all_index_desc = args.all_index_desc strip_module_paths = args.strip_module_paths modules = [] for module_spec in args.modules: if module_spec.count(":") == 2: title, suffix, path_s = module_spec.split(":") index_text = DEFAULT_INDEX_DESCRIPTION elif module_spec.count(":") == 3: title, suffix, path_s, index_text_fname = module_spec.split(":") index_text = desc_from_file(index_text_fname) else: sys.exit("error: --modules argument '{}' should have the format " "<title>:<suffix>:<path> or the format " "<title>:<suffix>:<path>:<index description filename>" .format(module_spec)) path = pathlib.Path(path_s).resolve() if not path.exists(): sys.exit("error: path '{}' in --modules argument does not exist" .format(path)) modules.append((title, suffix, path, index_text)) 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( "--non-module-title", metavar="NON_MODULE_TITLE", default="Zephyr", help="""\ The title used for the index page that lists the symbols that do not appear in any module (index-main.rst). Only meaningful in --modules mode.""") parser.add_argument( "--top-index-desc", metavar="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 filename> in the --modules description as well.""") parser.add_argument( "--non-module-index-desc", metavar="FILE", help="""\ Like --top-index-desc, but for the index page that lists the non-module symbols in --modules mode (index-main.rst).""") parser.add_argument( "--all-index-desc", metavar="FILE", help="""\ Like --top-index-desc, but for the index page that lists all symbols in --modules mode (index-all.rst).""") parser.add_argument( "--modules", metavar="MODULE_SPECIFICATION", nargs="+", default=[], help="""\ Used to split the documentation into several index pages, based on where symbols are defined. MODULE_SPECIFICATION is either a <title>:<suffix>:<path> tuple or a <title>:<suffix>:<path>:<index description filename> tuple. If the second form is used, <index description filename> should be the path to an RST file, the contents of which will appear on the index page that lists the symbols for the module (under an automatically-inserted Overview heading). If the first form is used, a generic description will be used instead. A separate index-<suffix>.rst index page is generated for each tuple, with the title "<title> Configuration Options", a 'configuration_options_<suffix>' RST link target, and links to all symbols that appear under the tuple's <path> (possibly more than one level deep). Symbols that do not appear in any module are added to index-main.rst. A separate index-all.rst page is generated that lists all symbols, regardless of whether they're from a module or not. The generated index.rst contains a TOC tree that links to the other index-*.rst pages. If a symbol is defined in more than one module (or both inside and outside a 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 path rewriting can be disabled with --keep-module-paths.""") 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_module_index_pages(): # Generate all index pages. Passing --modules will generate more than one. write_toplevel_index() # Maps each module title to a set of Symbols in the module module2syms = collections.defaultdict(set) # Symbols that do not appear in any module non_module_syms = 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 None: non_module_syms.add(node.item) else: module2syms[mod_title].add(node.item) # Write the index-main.rst index page, which lists the symbols that aren't # from a module write_index_page(non_module_syms, non_module_title, "main", desc_from_file(non_module_index_desc)) # Write the index-<suffix>.rst index pages, which list symbols from # modules. Iterate 'modules' instead of 'module2syms' so that an index page # gets written even if a module has no symbols, for consistency. for title, suffix, _, text in modules: write_index_page(module2syms[title], title, suffix, text) # Write the index-all.rst index page, which lists all symbols, including # both module and non-module symbols write_index_page(kconf.unique_defined_syms, "All", "all", desc_from_file(all_index_desc)) def write_toplevel_index(): # Used in --modules mode. Writes an index.rst with a TOC tree that links to # index-main.rst and the index-<suffix>.rst pages. rst = index_page_header(None, None, desc_from_file(top_index_desc)) + """ Subsystems ********** .. toctree:: :maxdepth: 1 """ rst += " index-main\n" for _, suffix, _, _ in modules: rst += " index-{}\n".format(suffix) rst += " index-all\n" write_if_updated("index.rst", rst) def write_index_page(syms, title, suffix, text): # Writes an index page for the Symbols in 'syms' to 'index-<suffix>.rst' # (or index.rst if 'suffix' is None). 'title', 'suffix', and 'text' are # also used for to generate the index page header. See index_page_header(). rst = index_page_header(title, suffix, text) rst += """ Configuration symbols ********************* .. list-table:: Alphabetized Index of Configuration Options :header-rows: 1 * - Kconfig Symbol - Description """ for sym in sorted(syms, key=attrgetter("name")): # Add an index entry for the symbol that links to its page. Also list # its prompt(s), if any. (A symbol can have multiple prompts if it has # multiple definitions.) rst += " * - :option:`CONFIG_{}`\n - {}\n".format( sym.name, " / ".join(node.prompt[0] for node in sym.nodes if node.prompt)) if suffix is None: fname = "index.rst" else: fname = "index-{}.rst".format(suffix) write_if_updated(fname, rst) def index_page_header(title, link, description): # write_index_page() helper. Returns the RST for the beginning of a symbol # index page. # # title: # String used for the page title, as '<title> Configuration Options'. If # None, just 'Configuration Options' is used as the title. # # link: # String used for link target, as 'configuration_options_<link>'. If # None, the link will be 'configuration_options'. # # description: # RST put into an Overview section at the beginning of the page if title is None: title = "Configuration Options" else: title = title + " Configuration Options" if link is None: link = "configuration_options" else: link = "configuration_options_" + link title += "\n" + len(title)*"=" return """\ .. _{}: {} Overview ******** {} This documentation is generated automatically from the :file:`Kconfig` files by the :file:`{}` script. Click on symbols for more information. """.format(link, title, description, os.path.basename(__file__)) DEFAULT_INDEX_DESCRIPTION = """\ :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 desc_from_file(fname): # Helper for loading files with descriptions for index pages. Returns # DEFAULT_INDEX_DESCRIPTION if 'fname' is None, and the contents of the # file otherwise. if fname is None: return DEFAULT_INDEX_DESCRIPTION try: with open(fname, "r", encoding="utf-8") as f: return f.read() except OSError as e: sys.exit("error: failed to open index description file '{}': {}" .format(fname, e)) def write_sym_pages(): # Generates 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, to speed 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 += ".. option:: CONFIG_{}\n".format(sym.name) 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("CONFIG_{}.rst".format(sym.name), 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" \ ".. title:: {0}\n\n" \ ".. option:: CONFIG_{0}\n\n" \ "{1}\n\n" \ "Type: ``{2}``\n\n" \ .format(sym.name, prompt_rst(sym), kconfiglib.TYPE_TO_STR[sym.type]) def choice_header_rst(choice): # Returns RST that appears at the top of choice reference pages return ":orphan:\n\n" \ ".. title:: {0}\n\n" \ ".. _{1}:\n\n" \ ".. describe:: {0}\n\n" \ "{2}\n\n" \ "Type: ``{3}``\n\n" \ .format(choice_desc(choice), choice_id(choice), prompt_rst(choice), kconfiglib.TYPE_TO_STR[choice.type]) def prompt_rst(sc): # Returns RST that lists the prompts of 'sc' (symbol or choice) return "\n\n".join("*{}*".format(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" \ "{}\n\n" \ .format(node.help) 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" \ "{}\n\n" \ "*(Includes any dependencies from ifs and menus.)*\n\n" \ .format(expr_str(sc.direct_dep)) 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 = "{}\n{}\n\n".format(heading, len(heading)*"=") 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 += "- {}\n".format(expr_str(sym)) 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 = "Symbols {} by this symbol".format(type_str) rst += "{}\n{}\n\n".format(heading, len(heading)*"=") 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 = "Symbols that {} this symbol".format(type_str) rst += "{}\n{}\n\n".format(heading, len(heading)*"=") # 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("``{}:{}``".format(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 = "{}\n{}\n\n".format(heading, len(heading)*"=") rst += ".. highlight:: kconfig" for node in sc.nodes: rst += "\n\n" \ "At ``{}:{}``\n\n" \ "{}" \ "Menu path: {}\n\n" \ ".. parsed-literal::\n\n{}" \ .format(strip_module_path(node.filename), node.linenr, include_path(node), menu_path(node), 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 "choice_{}".format(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 "<{}>{}{}".format(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()