1# -*- coding: utf-8 -*-
2#
3# QAPI code generation
4#
5# Copyright (c) 2015-2019 Red Hat Inc.
6#
7# Authors:
8#  Markus Armbruster <armbru@redhat.com>
9#  Marc-André Lureau <marcandre.lureau@redhat.com>
10#
11# This work is licensed under the terms of the GNU GPL, version 2.
12# See the COPYING file in the top-level directory.
13
14from contextlib import contextmanager
15import os
16import re
17from typing import (
18    Dict,
19    Iterator,
20    Optional,
21    Tuple,
22)
23
24from .common import (
25    c_fname,
26    c_name,
27    guardend,
28    guardstart,
29    mcgen,
30)
31from .schema import (
32    QAPISchemaIfCond,
33    QAPISchemaModule,
34    QAPISchemaObjectType,
35    QAPISchemaVisitor,
36)
37from .source import QAPISourceInfo
38
39
40class QAPIGen:
41    def __init__(self, fname: str):
42        self.fname = fname
43        self._preamble = ''
44        self._body = ''
45
46    def preamble_add(self, text: str) -> None:
47        self._preamble += text
48
49    def add(self, text: str) -> None:
50        self._body += text
51
52    def get_content(self) -> str:
53        return self._top() + self._preamble + self._body + self._bottom()
54
55    def _top(self) -> str:
56        # pylint: disable=no-self-use
57        return ''
58
59    def _bottom(self) -> str:
60        # pylint: disable=no-self-use
61        return ''
62
63    def write(self, output_dir: str) -> None:
64        # Include paths starting with ../ are used to reuse modules of the main
65        # schema in specialised schemas. Don't overwrite the files that are
66        # already generated for the main schema.
67        if self.fname.startswith('../'):
68            return
69        pathname = os.path.join(output_dir, self.fname)
70        odir = os.path.dirname(pathname)
71
72        if odir:
73            os.makedirs(odir, exist_ok=True)
74
75        # use os.open for O_CREAT to create and read a non-existant file
76        fd = os.open(pathname, os.O_RDWR | os.O_CREAT, 0o666)
77        with os.fdopen(fd, 'r+', encoding='utf-8') as fp:
78            text = self.get_content()
79            oldtext = fp.read(len(text) + 1)
80            if text != oldtext:
81                fp.seek(0)
82                fp.truncate(0)
83                fp.write(text)
84
85
86def _wrap_ifcond(ifcond: QAPISchemaIfCond, before: str, after: str) -> str:
87    if before == after:
88        return after   # suppress empty #if ... #endif
89
90    assert after.startswith(before)
91    out = before
92    added = after[len(before):]
93    if added[0] == '\n':
94        out += '\n'
95        added = added[1:]
96    out += ifcond.gen_if()
97    out += added
98    out += ifcond.gen_endif()
99    return out
100
101
102def build_params(arg_type: Optional[QAPISchemaObjectType],
103                 boxed: bool,
104                 extra: Optional[str] = None) -> str:
105    ret = ''
106    sep = ''
107    if boxed:
108        assert arg_type
109        ret += '%s arg' % arg_type.c_param_type()
110        sep = ', '
111    elif arg_type:
112        assert not arg_type.variants
113        for memb in arg_type.members:
114            ret += sep
115            sep = ', '
116            if memb.optional:
117                ret += 'bool has_%s, ' % c_name(memb.name)
118            ret += '%s %s' % (memb.type.c_param_type(),
119                              c_name(memb.name))
120    if extra:
121        ret += sep + extra
122    return ret if ret else 'void'
123
124
125class QAPIGenCCode(QAPIGen):
126    def __init__(self, fname: str):
127        super().__init__(fname)
128        self._start_if: Optional[Tuple[QAPISchemaIfCond, str, str]] = None
129
130    def start_if(self, ifcond: QAPISchemaIfCond) -> None:
131        assert self._start_if is None
132        self._start_if = (ifcond, self._body, self._preamble)
133
134    def end_if(self) -> None:
135        assert self._start_if is not None
136        self._body = _wrap_ifcond(self._start_if[0],
137                                  self._start_if[1], self._body)
138        self._preamble = _wrap_ifcond(self._start_if[0],
139                                      self._start_if[2], self._preamble)
140        self._start_if = None
141
142    def get_content(self) -> str:
143        assert self._start_if is None
144        return super().get_content()
145
146
147class QAPIGenC(QAPIGenCCode):
148    def __init__(self, fname: str, blurb: str, pydoc: str):
149        super().__init__(fname)
150        self._blurb = blurb
151        self._copyright = '\n * '.join(re.findall(r'^Copyright .*', pydoc,
152                                                  re.MULTILINE))
153
154    def _top(self) -> str:
155        return mcgen('''
156/* AUTOMATICALLY GENERATED, DO NOT MODIFY */
157
158/*
159%(blurb)s
160 *
161 * %(copyright)s
162 *
163 * This work is licensed under the terms of the GNU LGPL, version 2.1 or later.
164 * See the COPYING.LIB file in the top-level directory.
165 */
166
167''',
168                     blurb=self._blurb, copyright=self._copyright)
169
170    def _bottom(self) -> str:
171        return mcgen('''
172
173/* Dummy declaration to prevent empty .o file */
174char qapi_dummy_%(name)s;
175''',
176                     name=c_fname(self.fname))
177
178
179class QAPIGenH(QAPIGenC):
180    def _top(self) -> str:
181        return super()._top() + guardstart(self.fname)
182
183    def _bottom(self) -> str:
184        return guardend(self.fname)
185
186
187@contextmanager
188def ifcontext(ifcond: QAPISchemaIfCond, *args: QAPIGenCCode) -> Iterator[None]:
189    """
190    A with-statement context manager that wraps with `start_if()` / `end_if()`.
191
192    :param ifcond: A sequence of conditionals, passed to `start_if()`.
193    :param args: any number of `QAPIGenCCode`.
194
195    Example::
196
197        with ifcontext(ifcond, self._genh, self._genc):
198            modify self._genh and self._genc ...
199
200    Is equivalent to calling::
201
202        self._genh.start_if(ifcond)
203        self._genc.start_if(ifcond)
204        modify self._genh and self._genc ...
205        self._genh.end_if()
206        self._genc.end_if()
207    """
208    for arg in args:
209        arg.start_if(ifcond)
210    yield
211    for arg in args:
212        arg.end_if()
213
214
215class QAPISchemaMonolithicCVisitor(QAPISchemaVisitor):
216    def __init__(self,
217                 prefix: str,
218                 what: str,
219                 blurb: str,
220                 pydoc: str):
221        self._prefix = prefix
222        self._what = what
223        self._genc = QAPIGenC(self._prefix + self._what + '.c',
224                              blurb, pydoc)
225        self._genh = QAPIGenH(self._prefix + self._what + '.h',
226                              blurb, pydoc)
227
228    def write(self, output_dir: str) -> None:
229        self._genc.write(output_dir)
230        self._genh.write(output_dir)
231
232
233class QAPISchemaModularCVisitor(QAPISchemaVisitor):
234    def __init__(self,
235                 prefix: str,
236                 what: str,
237                 user_blurb: str,
238                 builtin_blurb: Optional[str],
239                 pydoc: str):
240        self._prefix = prefix
241        self._what = what
242        self._user_blurb = user_blurb
243        self._builtin_blurb = builtin_blurb
244        self._pydoc = pydoc
245        self._current_module: Optional[str] = None
246        self._module: Dict[str, Tuple[QAPIGenC, QAPIGenH]] = {}
247        self._main_module: Optional[str] = None
248
249    @property
250    def _genc(self) -> QAPIGenC:
251        assert self._current_module is not None
252        return self._module[self._current_module][0]
253
254    @property
255    def _genh(self) -> QAPIGenH:
256        assert self._current_module is not None
257        return self._module[self._current_module][1]
258
259    @staticmethod
260    def _module_dirname(name: str) -> str:
261        if QAPISchemaModule.is_user_module(name):
262            return os.path.dirname(name)
263        return ''
264
265    def _module_basename(self, what: str, name: str) -> str:
266        ret = '' if QAPISchemaModule.is_builtin_module(name) else self._prefix
267        if QAPISchemaModule.is_user_module(name):
268            basename = os.path.basename(name)
269            ret += what
270            if name != self._main_module:
271                ret += '-' + os.path.splitext(basename)[0]
272        else:
273            assert QAPISchemaModule.is_system_module(name)
274            ret += re.sub(r'-', '-' + name[2:] + '-', what)
275        return ret
276
277    def _module_filename(self, what: str, name: str) -> str:
278        return os.path.join(self._module_dirname(name),
279                            self._module_basename(what, name))
280
281    def _add_module(self, name: str, blurb: str) -> None:
282        if QAPISchemaModule.is_user_module(name):
283            if self._main_module is None:
284                self._main_module = name
285        basename = self._module_filename(self._what, name)
286        genc = QAPIGenC(basename + '.c', blurb, self._pydoc)
287        genh = QAPIGenH(basename + '.h', blurb, self._pydoc)
288        self._module[name] = (genc, genh)
289        self._current_module = name
290
291    @contextmanager
292    def _temp_module(self, name: str) -> Iterator[None]:
293        old_module = self._current_module
294        self._current_module = name
295        yield
296        self._current_module = old_module
297
298    def write(self, output_dir: str, opt_builtins: bool = False) -> None:
299        for name in self._module:
300            if QAPISchemaModule.is_builtin_module(name) and not opt_builtins:
301                continue
302            (genc, genh) = self._module[name]
303            genc.write(output_dir)
304            genh.write(output_dir)
305
306    def _begin_builtin_module(self) -> None:
307        pass
308
309    def _begin_user_module(self, name: str) -> None:
310        pass
311
312    def visit_module(self, name: str) -> None:
313        if QAPISchemaModule.is_builtin_module(name):
314            if self._builtin_blurb:
315                self._add_module(name, self._builtin_blurb)
316                self._begin_builtin_module()
317            else:
318                # The built-in module has not been created.  No code may
319                # be generated.
320                self._current_module = None
321        else:
322            assert QAPISchemaModule.is_user_module(name)
323            self._add_module(name, self._user_blurb)
324            self._begin_user_module(name)
325
326    def visit_include(self, name: str, info: Optional[QAPISourceInfo]) -> None:
327        relname = os.path.relpath(self._module_filename(self._what, name),
328                                  os.path.dirname(self._genh.fname))
329        self._genh.preamble_add(mcgen('''
330#include "%(relname)s.h"
331''',
332                                      relname=relname))
333