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