e9a93c654a194bfb455ab2c391719c0e3daa969e
[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     if not cpp_cmd:
78         print("Warning: could not guess preprocessor, using env's CC")
79         cpp_cmd = os.environ.get('CC', 'cc').split()
80         cpp_cmd += ['-E']
81     if 'emcc' in cpp_cmd:
82         cpp_cmd += ['-x', 'c'] # emcc defaults to c++, force C language
83     return cpp_cmd
84
85
86 def get_c_declarations(header=header, usedouble=False):
87     ''' return a dense and preprocessed  string of all c declarations implied by aubio.h
88     '''
89     cpp_output = get_cpp_output(header=header, usedouble=usedouble)
90     return filter_cpp_output (cpp_output)
91
92
93 def get_cpp_output(header=header, usedouble=False):
94     ''' find and run a C pre-processor on aubio.h '''
95     cpp_cmd = get_preprocessor()
96
97     macros = [('AUBIO_UNSTABLE', 1)]
98     if usedouble:
99         macros += [('HAVE_AUBIO_DOUBLE', 1)]
100
101     if not os.path.isfile(header):
102         raise Exception("could not find include file " + header)
103
104     includes = [os.path.dirname(header)]
105     cpp_cmd += distutils.ccompiler.gen_preprocess_options(macros, includes)
106     cpp_cmd += [header]
107
108     print("Running command: {:s}".format(" ".join(cpp_cmd)))
109     proc = subprocess.Popen(cpp_cmd,
110                             stderr=subprocess.PIPE,
111                             stdout=subprocess.PIPE)
112     assert proc, 'Proc was none'
113     cpp_output = proc.stdout.read()
114     err_output = proc.stderr.read()
115     if err_output:
116         print("Warning: preprocessor produced errors or warnings:\n%s" \
117                 % err_output.decode('utf8'))
118     if not cpp_output:
119         raise_msg = "preprocessor output is empty! Running command " \
120                 + "\"%s\" failed" % " ".join(cpp_cmd)
121         if err_output:
122             raise_msg += " with stderr: \"%s\"" % err_output.decode('utf8')
123         else:
124             raise_msg += " with no stdout or stderr"
125         raise Exception(raise_msg)
126     if not isinstance(cpp_output, list):
127         cpp_output = [l.strip() for l in cpp_output.decode('utf8').split('\n')]
128
129     return cpp_output
130
131 def filter_cpp_output(cpp_raw_output):
132     ''' prepare cpp-output for parsing '''
133     cpp_output = filter(lambda y: len(y) > 1, cpp_raw_output)
134     cpp_output = list(filter(lambda y: not y.startswith('#'), cpp_output))
135
136     i = 1
137     while 1:
138         if i >= len(cpp_output):
139             break
140         if ('{' in cpp_output[i - 1]) and ('}' not in cpp_output[i - 1]) or (';' not in cpp_output[i - 1]):
141             cpp_output[i] = cpp_output[i - 1] + ' ' + cpp_output[i]
142             cpp_output.pop(i - 1)
143         elif ('}' in cpp_output[i]):
144             cpp_output[i] = cpp_output[i - 1] + ' ' + cpp_output[i]
145             cpp_output.pop(i - 1)
146         else:
147             i += 1
148
149     # clean pointer notations
150     tmp = []
151     for l in cpp_output:
152         tmp += [l.replace(' *', ' * ')]
153     cpp_output = tmp
154
155     return cpp_output
156
157
158 def get_cpp_objects_from_c_declarations(c_declarations, skip_objects=None):
159     if skip_objects is None:
160         skip_objects = default_skip_objects
161     typedefs = filter(lambda y: y.startswith('typedef struct _aubio'), c_declarations)
162     cpp_objects = [a.split()[3][:-1] for a in typedefs]
163     cpp_objects_filtered = filter(lambda y: not y[6:-2] in skip_objects, cpp_objects)
164     return cpp_objects_filtered
165
166
167 def get_all_func_names_from_lib(lib):
168     ''' return flat string of all function used in lib
169     '''
170     res = []
171     for _, v in lib.items():
172         if isinstance(v, dict):
173             res += get_all_func_names_from_lib(v)
174         elif isinstance(v, list):
175             for elem in v:
176                 e = elem.split('(')
177                 if len(e) < 2:
178                     continue  # not a function
179                 fname_part = e[0].strip().split(' ')
180                 fname = fname_part[-1]
181                 if fname:
182                     res += [fname]
183                 else:
184                     raise NameError('gen_lib : weird function: ' + str(e))
185
186     return res
187
188
189 def generate_lib_from_c_declarations(cpp_objects, c_declarations):
190     ''' returns a lib from given cpp_object names
191
192     a lib is a dict grouping functions by family (onset,pitch...)
193         each eement is itself a dict of functions grouped by puposes as : 
194         struct, new, del, do, get, set and other
195     '''
196     lib = {}
197
198     for o in cpp_objects:
199         shortname = o
200         if o[:6] == 'aubio_':
201             shortname = o[6:-2]  # without aubio_ prefix and _t suffix
202
203         lib[shortname] = {'struct': [], 'new': [], 'del': [], 'do': [], 'rdo': [], 'get': [], 'set': [], 'other': []}
204         lib[shortname]['longname'] = o
205         lib[shortname]['shortname'] = shortname
206
207         fullshortname = o[:-2]  # name without _t suffix
208
209         for fn in c_declarations:
210             func_name = fn.split('(')[0].strip().split(' ')[-1]
211             if func_name.startswith(fullshortname + '_') or func_name.endswith(fullshortname):
212                 # print "found", shortname, "in", fn
213                 if 'typedef struct ' in fn:
214                     lib[shortname]['struct'].append(fn)
215                 elif '_do' in fn:
216                     lib[shortname]['do'].append(fn)
217                 elif '_rdo' in fn:
218                     lib[shortname]['rdo'].append(fn)
219                 elif 'new_' in fn:
220                     lib[shortname]['new'].append(fn)
221                 elif 'del_' in fn:
222                     lib[shortname]['del'].append(fn)
223                 elif '_get_' in fn:
224                     lib[shortname]['get'].append(fn)
225                 elif '_set_' in fn:
226                     lib[shortname]['set'].append(fn)
227                 else:
228                     # print "no idea what to do about", fn
229                     lib[shortname]['other'].append(fn)
230     return lib
231
232
233 def print_c_declarations_results(lib, c_declarations):
234     for fn in c_declarations:
235         found = 0
236         for o in lib:
237             for family in lib[o]:
238                 if fn in lib[o][family]:
239                     found = 1
240         if found == 0:
241             print("missing", fn)
242
243     for o in lib:
244         for family in lib[o]:
245             if type(lib[o][family]) == str:
246                 print("{:15s} {:10s} {:s}".format(o, family, lib[o][family]))
247             elif len(lib[o][family]) == 1:
248                 print("{:15s} {:10s} {:s}".format(o, family, lib[o][family][0]))
249             else:
250                 print("{:15s} {:10s} {:s}".format(o, family, lib[o][family]))
251
252
253 def generate_external(header=header, output_path=output_path, usedouble=False, overwrite=True):
254     if not os.path.isdir(output_path):
255         os.mkdir(output_path)
256     elif not overwrite:
257         return sorted(glob.glob(os.path.join(output_path, '*.c')))
258
259     c_declarations = get_c_declarations(header, usedouble=usedouble)
260     cpp_objects = get_cpp_objects_from_c_declarations(c_declarations)
261
262     lib = generate_lib_from_c_declarations(cpp_objects, c_declarations)
263     # print_c_declarations_results(lib, c_declarations)
264
265     sources_list = []
266     for o in lib:
267         out = source_header
268         mapped = MappedObject(lib[o], usedouble=usedouble)
269         out += mapped.gen_code()
270         output_file = os.path.join(output_path, 'gen-%s.c' % o)
271         with open(output_file, 'w') as f:
272             f.write(out)
273             print("wrote %s" % output_file)
274             sources_list.append(output_file)
275
276     out = source_header
277     out += "#include \"aubio-generated.h\""
278     check_types = "\n     ||  ".join(["PyType_Ready(&Py_%sType) < 0" % o for o in lib])
279     out += """
280
281 int generated_types_ready (void)
282 {{
283   return ({pycheck_types});
284 }}
285 """.format(pycheck_types=check_types)
286
287     add_types = "".join(["""
288   Py_INCREF (&Py_{name}Type);
289   PyModule_AddObject(m, "{name}", (PyObject *) & Py_{name}Type);""".format(name=o) for o in lib])
290     out += """
291
292 void add_generated_objects ( PyObject *m )
293 {{
294 {add_types}
295 }}
296 """.format(add_types=add_types)
297
298     output_file = os.path.join(output_path, 'aubio-generated.c')
299     with open(output_file, 'w') as f:
300         f.write(out)
301         print("wrote %s" % output_file)
302         sources_list.append(output_file)
303
304     objlist = "".join(["extern PyTypeObject Py_%sType;\n" % p for p in lib])
305     out = """// generated list of objects created with gen_external.py
306
307 #include <Python.h>
308 """
309     if usedouble:
310         out += """
311 #ifndef HAVE_AUBIO_DOUBLE
312 #define HAVE_AUBIO_DOUBLE 1
313 #endif
314 """
315     out += """
316 {objlist}
317 int generated_objects ( void );
318 void add_generated_objects( PyObject *m );
319 """.format(objlist=objlist)
320
321     output_file = os.path.join(output_path, 'aubio-generated.h')
322     with open(output_file, 'w') as f:
323         f.write(out)
324         print("wrote %s" % output_file)
325         # no need to add header to list of sources
326
327     return sorted(sources_list)
328
329 if __name__ == '__main__':
330     if len(sys.argv) > 1:
331         header = sys.argv[1]
332     if len(sys.argv) > 2:
333         output_path = sys.argv[2]
334     generate_external(header, output_path)