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