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