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 43from pprint import pformat 44 45srctree = os.path.abspath(os.environ["srctree"]) 46sys.path.insert(0, os.path.join(srctree, "scripts/lib/kdoc")) 47 48from kdoc_files import KernelFiles 49from kdoc_output import RestFormat 50 51__version__ = '1.0' 52kfiles = None 53logger = logging.getLogger(__name__) 54 55def cmd_str(cmd): 56 """ 57 Helper function to output a command line that can be used to produce 58 the same records via command line. Helpful to debug troubles at the 59 script. 60 """ 61 62 cmd_line = "" 63 64 for w in cmd: 65 if w == "" or " " in w: 66 esc_cmd = "'" + w + "'" 67 else: 68 esc_cmd = w 69 70 if cmd_line: 71 cmd_line += " " + esc_cmd 72 continue 73 else: 74 cmd_line = esc_cmd 75 76 return cmd_line 77 78class KernelDocDirective(Directive): 79 """Extract kernel-doc comments from the specified file""" 80 required_argument = 1 81 optional_arguments = 4 82 option_spec = { 83 'doc': directives.unchanged_required, 84 'export': directives.unchanged, 85 'internal': directives.unchanged, 86 'identifiers': directives.unchanged, 87 'no-identifiers': directives.unchanged, 88 'functions': directives.unchanged, 89 } 90 has_content = False 91 verbose = 0 92 93 parse_args = {} 94 msg_args = {} 95 96 def handle_args(self): 97 98 env = self.state.document.settings.env 99 cmd = [env.config.kerneldoc_bin, '-rst', '-enable-lineno'] 100 101 filename = env.config.kerneldoc_srctree + '/' + self.arguments[0] 102 103 # Arguments used by KernelFiles.parse() function 104 self.parse_args = { 105 "file_list": [filename], 106 "export_file": [] 107 } 108 109 # Arguments used by KernelFiles.msg() function 110 self.msg_args = { 111 "enable_lineno": True, 112 "export": False, 113 "internal": False, 114 "symbol": [], 115 "nosymbol": [], 116 "no_doc_sections": False 117 } 118 119 export_file_patterns = [] 120 121 verbose = os.environ.get("V") 122 if verbose: 123 try: 124 self.verbose = int(verbose) 125 except ValueError: 126 pass 127 128 # Tell sphinx of the dependency 129 env.note_dependency(os.path.abspath(filename)) 130 131 self.tab_width = self.options.get('tab-width', 132 self.state.document.settings.tab_width) 133 134 # 'function' is an alias of 'identifiers' 135 if 'functions' in self.options: 136 self.options['identifiers'] = self.options.get('functions') 137 138 # FIXME: make this nicer and more robust against errors 139 if 'export' in self.options: 140 cmd += ['-export'] 141 self.msg_args["export"] = True 142 export_file_patterns = str(self.options.get('export')).split() 143 elif 'internal' in self.options: 144 cmd += ['-internal'] 145 self.msg_args["internal"] = True 146 export_file_patterns = str(self.options.get('internal')).split() 147 elif 'doc' in self.options: 148 func = str(self.options.get('doc')) 149 cmd += ['-function', func] 150 self.msg_args["symbol"].append(func) 151 elif 'identifiers' in self.options: 152 identifiers = self.options.get('identifiers').split() 153 if identifiers: 154 for i in identifiers: 155 i = i.rstrip("\\").strip() 156 if not i: 157 continue 158 159 cmd += ['-function', i] 160 self.msg_args["symbol"].append(i) 161 else: 162 cmd += ['-no-doc-sections'] 163 self.msg_args["no_doc_sections"] = True 164 165 if 'no-identifiers' in self.options: 166 no_identifiers = self.options.get('no-identifiers').split() 167 if no_identifiers: 168 for i in no_identifiers: 169 i = i.rstrip("\\").strip() 170 if not i: 171 continue 172 173 cmd += ['-nosymbol', i] 174 self.msg_args["nosymbol"].append(i) 175 176 for pattern in export_file_patterns: 177 pattern = pattern.rstrip("\\").strip() 178 if not pattern: 179 continue 180 181 for f in glob.glob(env.config.kerneldoc_srctree + '/' + pattern): 182 env.note_dependency(os.path.abspath(f)) 183 cmd += ['-export-file', f] 184 self.parse_args["export_file"].append(f) 185 186 # Export file is needed by both parse and msg, as kernel-doc 187 # cache exports. 188 self.msg_args["export_file"] = self.parse_args["export_file"] 189 190 cmd += [filename] 191 192 return cmd 193 194 def run_cmd(self, cmd): 195 """ 196 Execute an external kernel-doc command. 197 """ 198 199 env = self.state.document.settings.env 200 node = nodes.section() 201 202 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 203 out, err = p.communicate() 204 205 out, err = codecs.decode(out, 'utf-8'), codecs.decode(err, 'utf-8') 206 207 if p.returncode != 0: 208 sys.stderr.write(err) 209 210 logger.warning("kernel-doc '%s' failed with return code %d" 211 % (" ".join(cmd), p.returncode)) 212 return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))] 213 elif env.config.kerneldoc_verbosity > 0: 214 sys.stderr.write(err) 215 216 filenames = self.parse_args["file_list"] 217 for filename in filenames: 218 self.parse_msg(filename, node, out, cmd) 219 220 return node.children 221 222 def parse_msg(self, filename, node, out, cmd): 223 """ 224 Handles a kernel-doc output for a given file 225 """ 226 227 env = self.state.document.settings.env 228 229 lines = statemachine.string2lines(out, self.tab_width, 230 convert_whitespace=True) 231 result = ViewList() 232 233 lineoffset = 0; 234 line_regex = re.compile(r"^\.\. LINENO ([0-9]+)$") 235 for line in lines: 236 match = line_regex.search(line) 237 if match: 238 # sphinx counts lines from 0 239 lineoffset = int(match.group(1)) - 1 240 # we must eat our comments since the upset the markup 241 else: 242 doc = str(env.srcdir) + "/" + env.docname + ":" + str(self.lineno) 243 result.append(line, doc + ": " + filename, lineoffset) 244 lineoffset += 1 245 246 self.do_parse(result, node) 247 248 def run_kdoc(self, cmd, kfiles): 249 """ 250 Execute kernel-doc classes directly instead of running as a separate 251 command. 252 """ 253 254 env = self.state.document.settings.env 255 256 node = nodes.section() 257 258 kfiles.parse(**self.parse_args) 259 filenames = self.parse_args["file_list"] 260 261 for filename, out in kfiles.msg(**self.msg_args, filenames=filenames): 262 self.parse_msg(filename, node, out, cmd) 263 264 return node.children 265 266 def run(self): 267 global kfiles 268 269 cmd = self.handle_args() 270 if self.verbose >= 1: 271 logger.info(cmd_str(cmd)) 272 273 try: 274 if kfiles: 275 return self.run_kdoc(cmd, kfiles) 276 else: 277 return self.run_cmd(cmd) 278 279 except Exception as e: # pylint: disable=W0703 280 logger.warning("kernel-doc '%s' processing failed with: %s" % 281 (cmd_str(cmd), pformat(e))) 282 return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))] 283 284 def do_parse(self, result, node): 285 with switch_source_input(self.state, result): 286 self.state.nested_parse(result, 0, node, match_titles=1) 287 288def setup_kfiles(app): 289 global kfiles 290 291 kerneldoc_bin = app.env.config.kerneldoc_bin 292 293 if kerneldoc_bin and kerneldoc_bin.endswith("kernel-doc.py"): 294 print("Using Python kernel-doc") 295 out_style = RestFormat() 296 kfiles = KernelFiles(out_style=out_style, logger=logger) 297 else: 298 print(f"Using {kerneldoc_bin}") 299 300 301def setup(app): 302 app.add_config_value('kerneldoc_bin', None, 'env') 303 app.add_config_value('kerneldoc_srctree', None, 'env') 304 app.add_config_value('kerneldoc_verbosity', 1, 'env') 305 306 app.add_directive('kernel-doc', KernelDocDirective) 307 308 app.connect('builder-inited', setup_kfiles) 309 310 return dict( 311 version = __version__, 312 parallel_read_safe = True, 313 parallel_write_safe = True 314 ) 315