1try:
2    # python3 vs. python2
3    from configparser import ConfigParser
4except ImportError:
5    from ConfigParser import SafeConfigParser as ConfigParser
6import io
7import logging
8import numpy.distutils.system_info as numpy_sys
9import numpy
10import os
11from shlex import split as shsplit
12import sys
13
14logger = logging.getLogger('pythran')
15
16
17def get_include():
18    # using / as separator as advised in the distutils doc
19    return (os.path.dirname(os.path.dirname(__file__)) or '.') + '/pythran'
20
21
22class silent(object):
23    '''
24    Silent sys.stderr at the system level
25    '''
26
27    def __enter__(self):
28        try:
29            self.prevfd = os.dup(sys.stderr.fileno())
30            os.close(sys.stderr.fileno())
31        except io.UnsupportedOperation:
32            self.prevfd = None
33
34        self.prevstream = sys.stderr
35        sys.stderr = open(os.devnull, 'r')
36
37    def __exit__(self, exc_type, exc_value, traceback):
38        sys.stderr.close()
39        sys.stderr = self.prevstream
40        if self.prevfd:
41            os.dup2(self.prevfd, sys.stderr.fileno())
42            os.close(self.prevfd)
43
44
45def get_paths_cfg(
46        sys_file='pythran.cfg',
47        platform_file='pythran-{}.cfg'.format(sys.platform),
48        user_file='.pythranrc'
49):
50    sys_config_dir = os.path.dirname(__file__)
51    sys_config_path = os.path.join(sys_config_dir, sys_file)
52
53    platform_config_path = os.path.join(sys_config_dir, platform_file)
54    if not os.path.exists(platform_config_path):
55        platform_config_path = os.path.join(sys_config_dir,
56                                            "pythran-default.cfg")
57
58    user_config_path = os.environ.get('PYTHRANRC', None)
59    if not user_config_path:
60        user_config_dir = os.environ.get('XDG_CONFIG_HOME', None)
61        if not user_config_dir:
62            user_config_dir = os.environ.get('HOME', None)
63        if not user_config_dir:
64            user_config_dir = '~'
65        user_config_path = os.path.expanduser(
66            os.path.join(user_config_dir, user_file))
67    return {"sys": sys_config_path,
68            "platform": platform_config_path,
69            "user": user_config_path}
70
71
72def init_cfg(sys_file, platform_file, user_file, config_args=None):
73    paths = get_paths_cfg(sys_file, platform_file, user_file)
74
75    sys_config_path = paths["sys"]
76    platform_config_path = paths["platform"]
77    user_config_path = paths["user"]
78
79    cfgp = ConfigParser()
80    for required in (sys_config_path, platform_config_path):
81        cfgp.read([required])
82    cfgp.read([user_config_path])
83
84    if config_args is not None:
85        update_cfg(cfgp, config_args)
86
87    return cfgp
88
89
90def update_cfg(cfgp, config_args):
91    # Override the config options with those provided on the command line
92    # e.g. compiler.blas=pythran-openblas.
93    for arg in config_args:
94        try:
95            lhs, rhs = arg.split('=', maxsplit=1)
96            section, item = lhs.split('.')
97            if not cfgp.has_section(section):
98                cfgp.add_section(section)
99            cfgp.set(section, item, rhs)
100        except Exception:
101            pass
102
103
104def lint_cfg(cfgp, **paths):
105    if not paths:
106        paths = get_paths_cfg()
107
108    # Use configuration from sys and platform as "reference"
109    cfgp_ref = ConfigParser()
110    cfgp_ref.read([paths["sys"], paths["platform"]])
111
112    # Check if pythran configuration files exists
113    for loc, path in paths.items():
114        exists = os.path.exists(path)
115
116        msg = " ".join([
117            "{} file".format(loc).rjust(13),
118            "exists:" if exists else "does not exist:",
119            path
120        ])
121        logger.info(msg) if exists else logger.warning(msg)
122
123    for section in cfgp.sections():
124        # Check if section in the current configuration exists in the
125        # reference configuration
126        if cfgp_ref.has_section(section):
127            options = set(cfgp.options(section))
128            options_ref = set(cfgp_ref.options(section))
129
130            # Check if the options in the section are supported by the
131            # reference configuration
132            if options.issubset(options_ref):
133                logger.info(
134                    (
135                        "pythranrc section [{}] is valid and options are "
136                        "correct"
137                    ).format(section)
138                )
139            else:
140                logger.warning(
141                    (
142                        "pythranrc section [{}] is valid but options {} "
143                        "are incorrect!"
144                    ).format(section, options.difference(options_ref))
145                )
146        else:
147            logger.warning("pythranrc section [{}] is invalid!"
148                           .format(section))
149
150
151def make_extension(python, **extra):
152    # load platform specific configuration then user configuration
153    cfg = init_cfg('pythran.cfg',
154                   'pythran-{}.cfg'.format(sys.platform),
155                   '.pythranrc',
156                   extra.get('config', None))
157
158    if 'config' in extra:
159        extra.pop('config')
160
161    def parse_define(define):
162        index = define.find('=')
163        if index < 0:
164            return (define, None)
165        else:
166            return define[:index], define[index + 1:]
167
168    extension = {
169        "language": "c++",
170        # forcing str conversion to handle Unicode case (the default on MS)
171        "define_macros": [str(x) for x in
172                          shsplit(cfg.get('compiler', 'defines'))],
173        "undef_macros": [str(x) for x in
174                         shsplit(cfg.get('compiler', 'undefs'))],
175        "include_dirs": [str(x) for x in
176                         shsplit(cfg.get('compiler', 'include_dirs'))],
177        "library_dirs": [str(x) for x in
178                         shsplit(cfg.get('compiler', 'library_dirs'))],
179        "libraries": [str(x) for x in
180                      shsplit(cfg.get('compiler', 'libs'))],
181        "extra_compile_args": [str(x) for x in
182                               shsplit(cfg.get('compiler', 'cflags'))],
183        "extra_link_args": [str(x) for x in
184                            shsplit(cfg.get('compiler', 'ldflags'))],
185        "extra_objects": []
186    }
187
188    if python:
189        extension['define_macros'].append('ENABLE_PYTHON_MODULE')
190    extension['define_macros'].append(
191        '__PYTHRAN__={}'.format(sys.version_info.major))
192
193    pythonic_dir = get_include()
194
195    extension["include_dirs"].append(pythonic_dir)
196
197    extra.pop('language', None)  # forced to c++ anyway
198    cxx = extra.pop('cxx', None)
199    cc = extra.pop('cc', None)
200
201    if cxx is None:
202        cxx = compiler()
203    if cxx is not None:
204        extension['cxx'] = cxx
205        extension['cc'] = cc or cxx
206
207    # Honor CXXFLAGS (note: Pythran calls this `cflags` everywhere, however the
208    # standard environment variable is `CXXFLAGS` not `CFLAGS`).
209    cflags = os.environ.get('CXXFLAGS', None)
210    if cflags is not None:
211        extension['extra_compile_args'].extend(shsplit(cflags))
212
213    # Honor LDFLAGS
214    ldflags = os.environ.get('LDFLAGS', None)
215    if ldflags is not None:
216        extension['extra_link_args'].extend(shsplit(ldflags))
217
218    for k, w in extra.items():
219        extension[k].extend(w)
220    if cfg.getboolean('pythran', 'complex_hook'):
221        # the patch is *not* portable
222        extension["include_dirs"].append(pythonic_dir + '/pythonic/patch')
223
224    # numpy specific
225    if python:
226        extension['include_dirs'].append(numpy.get_include())
227
228    # blas dependency
229    reserved_blas_entries = 'pythran-openblas', 'none'
230    user_blas = cfg.get('compiler', 'blas')
231    if user_blas == 'pythran-openblas':
232        try:
233            import pythran_openblas as openblas
234            # required to cope with atlas missing extern "C"
235            extension['define_macros'].append('PYTHRAN_BLAS_OPENBLAS')
236            extension['include_dirs'].extend(openblas.include_dirs)
237            extension['extra_objects'].append(
238                os.path.join(openblas.library_dir, openblas.static_library)
239            )
240        except ImportError:
241            logger.warning("Failed to find 'pythran-openblas' package. "
242                           "Please install it or change the compiler.blas "
243                           "setting. Defaulting to 'blas'")
244            user_blas = 'blas'
245    elif user_blas == 'none':
246        extension['define_macros'].append('PYTHRAN_BLAS_NONE')
247
248    if user_blas not in reserved_blas_entries:
249        # Numpy can pollute stdout with checks
250        with silent():
251            numpy_blas = numpy_sys.get_info(user_blas)
252            # required to cope with atlas missing extern "C"
253            extension['define_macros'].append('PYTHRAN_BLAS_{}'
254                                              .format(user_blas.upper()))
255            extension['libraries'].extend(numpy_blas.get('libraries', []))
256            extension['library_dirs'].extend(
257                numpy_blas.get('library_dirs', []))
258            extension['include_dirs'].extend(
259                numpy_blas.get('include_dirs', []))
260
261
262    # final macro normalization
263    extension["define_macros"] = [
264        dm if isinstance(dm, tuple) else parse_define(dm)
265        for dm in extension["define_macros"]]
266    return extension
267
268
269def compiler():
270    """Get compiler to use for C++ to binary process. The precedence for
271    choosing the compiler is as follows::
272
273      1. `CXX` environment variable
274      2. User configuration (~/.pythranrc)
275
276    Returns None if none is set or if it's set to the empty string
277
278    """
279    cfg_cxx = str(cfg.get('compiler', 'CXX'))
280    if not cfg_cxx:
281        cfg_cxx = None
282    return os.environ.get('CXX', cfg_cxx) or None
283
284
285# load platform specific configuration then user configuration
286cfg = init_cfg('pythran.cfg',
287               'pythran-{}.cfg'.format(sys.platform),
288               '.pythranrc')
289
290
291def run():
292    '''
293    Dump on stdout the config flags required to compile pythran-generated code.
294    '''
295    import argparse
296    import distutils.ccompiler
297    import distutils.sysconfig
298    import pythran
299    import numpy
300
301    parser = argparse.ArgumentParser(
302        prog='pythran-config',
303        description='output build options for pythran-generated code',
304        epilog="It's a megablast!"
305    )
306
307    parser.add_argument('--compiler', action='store_true',
308                        help='print default compiler')
309
310    parser.add_argument('--cflags', action='store_true',
311                        help='print compilation flags')
312
313    parser.add_argument('--libs', action='store_true',
314                        help='print linker flags')
315
316    parser.add_argument('--no-python', action='store_true',
317                        help='do not include Python-related flags')
318
319    parser.add_argument('--verbose', '-v', action='count', default=0,
320                        help=(
321                            'verbose mode: [-v] prints warnings if pythranrc '
322                            'has an invalid configuration; use '
323                            '[-vv] for more information')
324                        )
325
326    args = parser.parse_args(sys.argv[1:])
327
328    args.python = not args.no_python
329
330    output = []
331
332    extension = pythran.config.make_extension(python=args.python)
333
334    if args.verbose >= 1:
335        if args.verbose == 1:
336            logger.setLevel(logging.WARNING)
337        else:
338            logger.setLevel(logging.INFO)
339
340        lint_cfg(cfg)
341
342    if args.compiler or args.verbose >= 2:
343        cxx = compiler() or 'c++'
344        logger.info('CXX = '.rjust(10) + cxx)
345        if args.compiler:
346            output.append(cxx)
347
348    compiler_obj = distutils.ccompiler.new_compiler()
349    distutils.sysconfig.customize_compiler(compiler_obj)
350
351    if args.cflags or args.verbose >= 2:
352        def fmt_define(define):
353            name, value = define
354            if value is None:
355                return '-D' + name
356            else:
357                return '-D' + name + '=' + value
358
359        cflags = []
360        cflags.extend(fmt_define(define)
361                      for define in extension['define_macros'])
362        cflags.extend(('-I' + include)
363                      for include in extension['include_dirs'])
364        if args.python:
365            cflags.append('-I' + numpy.get_include())
366            cflags.append('-I' + distutils.sysconfig.get_python_inc())
367
368        logger.info('CXXFLAGS = '.rjust(10) + ' '.join(cflags))
369        if args.cflags:
370            output.extend(cflags)
371
372    if args.libs or args.verbose >= 2:
373        ldflags = []
374        ldflags.extend((compiler_obj.library_dir_option(include))
375                       for include in extension['library_dirs'])
376        ldflags.extend((compiler_obj.library_option(include))
377                       for include in extension['libraries'])
378
379        if args.python:
380            libpl = distutils.sysconfig.get_config_var('LIBPL')
381            if libpl:
382                ldflags.append(libpl)
383            libs = distutils.sysconfig.get_config_var('LIBS')
384            if libs:
385                ldflags.extend(shsplit(libs))
386            ldflags.append(compiler_obj.library_option('python')
387                           + distutils.sysconfig.get_config_var('VERSION'))
388
389        logger.info('LDFLAGS = '.rjust(10) + ' '.join(ldflags))
390        if args.libs:
391            output.extend(ldflags)
392
393    if output:
394        print(' '.join(output))
395
396
397if __name__ == '__main__':
398    run()
399