blob: 5aefd2e5945f1e8af082be68b8e1d3fe73b012a8 [file] [log] [blame]
"""
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} <CONFIG_{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 (<title>, <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()