From 09c813793bb6f11caea76db39cc97bf40d84cc0d Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Thu, 17 May 2018 20:10:30 +0200 Subject: [PATCH] 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 --- doc/scripts/genrest/genrest.py | 237 ++++++++++++++++++--------------- 1 file changed, 128 insertions(+), 109 deletions(-) diff --git a/doc/scripts/genrest/genrest.py b/doc/scripts/genrest/genrest.py index 02c2a855b60..28a61a1e851 100644 --- a/doc/scripts/genrest/genrest.py +++ b/doc/scripts/genrest/genrest.py @@ -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 /CONFIG_.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. ). 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()