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