1#!/usr/local/bin/python3.8
2#
3# Copyright 2015-2018 the openage authors. See copying.md for legal info.
4
5"""
6Runs Cython on all modules that were listed via add_cython_module.
7"""
8
9import argparse
10import os
11import sys
12
13
14class LineFilter:
15    """ Proxy for a stream (default stdout) to filter out whole unwanted lines """
16    # pylint: disable=too-few-public-methods
17    def __init__(self, stream=None, filters=None):
18        self.stream = stream or sys.stdout
19        self.filters = filters or []
20        self.buf = ""
21
22    def __getattr__(self, attr_name):
23        return getattr(self.stream, attr_name)
24
25    def __enter__(self):
26        return self
27
28    def __exit__(self, *args):
29        self.stream.write(self.buf)
30        self.buf = ""
31
32    def write(self, data):
33        """
34        Writes to output stream, buffered line-wise,
35        omitting lines given in to constructor in filter
36        """
37        self.buf += data
38        lines = self.buf.split('\n')
39        for line in lines[:-1]:
40            if not any(f(line) for f in self.filters):
41                self.stream.write(line + '\n')
42        self.buf = lines[-1]
43
44
45class CythonFilter(LineFilter):
46    """ Filters output of cythonize for useless warnings """
47    # pylint: disable=too-few-public-methods
48    def __init__(self):
49        filters = [
50            lambda x: x == 'Please put "# distutils: language=c++" in your .pyx or .pxd file(s)',
51            lambda x: x.startswith('Compiling ') and x.endswith(' because it changed.')
52        ]
53        super().__init__(filters=filters)
54
55
56def read_list_from_file(filename):
57    """ Reads a semicolon-separated list of file entires """
58    with open(filename) as fileobj:
59        data = fileobj.read().strip()
60
61    data = [os.path.realpath(os.path.normpath(filename)) for filename in data.split(';')]
62    if data == ['']:
63        return []
64
65    return data
66
67
68def remove_if_exists(filename):
69    """ Deletes the file (if it exists) """
70    if os.path.exists(filename):
71        print(os.path.relpath(filename, os.getcwd()))
72        os.remove(filename)
73
74
75def cythonize_cpp_wrapper(modules):
76    """ Calls cythonize, filtering useless warnings """
77    from Cython.Build import cythonize
78    from contextlib import redirect_stdout
79    from multiprocessing import cpu_count
80
81    if not modules:
82        return
83
84    with CythonFilter() as cython_filter:
85        with redirect_stdout(cython_filter):
86            cythonize(modules, language='c++', nthreads=cpu_count())
87
88
89def main():
90    """ CLI entry point """
91    cli = argparse.ArgumentParser()
92    cli.add_argument("module_list", help=(
93        "Module list file (semicolon-separated)."
94    ))
95    cli.add_argument("embedded_module_list", help=(
96        "Embedded module list file (semicolon-separated).\n"
97        "Modules in this list are compiled with the --embed option."
98    ))
99    cli.add_argument("depends_list", help=(
100        "Dependency list file (semicolon-separated).\n"
101        "Contains all .pxd and other files that may get included.\n"
102        "Used to verify that all dependencies are properly listed "
103        "in the CMake build configuration."
104    ))
105    cli.add_argument("--clean", action="store_true", help=(
106        "Clean compilation results and exit."
107    ))
108    args = cli.parse_args()
109
110    modules = read_list_from_file(args.module_list)
111    embedded_modules = read_list_from_file(args.embedded_module_list)
112    depends = set(read_list_from_file(args.depends_list))
113
114    if args.clean:
115        for module in modules + embedded_modules:
116            module = os.path.splitext(module)[0]
117            remove_if_exists(module + '.cpp')
118            remove_if_exists(module + '.html')
119        sys.exit(0)
120
121    from Cython.Compiler import Options
122    Options.annotate = True
123    Options.fast_fail = True
124    # TODO https://github.com/cython/cython/pull/415
125    #      Until then, this has no effect.
126    Options.short_cfilenm = '"cpp"'
127
128    cythonize_cpp_wrapper(modules)
129
130    Options.embed = "main"
131
132    cythonize_cpp_wrapper(embedded_modules)
133
134    # verify depends
135    from Cython.Build.Dependencies import _dep_tree
136    # TODO figure out a less hacky way of getting the depends out of Cython
137    # pylint: disable=no-member, protected-access
138    for module, files in _dep_tree.__cimported_files_cache.items():
139        for filename in files:
140            if not filename.startswith('.'):
141                # system include
142                continue
143
144            if os.path.realpath(os.path.abspath(filename)) not in depends:
145                print("\x1b[31mERR\x1b[m unlisted dependency: " + filename)
146                sys.exit(1)
147
148
149if __name__ == '__main__':
150    main()
151