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