source: python/lib/gen_external.py

Last change on this file was 66fff6c, checked in by Paul Brossier <piem@piem.org>, 14 months ago

[py] use universal_newlines=True to prevent decoding errors (see gh-247 gh-248)

  • Property mode set to 100644
File size: 11.1 KB
Line 
1import distutils.ccompiler
2import sys
3import os
4import subprocess
5import glob
6from distutils.sysconfig import customize_compiler
7from gen_code import MappedObject
8
9header = os.path.join('src', 'aubio.h')
10output_path = os.path.join('python', 'gen')
11
12source_header = """// this file is generated! do not modify
13#include "aubio-types.h"
14"""
15
16default_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
52def 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
98def 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
105def 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
144def 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
171def 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
180def 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
202def 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
246def 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
266def 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
294int 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
305void 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}
330int generated_objects ( void );
331void 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
342if __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)
Note: See TracBrowser for help on using the repository browser.