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