python/lib/gen_external.py: verbose compiler warnings
[aubio.git] / python / lib / gen_external.py
1 import distutils.ccompiler
2 import sys
3 import os
4 import subprocess
5 import glob
6
7 header = os.path.join('src', 'aubio.h')
8 output_path = os.path.join('python', 'gen')
9
10 source_header = """// this file is generated! do not modify
11 #include "aubio-types.h"
12 """
13
14 default_skip_objects = [
15     # already in ext/
16     'fft',
17     'pvoc',
18     'filter',
19     'filterbank',
20     # AUBIO_UNSTABLE
21     'hist',
22     'parameter',
23     'scale',
24     'beattracking',
25     'resampler',
26     'peakpicker',
27     'pitchfcomb',
28     'pitchmcomb',
29     'pitchschmitt',
30     'pitchspecacf',
31     'pitchyin',
32     'pitchyinfft',
33     'pitchyinfast',
34     'sink',
35     'sink_apple_audio',
36     'sink_sndfile',
37     'sink_wavwrite',
38     #'mfcc',
39     'source',
40     'source_apple_audio',
41     'source_sndfile',
42     'source_avcodec',
43     'source_wavread',
44     #'sampler',
45     'audio_unit',
46     'spectral_whitening',
47 ]
48
49
50 def get_preprocessor():
51     # findout which compiler to use
52     from distutils.sysconfig import customize_compiler
53     compiler_name = distutils.ccompiler.get_default_compiler()
54     compiler = distutils.ccompiler.new_compiler(compiler=compiler_name)
55     try:
56         customize_compiler(compiler)
57     except AttributeError as e:
58         print("Warning: failed customizing compiler ({:s})".format(repr(e)))
59
60     if hasattr(compiler, 'initialize'):
61         try:
62             compiler.initialize()
63         except ValueError as e:
64             print("Warning: failed initializing compiler ({:s})".format(repr(e)))
65
66     cpp_cmd = None
67     if hasattr(compiler, 'preprocessor'):  # for unixccompiler
68         cpp_cmd = compiler.preprocessor
69     elif hasattr(compiler, 'compiler'):  # for ccompiler
70         cpp_cmd = compiler.compiler.split()
71         cpp_cmd += ['-E']
72     elif hasattr(compiler, 'cc'):  # for msvccompiler
73         cpp_cmd = compiler.cc.split()
74         cpp_cmd += ['-E']
75
76     if not cpp_cmd:
77         print("Warning: could not guess preprocessor, using env's CC")
78         cpp_cmd = os.environ.get('CC', 'cc').split()
79         cpp_cmd += ['-E']
80     if 'emcc' in cpp_cmd:
81         cpp_cmd += ['-x', 'c'] # emcc defaults to c++, force C language
82     return cpp_cmd
83
84
85 def get_c_declarations(header=header, usedouble=False):
86     ''' return a dense and preprocessed  string of all c declarations implied by aubio.h
87     '''
88     cpp_cmd = get_preprocessor()
89
90     macros = [('AUBIO_UNSTABLE', 1)]
91     if usedouble:
92         macros += [('HAVE_AUBIO_DOUBLE', 1)]
93
94     if not os.path.isfile(header):
95         raise Exception("could not find include file " + header)
96
97     includes = [os.path.dirname(header)]
98     cpp_cmd += distutils.ccompiler.gen_preprocess_options(macros, includes)
99     cpp_cmd += [header]
100
101     print("Running command: {:s}".format(" ".join(cpp_cmd)))
102     proc = subprocess.Popen(cpp_cmd,
103                             stderr=subprocess.PIPE,
104                             stdout=subprocess.PIPE)
105     assert proc, 'Proc was none'
106     cpp_output = proc.stdout.read()
107     err_output = proc.stderr.read()
108     if err_output:
109         print("Warning: preprocessor produced errors or warnings:\n%s" \
110                 % err_output.decode('utf8'))
111     if not cpp_output:
112         raise_msg = "preprocessor output is empty! Running command " \
113                 + "\"%s\" failed" % " ".join(cpp_cmd)
114         if err_output:
115             raise_msg += " with stderr: \"%s\"" % err_output.decode('utf8')
116         else:
117             raise_msg += " with no stdout or stderr"
118         raise Exception(raise_msg)
119     if not isinstance(cpp_output, list):
120         cpp_output = [l.strip() for l in cpp_output.decode('utf8').split('\n')]
121
122     cpp_output = filter(lambda y: len(y) > 1, cpp_output)
123     cpp_output = list(filter(lambda y: not y.startswith('#'), cpp_output))
124
125     i = 1
126     while 1:
127         if i >= len(cpp_output):
128             break
129         if ('{' in cpp_output[i - 1]) and ('}' not in cpp_output[i - 1]) or (';' not in cpp_output[i - 1]):
130             cpp_output[i] = cpp_output[i - 1] + ' ' + cpp_output[i]
131             cpp_output.pop(i - 1)
132         elif ('}' in cpp_output[i]):
133             cpp_output[i] = cpp_output[i - 1] + ' ' + cpp_output[i]
134             cpp_output.pop(i - 1)
135         else:
136             i += 1
137
138     # clean pointer notations
139     tmp = []
140     for l in cpp_output:
141         tmp += [l.replace(' *', ' * ')]
142     cpp_output = tmp
143
144     return cpp_output
145
146
147 def get_cpp_objects_from_c_declarations(c_declarations, skip_objects=None):
148     if skip_objects is None:
149         skip_objects = default_skip_objects
150     typedefs = filter(lambda y: y.startswith('typedef struct _aubio'), c_declarations)
151     cpp_objects = [a.split()[3][:-1] for a in typedefs]
152     cpp_objects_filtered = filter(lambda y: not y[6:-2] in skip_objects, cpp_objects)
153     return cpp_objects_filtered
154
155
156 def get_all_func_names_from_lib(lib):
157     ''' return flat string of all function used in lib
158     '''
159     res = []
160     for _, v in lib.items():
161         if isinstance(v, dict):
162             res += get_all_func_names_from_lib(v)
163         elif isinstance(v, list):
164             for elem in v:
165                 e = elem.split('(')
166                 if len(e) < 2:
167                     continue  # not a function
168                 fname_part = e[0].strip().split(' ')
169                 fname = fname_part[-1]
170                 if fname:
171                     res += [fname]
172                 else:
173                     raise NameError('gen_lib : weird function: ' + str(e))
174
175     return res
176
177
178 def generate_lib_from_c_declarations(cpp_objects, c_declarations):
179     ''' returns a lib from given cpp_object names
180
181     a lib is a dict grouping functions by family (onset,pitch...)
182         each eement is itself a dict of functions grouped by puposes as : 
183         struct, new, del, do, get, set and other
184     '''
185     lib = {}
186
187     for o in cpp_objects:
188         shortname = o
189         if o[:6] == 'aubio_':
190             shortname = o[6:-2]  # without aubio_ prefix and _t suffix
191
192         lib[shortname] = {'struct': [], 'new': [], 'del': [], 'do': [], 'rdo': [], 'get': [], 'set': [], 'other': []}
193         lib[shortname]['longname'] = o
194         lib[shortname]['shortname'] = shortname
195
196         fullshortname = o[:-2]  # name without _t suffix
197
198         for fn in c_declarations:
199             func_name = fn.split('(')[0].strip().split(' ')[-1]
200             if func_name.startswith(fullshortname + '_') or func_name.endswith(fullshortname):
201                 # print "found", shortname, "in", fn
202                 if 'typedef struct ' in fn:
203                     lib[shortname]['struct'].append(fn)
204                 elif '_do' in fn:
205                     lib[shortname]['do'].append(fn)
206                 elif '_rdo' in fn:
207                     lib[shortname]['rdo'].append(fn)
208                 elif 'new_' in fn:
209                     lib[shortname]['new'].append(fn)
210                 elif 'del_' in fn:
211                     lib[shortname]['del'].append(fn)
212                 elif '_get_' in fn:
213                     lib[shortname]['get'].append(fn)
214                 elif '_set_' in fn:
215                     lib[shortname]['set'].append(fn)
216                 else:
217                     # print "no idea what to do about", fn
218                     lib[shortname]['other'].append(fn)
219     return lib
220
221
222 def print_c_declarations_results(lib, c_declarations):
223     for fn in c_declarations:
224         found = 0
225         for o in lib:
226             for family in lib[o]:
227                 if fn in lib[o][family]:
228                     found = 1
229         if found == 0:
230             print("missing", fn)
231
232     for o in lib:
233         for family in lib[o]:
234             if type(lib[o][family]) == str:
235                 print("{:15s} {:10s} {:s}".format(o, family, lib[o][family]))
236             elif len(lib[o][family]) == 1:
237                 print("{:15s} {:10s} {:s}".format(o, family, lib[o][family][0]))
238             else:
239                 print("{:15s} {:10s} {:s}".format(o, family, lib[o][family]))
240
241
242 def generate_external(header=header, output_path=output_path, usedouble=False, overwrite=True):
243     if not os.path.isdir(output_path):
244         os.mkdir(output_path)
245     elif not overwrite:
246         return sorted(glob.glob(os.path.join(output_path, '*.c')))
247
248     c_declarations = get_c_declarations(header, usedouble=usedouble)
249     cpp_objects = get_cpp_objects_from_c_declarations(c_declarations)
250
251     lib = generate_lib_from_c_declarations(cpp_objects, c_declarations)
252     # print_c_declarations_results(lib, c_declarations)
253
254     sources_list = []
255     try:
256         from .gen_code import MappedObject
257     except (SystemError, ValueError):
258         from gen_code import MappedObject
259     for o in lib:
260         out = source_header
261         mapped = MappedObject(lib[o], usedouble=usedouble)
262         out += mapped.gen_code()
263         output_file = os.path.join(output_path, 'gen-%s.c' % o)
264         with open(output_file, 'w') as f:
265             f.write(out)
266             print("wrote %s" % output_file)
267             sources_list.append(output_file)
268
269     out = source_header
270     out += "#include \"aubio-generated.h\""
271     check_types = "\n     ||  ".join(["PyType_Ready(&Py_%sType) < 0" % o for o in lib])
272     out += """
273
274 int generated_types_ready (void)
275 {{
276   return ({pycheck_types});
277 }}
278 """.format(pycheck_types=check_types)
279
280     add_types = "".join(["""
281   Py_INCREF (&Py_{name}Type);
282   PyModule_AddObject(m, "{name}", (PyObject *) & Py_{name}Type);""".format(name=o) for o in lib])
283     out += """
284
285 void add_generated_objects ( PyObject *m )
286 {{
287 {add_types}
288 }}
289 """.format(add_types=add_types)
290
291     output_file = os.path.join(output_path, 'aubio-generated.c')
292     with open(output_file, 'w') as f:
293         f.write(out)
294         print("wrote %s" % output_file)
295         sources_list.append(output_file)
296
297     objlist = "".join(["extern PyTypeObject Py_%sType;\n" % p for p in lib])
298     out = """// generated list of objects created with gen_external.py
299
300 #include <Python.h>
301 """
302     if usedouble:
303         out += """
304 #ifndef HAVE_AUBIO_DOUBLE
305 #define HAVE_AUBIO_DOUBLE 1
306 #endif
307 """
308     out += """
309 {objlist}
310 int generated_objects ( void );
311 void add_generated_objects( PyObject *m );
312 """.format(objlist=objlist)
313
314     output_file = os.path.join(output_path, 'aubio-generated.h')
315     with open(output_file, 'w') as f:
316         f.write(out)
317         print("wrote %s" % output_file)
318         # no need to add header to list of sources
319
320     return sorted(sources_list)
321
322 if __name__ == '__main__':
323     if len(sys.argv) > 1:
324         header = sys.argv[1]
325     if len(sys.argv) > 2:
326         output_path = sys.argv[2]
327     generate_external(header, output_path)