doc: genrest: Speed up documentation rebuilding

Skip writing index and symbol RST files whose content hasn't changed, to
avoid updating their timestamps. This makes documentation rebuilds much
faster, as Sphinx looks at the timestamp to determine if an RST file has
been updated.

Rebuilding docs with symbol reference up-to-date, before:

    $ time make html
    real    4m52.838s
    user    4m46.242s
    sys     0m4.249s

After:

    $ time make html
    real    0m48.731s
    user    0m47.571s
    sys     0m0.908s

Testing was done with 'make VERBOSE=1 SPHINXOPTS= html', suggested by
Marti Bolivar.

Piggyback a small cleanup to the code generating the select/imply
information.

Signed-off-by: Ulf Magnusson <Ulf.Magnusson@nordicsemi.no>
This commit is contained in:
Ulf Magnusson 2018-05-17 20:10:30 +02:00 committed by Anas Nashif
parent 3c09bee921
commit 09c813793b

View File

@ -2,11 +2,13 @@
# CONFIG_FOO.rst file for each symbol, and an alphabetical index with links in
# index.rst.
import kconfiglib
import errno
import os
import sys
import textwrap
import kconfiglib
# "Extend" the standard kconfiglib.expr_str() to turn references to defined
# Kconfig symbols into RST links. Symbol.__str__() will then use the extended
# version.
@ -71,142 +73,159 @@ def write_kconfig_rst():
kconf = kconfiglib.Kconfig(sys.argv[1])
out_dir = sys.argv[2]
with open(os.path.join(out_dir, "index.rst"), "w") as index_rst:
index_rst.write(INDEX_RST_HEADER)
# String with the RST for the index page
index_rst = INDEX_RST_HEADER
# - Sort the symbols by name so that they end up in sorted order in
# index.rst
# - Sort the symbols by name so that they end up in sorted order in
# index.rst
#
# - Use set() to get rid of duplicates for symbols defined in multiple
# locations.
for sym in sorted(set(kconf.defined_syms), key=lambda sym: sym.name):
# Write an RST file for the symbol
write_sym_rst(sym, out_dir)
# Add an index entry for the symbol that links to its RST file. Also
# list its prompt(s), if any. (A symbol can have multiple prompts if it
# has multiple definitions.)
#
# - Use set() to get rid of duplicates for symbols defined in multiple
# locations.
for sym in sorted(set(kconf.defined_syms), key=lambda sym: sym.name):
# Write an RST file for the symbol
write_sym_rst(sym, out_dir)
# The strip() avoids RST choking on stuff like *foo *, when people
# accidentally include leading/trailing whitespace in prompts.
index_rst += " * - :option:`CONFIG_{}`\n - {}\n".format(
sym.name,
" / ".join(node.prompt[0].strip()
for node in sym.nodes if node.prompt))
# Add an index entry for the symbol that links to its RST file.
# Also list its prompt(s), if any. (A symbol can have multiple
# prompts if it has multiple definitions.)
#
# The strip() avoids RST choking on stuff like *foo *, when people
# accidentally include leading/trailing whitespace in prompts.
index_rst.write(" * - :option:`CONFIG_{}`\n - {}\n"
.format(sym.name,
" / ".join(node.prompt[0].strip()
for node in sym.nodes
if node.prompt)))
write_if_updated(os.path.join(out_dir, "index.rst"), index_rst)
def write_sym_rst(sym, out_dir):
# Writes documentation for 'sym' to <out_dir>/CONFIG_<sym.name>.rst
kconf = sym.kconfig
with open(os.path.join(out_dir, "CONFIG_{}.rst".format(sym.name)),
"w") as sym_rst:
# List all prompts on separate lines
prompt_str = "\n\n".join("*{}*".format(node.prompt[0].strip())
for node in sym.nodes if node.prompt) \
or "*(No prompt -- not directly user assignable.)*"
# List all prompts on separate lines
prompt_str = "\n\n".join("*{}*".format(node.prompt[0].strip())
for node in sym.nodes if node.prompt) \
or "*(No prompt -- not directly user assignable.)*"
# String with the RST for the symbol page
#
# - :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.
sym_rst = ":orphan:\n\n" \
".. title:: {0}\n\n" \
".. option:: CONFIG_{0}\n\n" \
"{1}\n\n" \
"Type: ``{2}``\n\n" \
.format(sym.name, prompt_str, kconfiglib.TYPE_TO_STR[sym.type])
# - :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.
sym_rst.write(":orphan:\n\n"
".. title:: {0}\n\n"
".. option:: CONFIG_{0}\n\n"
"{1}\n\n"
"Type: ``{2}``\n\n"
.format(sym.name,
prompt_str,
kconfiglib.TYPE_TO_STR[sym.type]))
# Symbols with multiple definitions can have multiple help texts
for node in sym.nodes:
if node.help is not None:
sym_rst += "Help\n" \
"====\n\n" \
"{}\n\n" \
.format(node.help)
# Symbols with multiple definitions can have multiple help texts
for node in sym.nodes:
if node.help is not None:
sym_rst.write("Help\n"
"====\n\n"
"{}\n\n"
.format(node.help))
if sym.direct_dep is not kconf.y:
sym_rst += "Direct dependencies\n" \
"===================\n\n" \
"{}\n\n" \
"*(Includes any dependencies from if's and menus.)*\n\n" \
.format(kconfiglib.expr_str(sym.direct_dep))
if sym.direct_dep is not kconf.y:
sym_rst.write("Direct dependencies\n"
"===================\n\n"
"{}\n\n"
"*(Includes any dependencies from if's and menus.)*\n\n"
.format(kconfiglib.expr_str(sym.direct_dep)))
if sym.defaults:
sym_rst += "Defaults\n" \
"========\n\n"
if sym.defaults:
sym_rst.write("Defaults\n"
"========\n\n")
for value, cond in sym.defaults:
default_str = kconfiglib.expr_str(value)
if cond is not kconf.y:
default_str += " if " + kconfiglib.expr_str(cond)
sym_rst += " - {}\n".format(default_str)
for value, cond in sym.defaults:
default_str = kconfiglib.expr_str(value)
if cond is not kconf.y:
default_str += " if " + kconfiglib.expr_str(cond)
sym_rst.write(" - {}\n".format(default_str))
sym_rst += "\n"
sym_rst.write("\n")
def add_select_imply_rst(type_str, expr):
# Writes a link for each selecting symbol (if 'expr' is sym.rev_dep) or
# each implying symbol (if 'expr' is sym.weak_rev_dep). Also adds a
# heading at the top, derived from type_str ("select"/"imply").
def write_select_imply_rst(expr):
# Writes a link for each selecting symbol (if 'expr' is
# sym.rev_dep) or each implying symbol (if 'expr' is
# sym.weak_rev_dep)
nonlocal sym_rst
# 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.
sym_rst.write(" - :option:`CONFIG_{}`\n".format(
kconfiglib.split_expr(select, kconfiglib.AND)[0].name))
heading = "Symbols that ``{}`` this symbol".format(type_str)
sym_rst += "{}\n{}\n\n".format(heading, len(heading)*"=")
sym_rst.write("\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.
sym_rst += " - :option:`CONFIG_{}`\n".format(
kconfiglib.split_expr(select, kconfiglib.AND)[0].name)
if sym.rev_dep is not kconf.n:
sym_rst.write("Symbols that ``select`` this symbol\n"
"===================================\n\n")
write_select_imply_rst(sym.rev_dep)
sym_rst += "\n"
if sym.weak_rev_dep is not kconf.n:
sym_rst.write("Symbols that ``imply`` this symbol\n"
"==================================\n\n")
write_select_imply_rst(sym.weak_rev_dep)
if sym.rev_dep is not kconf.n:
add_select_imply_rst("select", sym.rev_dep)
def menu_path(node):
path = ""
if sym.weak_rev_dep is not kconf.n:
add_select_imply_rst("imply", sym.weak_rev_dep)
menu = node.parent
while menu is not kconf.top_node:
# Fancy Unicode arrow. Added in '93, so ought to be pretty
# safe.
path = "" + menu.prompt[0] + path
menu = menu.parent
def menu_path(node):
path = ""
# The strip() avoids RST choking on leading/trailing whitespace in
# prompts
return ("(top menu)" + path).strip()
menu = node.parent
while menu is not kconf.top_node:
# Fancy Unicode arrow. Added in '93, so ought to be pretty
# safe.
path = "" + menu.prompt[0] + path
menu = menu.parent
heading = "Kconfig definition"
if len(sym.nodes) > 1:
heading += "s"
# The strip() avoids RST choking on leading/trailing whitespace in
# prompts
return ("(top menu)" + path).strip()
# Add ==... below heading. Adding too many '=' would be okay too, but
# this looks a bit neater in the RST file.
sym_rst.write("{}\n{}\n\n".format(heading, len(heading)*"="))
heading = "Kconfig definition"
if len(sym.nodes) > 1:
heading += "s"
sym_rst += "{}\n{}\n\n".format(heading, len(heading)*"=")
sym_rst.write("\n\n".join(
"At ``{}:{}``, in menu ``{}``:\n\n"
".. parsed-literal::\n\n"
"{}".format(node.filename, node.linenr, menu_path(node),
textwrap.indent(str(node), " "*4))
for node in sym.nodes))
sym_rst += "\n\n".join(
"At ``{}:{}``, in menu ``{}``:\n\n"
".. parsed-literal::\n\n"
"{}".format(node.filename, node.linenr, menu_path(node),
textwrap.indent(str(node), " "*4))
for node in sym.nodes)
sym_rst += "\n\n*(Definitions include propagated dependencies, " \
"including from if's and menus.)*"
write_if_updated(os.path.join(out_dir, "CONFIG_{}.rst".format(sym.name)),
sym_rst)
def write_if_updated(filename, s):
# Writes 's' as the contents of 'filename', but only if it differs from the
# current contents of the file. This avoids unnecessary timestamp updates,
# which trigger documentation rebuilds.
try:
with open(filename) as f:
if s == f.read():
return
except OSError as e:
if e.errno != errno.ENOENT:
raise
with open(filename, "w") as f:
f.write(s)
sym_rst.write("\n\n*(Definitions include propagated dependencies, "
"including from if's and menus.)*")
if __name__ == "__main__":
write_kconfig_rst()