| """ |
| Kconfig Extension |
| ################# |
| |
| Copyright (c) 2022 Nordic Semiconductor ASA |
| SPDX-License-Identifier: Apache-2.0 |
| |
| Introduction |
| ============ |
| |
| This extension adds a new domain (``kconfig``) for the Kconfig language. Unlike |
| many other domains, the Kconfig options are not rendered by Sphinx directly but |
| on the client side using a database built by the extension. A special directive |
| ``.. kconfig:search::`` can be inserted on any page to render a search box that |
| allows to browse the database. References to Kconfig options can be created by |
| using the ``:kconfig:option:`` role. Kconfig options behave as regular domain |
| objects, so they can also be referenced by other projects using Intersphinx. |
| |
| Options |
| ======= |
| |
| - kconfig_generate_db: Set to True if you want to generate the Kconfig database. |
| This is only required if you want to use the ``.. kconfig:search::`` |
| directive, not if you just need support for Kconfig domain (e.g. when using |
| Intersphinx in another project). Defaults to False. |
| - kconfig_ext_paths: A list of base paths where to search for external modules |
| Kconfig files when they use ``kconfig-ext: True``. The extension will look for |
| ${BASE_PATH}/modules/${MODULE_NAME}/Kconfig. |
| """ |
| |
| from distutils.command.build import build |
| from itertools import chain |
| import json |
| from operator import mod |
| import os |
| from pathlib import Path |
| import re |
| import sys |
| from tempfile import TemporaryDirectory |
| from typing import Any, Dict, Iterable, List, Optional, Tuple |
| |
| from docutils import nodes |
| from sphinx.addnodes import pending_xref |
| from sphinx.application import Sphinx |
| from sphinx.builders import Builder |
| from sphinx.domains import Domain, ObjType |
| from sphinx.environment import BuildEnvironment |
| from sphinx.errors import ExtensionError |
| from sphinx.roles import XRefRole |
| from sphinx.util import progress_message |
| from sphinx.util.docutils import SphinxDirective |
| from sphinx.util.nodes import make_refnode |
| |
| |
| __version__ = "0.1.0" |
| |
| |
| RESOURCES_DIR = Path(__file__).parent / "static" |
| ZEPHYR_BASE = Path(__file__).parents[4] |
| |
| SCRIPTS = ZEPHYR_BASE / "scripts" |
| sys.path.insert(0, str(SCRIPTS)) |
| |
| KCONFIGLIB = SCRIPTS / "kconfig" |
| sys.path.insert(0, str(KCONFIGLIB)) |
| |
| import zephyr_module |
| import kconfiglib |
| |
| |
| def kconfig_load(app: Sphinx) -> Tuple[kconfiglib.Kconfig, Dict[str, str]]: |
| """Load Kconfig""" |
| with TemporaryDirectory() as td: |
| modules = zephyr_module.parse_modules(ZEPHYR_BASE) |
| |
| # generate Kconfig.modules file |
| kconfig = "" |
| for module in modules: |
| kconfig += zephyr_module.process_kconfig(module.project, module.meta) |
| |
| with open(Path(td) / "Kconfig.modules", "w") as f: |
| f.write(kconfig) |
| |
| # generate dummy Kconfig.dts file |
| kconfig = "" |
| |
| with open(Path(td) / "Kconfig.dts", "w") as f: |
| f.write(kconfig) |
| |
| # base environment |
| os.environ["ZEPHYR_BASE"] = str(ZEPHYR_BASE) |
| os.environ["srctree"] = str(ZEPHYR_BASE) |
| os.environ["KCONFIG_DOC_MODE"] = "1" |
| os.environ["KCONFIG_BINARY_DIR"] = td |
| |
| # include all archs and boards |
| os.environ["ARCH_DIR"] = "arch" |
| os.environ["ARCH"] = "*" |
| os.environ["BOARD_DIR"] = "boards/*/*" |
| |
| # insert external Kconfigs to the environment |
| module_paths = dict() |
| for module in modules: |
| name = module.meta["name"] |
| name_var = module.meta["name-sanitized"].upper() |
| module_paths[name] = module.project |
| |
| build_conf = module.meta.get("build") |
| if not build_conf: |
| continue |
| |
| if build_conf.get("kconfig"): |
| kconfig = Path(module.project) / build_conf["kconfig"] |
| os.environ[f"ZEPHYR_{name_var}_KCONFIG"] = str(kconfig) |
| elif build_conf.get("kconfig-ext"): |
| for path in app.config.kconfig_ext_paths: |
| kconfig = Path(path) / "modules" / name / "Kconfig" |
| if kconfig.exists(): |
| os.environ[f"ZEPHYR_{name_var}_KCONFIG"] = str(kconfig) |
| |
| return kconfiglib.Kconfig(ZEPHYR_BASE / "Kconfig"), module_paths |
| |
| |
| class KconfigSearchNode(nodes.Element): |
| @staticmethod |
| def html(): |
| return '<div id="__kconfig-search"></div>' |
| |
| |
| def kconfig_search_visit_html(self, node: nodes.Node) -> None: |
| self.body.append(node.html()) |
| raise nodes.SkipNode |
| |
| |
| def kconfig_search_visit_latex(self, node: nodes.Node) -> None: |
| self.body.append("Kconfig search is only available on HTML output") |
| raise nodes.SkipNode |
| |
| |
| class KconfigSearch(SphinxDirective): |
| """Kconfig search directive""" |
| |
| has_content = False |
| |
| def run(self): |
| if not self.config.kconfig_generate_db: |
| raise ExtensionError( |
| "Kconfig search directive can not be used without database" |
| ) |
| |
| if "kconfig_search_inserted" in self.env.temp_data: |
| raise ExtensionError("Kconfig search directive can only be used once") |
| |
| self.env.temp_data["kconfig_search_inserted"] = True |
| |
| # register all options to the domain at this point, so that they all |
| # resolve to the page where the kconfig:search directive is inserted |
| domain = self.env.get_domain("kconfig") |
| unique = set({option["name"] for option in self.env.kconfig_db}) |
| for option in unique: |
| domain.add_option(option) |
| |
| return [KconfigSearchNode()] |
| |
| |
| class _FindKconfigSearchDirectiveVisitor(nodes.NodeVisitor): |
| def __init__(self, document): |
| super().__init__(document) |
| self._found = False |
| |
| def unknown_visit(self, node: nodes.Node) -> None: |
| if self._found: |
| return |
| |
| self._found = isinstance(node, KconfigSearchNode) |
| |
| @property |
| def found_kconfig_search_directive(self) -> bool: |
| return self._found |
| |
| |
| class KconfigDomain(Domain): |
| """Kconfig domain""" |
| |
| name = "kconfig" |
| label = "Kconfig" |
| object_types = {"option": ObjType("option", "option")} |
| roles = {"option": XRefRole()} |
| directives = {"search": KconfigSearch} |
| initial_data: Dict[str, Any] = {"options": []} |
| |
| def get_objects(self) -> Iterable[Tuple[str, str, str, str, str, int]]: |
| for obj in self.data["options"]: |
| yield obj |
| |
| def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: |
| self.data["options"] += otherdata["options"] |
| |
| def resolve_xref( |
| self, |
| env: BuildEnvironment, |
| fromdocname: str, |
| builder: Builder, |
| typ: str, |
| target: str, |
| node: pending_xref, |
| contnode: nodes.Element, |
| ) -> Optional[nodes.Element]: |
| match = [ |
| (docname, anchor) |
| for name, _, _, docname, anchor, _ in self.get_objects() |
| if name == target |
| ] |
| |
| if match: |
| todocname, anchor = match[0] |
| |
| return make_refnode( |
| builder, fromdocname, todocname, anchor, contnode, anchor |
| ) |
| else: |
| return None |
| |
| def add_option(self, option): |
| """Register a new Kconfig option to the domain.""" |
| |
| self.data["options"].append( |
| (option, option, "option", self.env.docname, option, -1) |
| ) |
| |
| |
| def sc_fmt(sc): |
| if isinstance(sc, kconfiglib.Symbol): |
| if sc.nodes: |
| return f'<a href="#CONFIG_{sc.name}">CONFIG_{sc.name}</a>' |
| elif isinstance(sc, kconfiglib.Choice): |
| if not sc.name: |
| return "<choice>" |
| return f'<choice <a href="#CONFIG_{sc.name}">CONFIG_{sc.name}</a>>' |
| |
| return kconfiglib.standard_sc_expr_str(sc) |
| |
| |
| def kconfig_build_resources(app: Sphinx) -> None: |
| """Build the Kconfig database and install HTML resources.""" |
| |
| if not app.config.kconfig_generate_db: |
| return |
| |
| with progress_message("Building Kconfig database..."): |
| kconfig, module_paths = kconfig_load(app) |
| db = list() |
| |
| for sc in sorted( |
| chain(kconfig.unique_defined_syms, kconfig.unique_choices), |
| key=lambda sc: sc.name if sc.name else "", |
| ): |
| # skip nameless symbols |
| if not sc.name: |
| continue |
| |
| # store alternative defaults (from defconfig files) |
| alt_defaults = list() |
| for node in sc.nodes: |
| if "defconfig" not in node.filename: |
| continue |
| |
| for value, cond in node.orig_defaults: |
| fmt = kconfiglib.expr_str(value, sc_fmt) |
| if cond is not sc.kconfig.y: |
| fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" |
| alt_defaults.append([fmt, node.filename]) |
| |
| # build list of symbols that select/imply the current one |
| # note: all reverse dependencies are ORed together, and conditionals |
| # (e.g. select/imply A if B) turns into A && B. So we first split |
| # by OR to include all entries, and we split each one by AND to just |
| # take the first entry. |
| selected_by = list() |
| if isinstance(sc, kconfiglib.Symbol) and sc.rev_dep != sc.kconfig.n: |
| for select in kconfiglib.split_expr(sc.rev_dep, kconfiglib.OR): |
| sym = kconfiglib.split_expr(select, kconfiglib.AND)[0] |
| selected_by.append(f"CONFIG_{sym.name}") |
| |
| implied_by = list() |
| if isinstance(sc, kconfiglib.Symbol) and sc.weak_rev_dep != sc.kconfig.n: |
| for select in kconfiglib.split_expr(sc.weak_rev_dep, kconfiglib.OR): |
| sym = kconfiglib.split_expr(select, kconfiglib.AND)[0] |
| implied_by.append(f"CONFIG_{sym.name}") |
| |
| # only process nodes with prompt or help |
| nodes = [node for node in sc.nodes if node.prompt or node.help] |
| |
| inserted_paths = list() |
| for node in nodes: |
| # avoid duplicate symbols by forcing unique paths. this can |
| # happen due to dependencies on 0, a trick used by some modules |
| path = f"{node.filename}:{node.linenr}" |
| if path in inserted_paths: |
| continue |
| inserted_paths.append(path) |
| |
| dependencies = None |
| if node.dep is not sc.kconfig.y: |
| dependencies = kconfiglib.expr_str(node.dep, sc_fmt) |
| |
| defaults = list() |
| for value, cond in node.orig_defaults: |
| fmt = kconfiglib.expr_str(value, sc_fmt) |
| if cond is not sc.kconfig.y: |
| fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" |
| defaults.append(fmt) |
| |
| selects = list() |
| for value, cond in node.orig_selects: |
| fmt = kconfiglib.expr_str(value, sc_fmt) |
| if cond is not sc.kconfig.y: |
| fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" |
| selects.append(fmt) |
| |
| implies = list() |
| for value, cond in node.orig_implies: |
| fmt = kconfiglib.expr_str(value, sc_fmt) |
| if cond is not sc.kconfig.y: |
| fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" |
| implies.append(fmt) |
| |
| ranges = list() |
| for min, max, cond in node.orig_ranges: |
| fmt = ( |
| f"[{kconfiglib.expr_str(min, sc_fmt)}, " |
| f"{kconfiglib.expr_str(max, sc_fmt)}]" |
| ) |
| if cond is not sc.kconfig.y: |
| fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" |
| ranges.append(fmt) |
| |
| choices = list() |
| if isinstance(sc, kconfiglib.Choice): |
| for sym in sc.syms: |
| choices.append(kconfiglib.expr_str(sym, sc_fmt)) |
| |
| menupath = "" |
| iternode = node |
| while iternode.parent is not iternode.kconfig.top_node: |
| iternode = iternode.parent |
| if iternode.prompt: |
| title = iternode.prompt[0] |
| else: |
| title = kconfiglib.standard_sc_expr_str(iternode.item) |
| menupath = f" > {title}" + menupath |
| |
| menupath = "(Top)" + menupath |
| |
| filename = node.filename |
| for name, path in module_paths.items(): |
| if node.filename.startswith(path): |
| filename = node.filename.replace(path, f"<module:{name}>") |
| break |
| |
| db.append( |
| { |
| "name": f"CONFIG_{sc.name}", |
| "prompt": node.prompt[0] if node.prompt else None, |
| "type": kconfiglib.TYPE_TO_STR[sc.type], |
| "help": node.help, |
| "dependencies": dependencies, |
| "defaults": defaults, |
| "alt_defaults": alt_defaults, |
| "selects": selects, |
| "selected_by": selected_by, |
| "implies": implies, |
| "implied_by": implied_by, |
| "ranges": ranges, |
| "choices": choices, |
| "filename": filename, |
| "linenr": node.linenr, |
| "menupath": menupath, |
| } |
| ) |
| |
| app.env.kconfig_db = db # type: ignore |
| |
| outdir = Path(app.outdir) / "kconfig" |
| outdir.mkdir(exist_ok=True) |
| |
| kconfig_db_file = outdir / "kconfig.json" |
| |
| with open(kconfig_db_file, "w") as f: |
| json.dump(db, f) |
| |
| app.config.html_extra_path.append(kconfig_db_file.as_posix()) |
| app.config.html_static_path.append(RESOURCES_DIR.as_posix()) |
| |
| |
| def kconfig_install( |
| app: Sphinx, |
| pagename: str, |
| templatename: str, |
| context: Dict, |
| doctree: Optional[nodes.Node], |
| ) -> None: |
| """Install the Kconfig library files on pages that require it.""" |
| if ( |
| not app.config.kconfig_generate_db |
| or app.builder.format != "html" |
| or not doctree |
| ): |
| return |
| |
| visitor = _FindKconfigSearchDirectiveVisitor(doctree) |
| doctree.walk(visitor) |
| if visitor.found_kconfig_search_directive: |
| app.add_css_file("kconfig.css") |
| app.add_js_file("kconfig.mjs", type="module") |
| |
| |
| def setup(app: Sphinx): |
| app.add_config_value("kconfig_generate_db", False, "env") |
| app.add_config_value("kconfig_ext_paths", [], "env") |
| |
| app.add_node( |
| KconfigSearchNode, |
| html=(kconfig_search_visit_html, None), |
| latex=(kconfig_search_visit_latex, None), |
| ) |
| |
| app.add_domain(KconfigDomain) |
| |
| app.connect("builder-inited", kconfig_build_resources) |
| app.connect("html-page-context", kconfig_install) |
| |
| return { |
| "version": __version__, |
| "parallel_read_safe": True, |
| "parallel_write_safe": True, |
| } |