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
48
49#
50# AutodocReporter is only good up to Sphinx 1.7
51#
52import sphinx
53
54Use_SSI = sphinx.__version__[:3] >= '1.7'
55if Use_SSI:
56    from sphinx.util.docutils import switch_source_input
57else:
58    from sphinx.ext.autodoc import AutodocReporter
59
60__version__  = '1.0'
61
62def setup(app):
63
64    app.add_directive("kernel-abi", KernelCmd)
65    return dict(
66        version = __version__
67        , parallel_read_safe = True
68        , parallel_write_safe = True
69    )
70
71class KernelCmd(Directive):
72
73    u"""KernelABI (``kernel-abi``) directive"""
74
75    required_arguments = 1
76    optional_arguments = 2
77    has_content = False
78    final_argument_whitespace = True
79
80    option_spec = {
81        "debug"     : directives.flag,
82        "rst"       : directives.unchanged
83    }
84
85    def run(self):
86
87        doc = self.state.document
88        if not doc.settings.file_insertion_enabled:
89            raise self.warning("docutils: file insertion disabled")
90
91        env = doc.settings.env
92        cwd = path.dirname(doc.current_source)
93        cmd = "get_abi.pl rest --enable-lineno --dir "
94        cmd += self.arguments[0]
95
96        if 'rst' in self.options:
97            cmd += " --rst-source"
98
99        srctree = path.abspath(os.environ["srctree"])
100
101        fname = cmd
102
103        # extend PATH with $(srctree)/scripts
104        path_env = os.pathsep.join([
105            srctree + os.sep + "scripts",
106            os.environ["PATH"]
107        ])
108        shell_env = os.environ.copy()
109        shell_env["PATH"]    = path_env
110        shell_env["srctree"] = srctree
111
112        lines = self.runCmd(cmd, shell=True, cwd=cwd, env=shell_env)
113        nodeList = self.nestedParse(lines, self.arguments[0])
114        return nodeList
115
116    def runCmd(self, cmd, **kwargs):
117        u"""Run command ``cmd`` and return it's stdout as unicode."""
118
119        try:
120            proc = subprocess.Popen(
121                cmd
122                , stdout = subprocess.PIPE
123                , stderr = subprocess.PIPE
124                , **kwargs
125            )
126            out, err = proc.communicate()
127
128            out, err = codecs.decode(out, 'utf-8'), codecs.decode(err, 'utf-8')
129
130            if proc.returncode != 0:
131                raise self.severe(
132                    u"command '%s' failed with return code %d"
133                    % (cmd, proc.returncode)
134                )
135        except OSError as exc:
136            raise self.severe(u"problems with '%s' directive: %s."
137                              % (self.name, ErrorString(exc)))
138        return out
139
140    def nestedParse(self, lines, fname):
141        content = ViewList()
142        node = nodes.section()
143
144        if "debug" in self.options:
145            code_block = "\n\n.. code-block:: rst\n    :linenos:\n"
146            for l in lines.split("\n"):
147                code_block += "\n    " + l
148            lines = code_block + "\n\n"
149
150        line_regex = re.compile("^#define LINENO (\S+)\#([0-9]+)$")
151        ln = 0
152        n = 0
153        f = fname
154
155        for line in lines.split("\n"):
156            n = n + 1
157            match = line_regex.search(line)
158            if match:
159                new_f = match.group(1)
160
161                # Sphinx parser is lazy: it stops parsing contents in the
162                # middle, if it is too big. So, handle it per input file
163                if new_f != f and content:
164                    self.do_parse(content, node)
165                    content = ViewList()
166
167                f = new_f
168
169                # sphinx counts lines from 0
170                ln = int(match.group(2)) - 1
171            else:
172                content.append(line, f, ln)
173
174        kernellog.info(self.state.document.settings.env.app, "%s: parsed %i lines" % (fname, n))
175
176        if content:
177            self.do_parse(content, node)
178
179        return node.children
180
181    def do_parse(self, content, node):
182        if Use_SSI:
183            with switch_source_input(self.state, content):
184                self.state.nested_parse(content, 0, node, match_titles=1)
185        else:
186            buf  = self.state.memo.title_styles, self.state.memo.section_level, self.state.memo.reporter
187
188            self.state.memo.title_styles  = []
189            self.state.memo.section_level = 0
190            self.state.memo.reporter      = AutodocReporter(content, self.state.memo.reporter)
191            try:
192                self.state.nested_parse(content, 0, node, match_titles=1)
193            finally:
194                self.state.memo.title_styles, self.state.memo.section_level, self.state.memo.reporter = buf
195