xref: /linux/Documentation/sphinx/kerneldoc.py (revision 6382f4e4e7bd65b829d1d598b1315e905accc15c)
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'
51use_kfiles = False
52
53def cmd_str(cmd):
54    """
55    Helper function to output a command line that can be used to produce
56    the same records via command line. Helpful to debug troubles at the
57    script.
58    """
59
60    cmd_line = ""
61
62    for w in cmd:
63        if w == "" or " " in w:
64            esc_cmd = "'" + w + "'"
65        else:
66            esc_cmd = w
67
68        if cmd_line:
69            cmd_line += " " + esc_cmd
70            continue
71        else:
72            cmd_line = esc_cmd
73
74    return cmd_line
75
76class KernelDocDirective(Directive):
77    """Extract kernel-doc comments from the specified file"""
78    required_argument = 1
79    optional_arguments = 4
80    option_spec = {
81        'doc': directives.unchanged_required,
82        'export': directives.unchanged,
83        'internal': directives.unchanged,
84        'identifiers': directives.unchanged,
85        'no-identifiers': directives.unchanged,
86        'functions': directives.unchanged,
87    }
88    has_content = False
89    logger = logging.getLogger('kerneldoc')
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            self.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                self.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            self.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            self.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 use_kfiles
296
297        if use_kfiles:
298            out_style = RestFormat()
299            kfiles = KernelFiles(out_style=out_style, logger=self.logger)
300            return self.run_kdoc(kfiles)
301        else:
302            return self.run_cmd()
303
304    def do_parse(self, result, node):
305        with switch_source_input(self.state, result):
306            self.state.nested_parse(result, 0, node, match_titles=1)
307
308def setup_kfiles(app):
309    global use_kfiles
310
311    kerneldoc_bin = app.env.config.kerneldoc_bin
312
313    if kerneldoc_bin and kerneldoc_bin.endswith("kernel-doc.py"):
314        print("Using Python kernel-doc")
315        use_kfiles = True
316    else:
317        print(f"Using {kerneldoc_bin}")
318
319
320def setup(app):
321    app.add_config_value('kerneldoc_bin', None, 'env')
322    app.add_config_value('kerneldoc_srctree', None, 'env')
323    app.add_config_value('kerneldoc_verbosity', 1, 'env')
324
325    app.add_directive('kernel-doc', KernelDocDirective)
326
327    app.connect('builder-inited', setup_kfiles)
328
329    return dict(
330        version = __version__,
331        parallel_read_safe = True,
332        parallel_write_safe = True
333    )
334