1""" 2Kconfig Extension 3################# 4 5Copyright (c) 2022 Nordic Semiconductor ASA 6SPDX-License-Identifier: Apache-2.0 7 8Introduction 9============ 10 11This extension adds a new domain (``kconfig``) for the Kconfig language. Unlike 12many other domains, the Kconfig options are not rendered by Sphinx directly but 13on the client side using a database built by the extension. A special directive 14``.. kconfig:search::`` can be inserted on any page to render a search box that 15allows to browse the database. References to Kconfig options can be created by 16using the ``:kconfig:option:`` role. Kconfig options behave as regular domain 17objects, so they can also be referenced by other projects using Intersphinx. 18 19Options 20======= 21 22- kconfig_generate_db: Set to True if you want to generate the Kconfig database. 23 This is only required if you want to use the ``.. kconfig:search::`` 24 directive, not if you just need support for Kconfig domain (e.g. when using 25 Intersphinx in another project). Defaults to False. 26- kconfig_ext_paths: A list of base paths where to search for external modules 27 Kconfig files when they use ``kconfig-ext: True``. The extension will look for 28 ${BASE_PATH}/modules/${MODULE_NAME}/Kconfig. 29""" 30 31import argparse 32import json 33import os 34import re 35import sys 36from collections.abc import Iterable 37from itertools import chain 38from pathlib import Path 39from tempfile import TemporaryDirectory 40from typing import Any 41 42from docutils import nodes 43from sphinx.addnodes import pending_xref 44from sphinx.application import Sphinx 45from sphinx.builders import Builder 46from sphinx.domains import Domain, ObjType 47from sphinx.environment import BuildEnvironment 48from sphinx.errors import ExtensionError 49from sphinx.roles import XRefRole 50from sphinx.util.display import progress_message 51from sphinx.util.docutils import SphinxDirective 52from sphinx.util.nodes import make_refnode 53 54__version__ = "0.1.0" 55 56 57sys.path.insert(0, str(Path(__file__).parents[4] / "scripts")) 58sys.path.insert(0, str(Path(__file__).parents[4] / "scripts/kconfig")) 59 60import kconfiglib 61import list_boards 62import list_hardware 63import zephyr_module 64 65RESOURCES_DIR = Path(__file__).parent / "static" 66ZEPHYR_BASE = Path(__file__).parents[4] 67 68 69def kconfig_load(app: Sphinx) -> tuple[kconfiglib.Kconfig, dict[str, str]]: 70 """Load Kconfig""" 71 with TemporaryDirectory() as td: 72 modules = zephyr_module.parse_modules(ZEPHYR_BASE) 73 74 # generate Kconfig.modules file 75 kconfig = "" 76 for module in modules: 77 kconfig += zephyr_module.process_kconfig(module.project, module.meta) 78 79 with open(Path(td) / "Kconfig.modules", "w") as f: 80 f.write(kconfig) 81 82 # generate dummy Kconfig.dts file 83 kconfig = "" 84 85 with open(Path(td) / "Kconfig.dts", "w") as f: 86 f.write(kconfig) 87 88 (Path(td) / 'soc').mkdir(exist_ok=True) 89 root_args = argparse.Namespace(**{'soc_roots': [Path(ZEPHYR_BASE)]}) 90 v2_systems = list_hardware.find_v2_systems(root_args) 91 92 soc_folders = {soc.folder[0] for soc in v2_systems.get_socs()} 93 with open(Path(td) / "soc" / "Kconfig.defconfig", "w") as f: 94 f.write('') 95 96 with open(Path(td) / "soc" / "Kconfig.soc", "w") as f: 97 for folder in soc_folders: 98 f.write('source "' + (Path(folder) / 'Kconfig.soc').as_posix() + '"\n') 99 100 with open(Path(td) / "soc" / "Kconfig", "w") as f: 101 for folder in soc_folders: 102 f.write('osource "' + (Path(folder) / 'Kconfig').as_posix() + '"\n') 103 104 (Path(td) / 'arch').mkdir(exist_ok=True) 105 root_args = argparse.Namespace(**{'arch_roots': [Path(ZEPHYR_BASE)], 'arch': None}) 106 v2_archs = list_hardware.find_v2_archs(root_args) 107 kconfig = "" 108 for arch in v2_archs['archs']: 109 kconfig += 'source "' + (Path(arch['path']) / 'Kconfig').as_posix() + '"\n' 110 with open(Path(td) / "arch" / "Kconfig", "w") as f: 111 f.write(kconfig) 112 113 (Path(td) / 'boards').mkdir(exist_ok=True) 114 root_args = argparse.Namespace(**{'board_roots': [Path(ZEPHYR_BASE)], 115 'soc_roots': [Path(ZEPHYR_BASE)], 'board': None, 116 'board_dir': []}) 117 v2_boards = list_boards.find_v2_boards(root_args).values() 118 119 with open(Path(td) / "boards" / "Kconfig.boards", "w") as f: 120 for board in v2_boards: 121 board_str = 'BOARD_' + re.sub(r"[^a-zA-Z0-9_]", "_", board.name).upper() 122 f.write('config ' + board_str + '\n') 123 f.write('\t bool\n') 124 for qualifier in list_boards.board_v2_qualifiers(board): 125 board_str = 'BOARD_' + re.sub(r"[^a-zA-Z0-9_]", "_", qualifier).upper() 126 f.write('config ' + board_str + '\n') 127 f.write('\t bool\n') 128 f.write('source "' + (board.dir / ('Kconfig.' + board.name)).as_posix() + '"\n\n') 129 130 # base environment 131 os.environ["ZEPHYR_BASE"] = str(ZEPHYR_BASE) 132 os.environ["srctree"] = str(ZEPHYR_BASE) # noqa: SIM112 133 os.environ["KCONFIG_DOC_MODE"] = "1" 134 os.environ["KCONFIG_BINARY_DIR"] = td 135 136 # include all archs and boards 137 os.environ["ARCH_DIR"] = "arch" 138 os.environ["ARCH"] = "[!v][!2]*" 139 os.environ["HWM_SCHEME"] = "v2" 140 141 os.environ["BOARD"] = "boards" 142 os.environ["KCONFIG_BOARD_DIR"] = str(Path(td) / "boards") 143 144 # insert external Kconfigs to the environment 145 module_paths = dict() 146 for module in modules: 147 name = module.meta["name"] 148 name_var = module.meta["name-sanitized"].upper() 149 module_paths[name] = module.project 150 151 build_conf = module.meta.get("build") 152 if not build_conf: 153 continue 154 155 if build_conf.get("kconfig"): 156 kconfig = Path(module.project) / build_conf["kconfig"] 157 os.environ[f"ZEPHYR_{name_var}_KCONFIG"] = str(kconfig) 158 elif build_conf.get("kconfig-ext"): 159 for path in app.config.kconfig_ext_paths: 160 kconfig = Path(path) / "modules" / name / "Kconfig" 161 if kconfig.exists(): 162 os.environ[f"ZEPHYR_{name_var}_KCONFIG"] = str(kconfig) 163 164 return kconfiglib.Kconfig(ZEPHYR_BASE / "Kconfig"), module_paths 165 166 167class KconfigSearchNode(nodes.Element): 168 @staticmethod 169 def html(): 170 return '<div id="__kconfig-search"></div>' 171 172 173def kconfig_search_visit_html(self, node: nodes.Node) -> None: 174 self.body.append(node.html()) 175 raise nodes.SkipNode 176 177 178def kconfig_search_visit_latex(self, node: nodes.Node) -> None: 179 self.body.append("Kconfig search is only available on HTML output") 180 raise nodes.SkipNode 181 182 183class KconfigSearch(SphinxDirective): 184 """Kconfig search directive""" 185 186 has_content = False 187 188 def run(self): 189 if not self.config.kconfig_generate_db: 190 raise ExtensionError( 191 "Kconfig search directive can not be used without database" 192 ) 193 194 if "kconfig_search_inserted" in self.env.temp_data: 195 raise ExtensionError("Kconfig search directive can only be used once") 196 197 self.env.temp_data["kconfig_search_inserted"] = True 198 199 # register all options to the domain at this point, so that they all 200 # resolve to the page where the kconfig:search directive is inserted 201 domain = self.env.get_domain("kconfig") 202 unique = set({option["name"] for option in self.env.kconfig_db}) 203 for option in unique: 204 domain.add_option(option) 205 206 return [KconfigSearchNode()] 207 208 209class _FindKconfigSearchDirectiveVisitor(nodes.NodeVisitor): 210 def __init__(self, document): 211 super().__init__(document) 212 self._found = False 213 214 def unknown_visit(self, node: nodes.Node) -> None: 215 if self._found: 216 return 217 218 self._found = isinstance(node, KconfigSearchNode) 219 220 @property 221 def found_kconfig_search_directive(self) -> bool: 222 return self._found 223 224 225class KconfigDomain(Domain): 226 """Kconfig domain""" 227 228 name = "kconfig" 229 label = "Kconfig" 230 object_types = {"option": ObjType("option", "option")} 231 roles = {"option": XRefRole()} 232 directives = {"search": KconfigSearch} 233 initial_data: dict[str, Any] = {"options": set()} 234 235 def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]: 236 yield from self.data["options"] 237 238 def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None: 239 self.data["options"].update(otherdata["options"]) 240 241 def resolve_xref( 242 self, 243 env: BuildEnvironment, 244 fromdocname: str, 245 builder: Builder, 246 typ: str, 247 target: str, 248 node: pending_xref, 249 contnode: nodes.Element, 250 ) -> nodes.Element | None: 251 match = [ 252 (docname, anchor) 253 for name, _, _, docname, anchor, _ in self.get_objects() 254 if name == target 255 ] 256 257 if match: 258 todocname, anchor = match[0] 259 260 return make_refnode( 261 builder, fromdocname, todocname, anchor, contnode, anchor 262 ) 263 else: 264 return None 265 266 def add_option(self, option): 267 """Register a new Kconfig option to the domain.""" 268 269 self.data["options"].add( 270 (option, option, "option", self.env.docname, option, 1) 271 ) 272 273 274def sc_fmt(sc): 275 if isinstance(sc, kconfiglib.Symbol): 276 if sc.nodes: 277 return f'<a href="#CONFIG_{sc.name}">CONFIG_{sc.name}</a>' 278 elif isinstance(sc, kconfiglib.Choice): 279 if not sc.name: 280 return "<choice>" 281 return f'<choice <a href="#CONFIG_{sc.name}">CONFIG_{sc.name}</a>>' 282 283 return kconfiglib.standard_sc_expr_str(sc) 284 285 286def kconfig_build_resources(app: Sphinx) -> None: 287 """Build the Kconfig database and install HTML resources.""" 288 289 if not app.config.kconfig_generate_db: 290 return 291 292 with progress_message("Building Kconfig database..."): 293 kconfig, module_paths = kconfig_load(app) 294 db = list() 295 296 for sc in sorted( 297 chain(kconfig.unique_defined_syms, kconfig.unique_choices), 298 key=lambda sc: sc.name if sc.name else "", 299 ): 300 # skip nameless symbols 301 if not sc.name: 302 continue 303 304 # store alternative defaults (from defconfig files) 305 alt_defaults = list() 306 for node in sc.nodes: 307 if "defconfig" not in node.filename: 308 continue 309 310 for value, cond in node.orig_defaults: 311 fmt = kconfiglib.expr_str(value, sc_fmt) 312 if cond is not sc.kconfig.y: 313 fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" 314 alt_defaults.append([fmt, node.filename]) 315 316 # build list of symbols that select/imply the current one 317 # note: all reverse dependencies are ORed together, and conditionals 318 # (e.g. select/imply A if B) turns into A && B. So we first split 319 # by OR to include all entries, and we split each one by AND to just 320 # take the first entry. 321 selected_by = list() 322 if isinstance(sc, kconfiglib.Symbol) and sc.rev_dep != sc.kconfig.n: 323 for select in kconfiglib.split_expr(sc.rev_dep, kconfiglib.OR): 324 sym = kconfiglib.split_expr(select, kconfiglib.AND)[0] 325 selected_by.append(f"CONFIG_{sym.name}") 326 327 implied_by = list() 328 if isinstance(sc, kconfiglib.Symbol) and sc.weak_rev_dep != sc.kconfig.n: 329 for select in kconfiglib.split_expr(sc.weak_rev_dep, kconfiglib.OR): 330 sym = kconfiglib.split_expr(select, kconfiglib.AND)[0] 331 implied_by.append(f"CONFIG_{sym.name}") 332 333 # only process nodes with prompt or help 334 nodes = [node for node in sc.nodes if node.prompt or node.help] 335 336 inserted_paths = list() 337 for node in nodes: 338 # avoid duplicate symbols by forcing unique paths. this can 339 # happen due to dependencies on 0, a trick used by some modules 340 path = f"{node.filename}:{node.linenr}" 341 if path in inserted_paths: 342 continue 343 inserted_paths.append(path) 344 345 dependencies = None 346 if node.dep is not sc.kconfig.y: 347 dependencies = kconfiglib.expr_str(node.dep, sc_fmt) 348 349 defaults = list() 350 for value, cond in node.orig_defaults: 351 fmt = kconfiglib.expr_str(value, sc_fmt) 352 if cond is not sc.kconfig.y: 353 fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" 354 defaults.append(fmt) 355 356 selects = list() 357 for value, cond in node.orig_selects: 358 fmt = kconfiglib.expr_str(value, sc_fmt) 359 if cond is not sc.kconfig.y: 360 fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" 361 selects.append(fmt) 362 363 implies = list() 364 for value, cond in node.orig_implies: 365 fmt = kconfiglib.expr_str(value, sc_fmt) 366 if cond is not sc.kconfig.y: 367 fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" 368 implies.append(fmt) 369 370 ranges = list() 371 for min, max, cond in node.orig_ranges: 372 fmt = ( 373 f"[{kconfiglib.expr_str(min, sc_fmt)}, " 374 f"{kconfiglib.expr_str(max, sc_fmt)}]" 375 ) 376 if cond is not sc.kconfig.y: 377 fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}" 378 ranges.append(fmt) 379 380 choices = list() 381 if isinstance(sc, kconfiglib.Choice): 382 for sym in sc.syms: 383 choices.append(kconfiglib.expr_str(sym, sc_fmt)) 384 385 menupath = "" 386 iternode = node 387 while iternode.parent is not iternode.kconfig.top_node: 388 iternode = iternode.parent 389 if iternode.prompt: 390 title = iternode.prompt[0] 391 else: 392 title = kconfiglib.standard_sc_expr_str(iternode.item) 393 menupath = f" > {title}" + menupath 394 395 menupath = "(Top)" + menupath 396 397 filename = node.filename 398 for name, path in module_paths.items(): 399 path += "/" 400 if node.filename.startswith(path): 401 filename = node.filename.replace(path, f"<module:{name}>/") 402 break 403 404 db.append( 405 { 406 "name": f"CONFIG_{sc.name}", 407 "prompt": node.prompt[0] if node.prompt else None, 408 "type": kconfiglib.TYPE_TO_STR[sc.type], 409 "help": node.help, 410 "dependencies": dependencies, 411 "defaults": defaults, 412 "alt_defaults": alt_defaults, 413 "selects": selects, 414 "selected_by": selected_by, 415 "implies": implies, 416 "implied_by": implied_by, 417 "ranges": ranges, 418 "choices": choices, 419 "filename": filename, 420 "linenr": node.linenr, 421 "menupath": menupath, 422 } 423 ) 424 425 app.env.kconfig_db = db # type: ignore 426 427 outdir = Path(app.outdir) / "kconfig" 428 outdir.mkdir(exist_ok=True) 429 430 kconfig_db_file = outdir / "kconfig.json" 431 432 with open(kconfig_db_file, "w") as f: 433 json.dump(db, f) 434 435 app.config.html_extra_path.append(kconfig_db_file.as_posix()) 436 app.config.html_static_path.append(RESOURCES_DIR.as_posix()) 437 438 439def kconfig_install( 440 app: Sphinx, 441 pagename: str, 442 templatename: str, 443 context: dict, 444 doctree: nodes.Node | None, 445) -> None: 446 """Install the Kconfig library files on pages that require it.""" 447 if ( 448 not app.config.kconfig_generate_db 449 or app.builder.format != "html" 450 or not doctree 451 ): 452 return 453 454 visitor = _FindKconfigSearchDirectiveVisitor(doctree) 455 doctree.walk(visitor) 456 if visitor.found_kconfig_search_directive: 457 app.add_css_file("kconfig.css") 458 app.add_js_file("kconfig.mjs", type="module") 459 460 461def setup(app: Sphinx): 462 app.add_config_value("kconfig_generate_db", False, "env") 463 app.add_config_value("kconfig_ext_paths", [], "env") 464 465 app.add_node( 466 KconfigSearchNode, 467 html=(kconfig_search_visit_html, None), 468 latex=(kconfig_search_visit_latex, None), 469 ) 470 471 app.add_domain(KconfigDomain) 472 473 app.connect("builder-inited", kconfig_build_resources) 474 app.connect("html-page-context", kconfig_install) 475 476 return { 477 "version": __version__, 478 "parallel_read_safe": True, 479 "parallel_write_safe": True, 480 } 481