1# -*- coding: utf-8; mode: python -*-
2# coding=utf-8
3# SPDX-License-Identifier: GPL-2.0
4#
5u"""
6    kernel-abi
7    ~~~~~~~~~~
8
9    Implementation of the ``kernel-abi`` reST-directive.
10
11    :copyright:  Copyright (C) 2016  Markus Heiser
12    :copyright:  Copyright (C) 2016-2020  Mauro Carvalho Chehab
13    :maintained-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
14    :license:    GPL Version 2, June 1991 see Linux/COPYING for details.
15
16    The ``kernel-abi`` (:py:class:`KernelCmd`) directive calls the
17    scripts/get_abi.pl script to parse the Kernel ABI files.
18
19    Overview of directive's argument and options.
20
21    .. code-block:: rst
22
23        .. kernel-abi:: <ABI directory location>
24            :debug:
25
26    The argument ``<ABI directory location>`` is required. It contains the
27    location of the ABI files to be parsed.
28
29    ``debug``
30      Inserts a code-block with the *raw* reST. Sometimes it is helpful to see
31      what reST is generated.
32
33"""
34
35import codecs
36import os
37import subprocess
38import sys
39import re
40import kernellog
41
42from os import path
43
44from docutils import nodes, statemachine
45from docutils.statemachine import ViewList
46from docutils.parsers.rst import directives, Directive
47from docutils.utils.error_reporting import ErrorString
48from sphinx.util.docutils import switch_source_input
49
50__version__  = '1.0'
51
52def setup(app):
53
54    app.add_directive("kernel-abi", KernelCmd)
55    return dict(
56        version = __version__
57        , parallel_read_safe = True
58        , parallel_write_safe = True
59    )
60
61class KernelCmd(Directive):
62
63    u"""KernelABI (``kernel-abi``) directive"""
64
65    required_arguments = 1
66    optional_arguments = 2
67    has_content = False
68    final_argument_whitespace = True
69
70    option_spec = {
71        "debug"     : directives.flag,
72        "rst"       : directives.unchanged
73    }
74
75    def run(self):
76
77        doc = self.state.document
78        if not doc.settings.file_insertion_enabled:
79            raise self.warning("docutils: file insertion disabled")
80
81        env = doc.settings.env
82        cwd = path.dirname(doc.current_source)
83        cmd = "get_abi.pl rest --enable-lineno --dir "
84        cmd += self.arguments[0]
85
86        if 'rst' in self.options:
87            cmd += " --rst-source"
88
89        srctree = path.abspath(os.environ["srctree"])
90
91        fname = cmd
92
93        # extend PATH with $(srctree)/scripts
94        path_env = os.pathsep.join([
95            srctree + os.sep + "scripts",
96            os.environ["PATH"]
97        ])
98        shell_env = os.environ.copy()
99        shell_env["PATH"]    = path_env
100        shell_env["srctree"] = srctree
101
102        lines = self.runCmd(cmd, shell=True, cwd=cwd, env=shell_env)
103        nodeList = self.nestedParse(lines, self.arguments[0])
104        return nodeList
105
106    def runCmd(self, cmd, **kwargs):
107        u"""Run command ``cmd`` and return its stdout as unicode."""
108
109        try:
110            proc = subprocess.Popen(
111                cmd
112                , stdout = subprocess.PIPE
113                , stderr = subprocess.PIPE
114                , **kwargs
115            )
116            out, err = proc.communicate()
117
118            out, err = codecs.decode(out, 'utf-8'), codecs.decode(err, 'utf-8')
119
120            if proc.returncode != 0:
121                raise self.severe(
122                    u"command '%s' failed with return code %d"
123                    % (cmd, proc.returncode)
124                )
125        except OSError as exc:
126            raise self.severe(u"problems with '%s' directive: %s."
127                              % (self.name, ErrorString(exc)))
128        return out
129
130    def nestedParse(self, lines, fname):
131        env = self.state.document.settings.env
132        content = ViewList()
133        node = nodes.section()
134
135        if "debug" in self.options:
136            code_block = "\n\n.. code-block:: rst\n    :linenos:\n"
137            for l in lines.split("\n"):
138                code_block += "\n    " + l
139            lines = code_block + "\n\n"
140
141        line_regex = re.compile("^\.\. LINENO (\S+)\#([0-9]+)$")
142        ln = 0
143        n = 0
144        f = fname
145
146        for line in lines.split("\n"):
147            n = n + 1
148            match = line_regex.search(line)
149            if match:
150                new_f = match.group(1)
151
152                # Sphinx parser is lazy: it stops parsing contents in the
153                # middle, if it is too big. So, handle it per input file
154                if new_f != f and content:
155                    self.do_parse(content, node)
156                    content = ViewList()
157
158                    # Add the file to Sphinx build dependencies
159                    env.note_dependency(os.path.abspath(f))
160
161                f = new_f
162
163                # sphinx counts lines from 0
164                ln = int(match.group(2)) - 1
165            else:
166                content.append(line, f, ln)
167
168        kernellog.info(self.state.document.settings.env.app, "%s: parsed %i lines" % (fname, n))
169
170        if content:
171            self.do_parse(content, node)
172
173        return node.children
174
175    def do_parse(self, content, node):
176        with switch_source_input(self.state, content):
177            self.state.nested_parse(content, 0, node, match_titles=1)
178