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