1# coding=utf-8 2# SPDX-License-Identifier: MIT 3# 4# Copyright © 2016 Intel Corporation 5# 6# Permission is hereby granted, free of charge, to any person obtaining a 7# copy of this software and associated documentation files (the "Software"), 8# to deal in the Software without restriction, including without limitation 9# the rights to use, copy, modify, merge, publish, distribute, sublicense, 10# and/or sell copies of the Software, and to permit persons to whom the 11# Software is furnished to do so, subject to the following conditions: 12# 13# The above copyright notice and this permission notice (including the next 14# paragraph) shall be included in all copies or substantial portions of the 15# Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23# IN THE SOFTWARE. 24# 25# Authors: 26# Jani Nikula <jani.nikula@intel.com> 27# 28# Please make sure this works on both python2 and python3. 29# 30 31import codecs 32import os 33import subprocess 34import sys 35import re 36import glob 37 38from docutils import nodes, statemachine 39from docutils.statemachine import ViewList 40from docutils.parsers.rst import directives, Directive 41import sphinx 42from sphinx.util.docutils import switch_source_input 43from sphinx.util import logging 44from pprint import pformat 45 46srctree = os.path.abspath(os.environ["srctree"]) 47sys.path.insert(0, os.path.join(srctree, "scripts/lib/kdoc")) 48 49from kdoc_files import KernelFiles 50from kdoc_output import RestFormat 51 52__version__ = '1.0' 53kfiles = None 54logger = logging.getLogger(__name__) 55 56def cmd_str(cmd): 57 """ 58 Helper function to output a command line that can be used to produce 59 the same records via command line. Helpful to debug troubles at the 60 script. 61 """ 62 63 cmd_line = "" 64 65 for w in cmd: 66 if w == "" or " " in w: 67 esc_cmd = "'" + w + "'" 68 else: 69 esc_cmd = w 70 71 if cmd_line: 72 cmd_line += " " + esc_cmd 73 continue 74 else: 75 cmd_line = esc_cmd 76 77 return cmd_line 78 79class KernelDocDirective(Directive): 80 """Extract kernel-doc comments from the specified file""" 81 required_argument = 1 82 optional_arguments = 4 83 option_spec = { 84 'doc': directives.unchanged_required, 85 'export': directives.unchanged, 86 'internal': directives.unchanged, 87 'identifiers': directives.unchanged, 88 'no-identifiers': directives.unchanged, 89 'functions': directives.unchanged, 90 } 91 has_content = False 92 verbose = 0 93 94 parse_args = {} 95 msg_args = {} 96 97 def handle_args(self): 98 99 env = self.state.document.settings.env 100 cmd = [env.config.kerneldoc_bin, '-rst', '-enable-lineno'] 101 102 filename = env.config.kerneldoc_srctree + '/' + self.arguments[0] 103 104 # Arguments used by KernelFiles.parse() function 105 self.parse_args = { 106 "file_list": [filename], 107 "export_file": [] 108 } 109 110 # Arguments used by KernelFiles.msg() function 111 self.msg_args = { 112 "enable_lineno": True, 113 "export": False, 114 "internal": False, 115 "symbol": [], 116 "nosymbol": [], 117 "no_doc_sections": False 118 } 119 120 export_file_patterns = [] 121 122 verbose = os.environ.get("V") 123 if verbose: 124 try: 125 self.verbose = int(verbose) 126 except ValueError: 127 pass 128 129 # Tell sphinx of the dependency 130 env.note_dependency(os.path.abspath(filename)) 131 132 self.tab_width = self.options.get('tab-width', 133 self.state.document.settings.tab_width) 134 135 # 'function' is an alias of 'identifiers' 136 if 'functions' in self.options: 137 self.options['identifiers'] = self.options.get('functions') 138 139 # FIXME: make this nicer and more robust against errors 140 if 'export' in self.options: 141 cmd += ['-export'] 142 self.msg_args["export"] = True 143 export_file_patterns = str(self.options.get('export')).split() 144 elif 'internal' in self.options: 145 cmd += ['-internal'] 146 self.msg_args["internal"] = True 147 export_file_patterns = str(self.options.get('internal')).split() 148 elif 'doc' in self.options: 149 func = str(self.options.get('doc')) 150 cmd += ['-function', func] 151 self.msg_args["symbol"].append(func) 152 elif 'identifiers' in self.options: 153 identifiers = self.options.get('identifiers').split() 154 if identifiers: 155 for i in identifiers: 156 i = i.rstrip("\\").strip() 157 if not i: 158 continue 159 160 cmd += ['-function', i] 161 self.msg_args["symbol"].append(i) 162 else: 163 cmd += ['-no-doc-sections'] 164 self.msg_args["no_doc_sections"] = True 165 166 if 'no-identifiers' in self.options: 167 no_identifiers = self.options.get('no-identifiers').split() 168 if no_identifiers: 169 for i in no_identifiers: 170 i = i.rstrip("\\").strip() 171 if not i: 172 continue 173 174 cmd += ['-nosymbol', i] 175 self.msg_args["nosymbol"].append(i) 176 177 for pattern in export_file_patterns: 178 pattern = pattern.rstrip("\\").strip() 179 if not pattern: 180 continue 181 182 for f in glob.glob(env.config.kerneldoc_srctree + '/' + pattern): 183 env.note_dependency(os.path.abspath(f)) 184 cmd += ['-export-file', f] 185 self.parse_args["export_file"].append(f) 186 187 # Export file is needed by both parse and msg, as kernel-doc 188 # cache exports. 189 self.msg_args["export_file"] = self.parse_args["export_file"] 190 191 cmd += [filename] 192 193 return cmd 194 195 def run_cmd(self, cmd): 196 """ 197 Execute an external kernel-doc command. 198 """ 199 200 env = self.state.document.settings.env 201 node = nodes.section() 202 203 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 204 out, err = p.communicate() 205 206 out, err = codecs.decode(out, 'utf-8'), codecs.decode(err, 'utf-8') 207 208 if p.returncode != 0: 209 sys.stderr.write(err) 210 211 logger.warning("kernel-doc '%s' failed with return code %d" 212 % (" ".join(cmd), p.returncode)) 213 return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))] 214 elif env.config.kerneldoc_verbosity > 0: 215 sys.stderr.write(err) 216 217 filenames = self.parse_args["file_list"] 218 for filename in filenames: 219 self.parse_msg(filename, node, out, cmd) 220 221 return node.children 222 223 def parse_msg(self, filename, node, out, cmd): 224 """ 225 Handles a kernel-doc output for a given file 226 """ 227 228 env = self.state.document.settings.env 229 230 lines = statemachine.string2lines(out, self.tab_width, 231 convert_whitespace=True) 232 result = ViewList() 233 234 lineoffset = 0; 235 line_regex = re.compile(r"^\.\. LINENO ([0-9]+)$") 236 for line in lines: 237 match = line_regex.search(line) 238 if match: 239 # sphinx counts lines from 0 240 lineoffset = int(match.group(1)) - 1 241 # we must eat our comments since the upset the markup 242 else: 243 doc = str(env.srcdir) + "/" + env.docname + ":" + str(self.lineno) 244 result.append(line, doc + ": " + filename, lineoffset) 245 lineoffset += 1 246 247 self.do_parse(result, node) 248 249 def run_kdoc(self, cmd, kfiles): 250 """ 251 Execute kernel-doc classes directly instead of running as a separate 252 command. 253 """ 254 255 env = self.state.document.settings.env 256 257 node = nodes.section() 258 259 kfiles.parse(**self.parse_args) 260 filenames = self.parse_args["file_list"] 261 262 for filename, out in kfiles.msg(**self.msg_args, filenames=filenames): 263 self.parse_msg(filename, node, out, cmd) 264 265 return node.children 266 267 def run(self): 268 global kfiles 269 270 cmd = self.handle_args() 271 if self.verbose >= 1: 272 logger.info(cmd_str(cmd)) 273 274 try: 275 if kfiles: 276 return self.run_kdoc(cmd, kfiles) 277 else: 278 return self.run_cmd(cmd) 279 280 except Exception as e: # pylint: disable=W0703 281 logger.warning("kernel-doc '%s' processing failed with: %s" % 282 (cmd_str(cmd), pformat(e))) 283 return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))] 284 285 def do_parse(self, result, node): 286 with switch_source_input(self.state, result): 287 self.state.nested_parse(result, 0, node, match_titles=1) 288 289def setup_kfiles(app): 290 global kfiles 291 292 kerneldoc_bin = app.env.config.kerneldoc_bin 293 294 if kerneldoc_bin and kerneldoc_bin.endswith("kernel-doc.py"): 295 print("Using Python kernel-doc") 296 out_style = RestFormat() 297 kfiles = KernelFiles(out_style=out_style, logger=logger) 298 else: 299 print(f"Using {kerneldoc_bin}") 300 301 302def setup(app): 303 app.add_config_value('kerneldoc_bin', None, 'env') 304 app.add_config_value('kerneldoc_srctree', None, 'env') 305 app.add_config_value('kerneldoc_verbosity', 1, 'env') 306 307 app.add_directive('kernel-doc', KernelDocDirective) 308 309 app.connect('builder-inited', setup_kfiles) 310 311 return dict( 312 version = __version__, 313 parallel_read_safe = True, 314 parallel_write_safe = True 315 ) 316