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