xref: /linux/Documentation/sphinx/kerneldoc.py (revision 4eaf6120c16a0fdb60177410507de86eefbe8c0f)
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