setup.py 20.7 KB
Newer Older
1
#!/usr/bin/env python3
2

3
# Copyright 2011-2016 Kwant authors.
4
#
Christoph Groth's avatar
Christoph Groth committed
5
6
# This file is part of Kwant.  It is subject to the license terms in the file
# LICENSE.rst found in the top-level directory of this distribution and at
7
# http://kwant-project.org/license.  A list of Kwant authors can be found in
Christoph Groth's avatar
Christoph Groth committed
8
# the file AUTHORS.rst at the top-level directory of this distribution and at
9
10
# http://kwant-project.org/authors.

11
from __future__ import print_function
Christoph Groth's avatar
Christoph Groth committed
12
13

import sys
14

15

16
17
18
19
20
21
22
23
24
25
26
def ensure_python(required_version):
    v = sys.version_info
    if v[:3] < required_version:
        error = "This version of Kwant requires Python {} or above.".format(
            ".".join(str(p) for p in required_version))
        if v[0] == 2:
            error += "\nKwant 1.1 is the last version to support Python 2."
        print(error, file=sys.stderr)
        sys.exit(1)

ensure_python((3, 4))
27

28
import re
Christoph Groth's avatar
Christoph Groth committed
29
import os
30
import glob
31
import imp
Christoph Groth's avatar
Christoph Groth committed
32
import subprocess
33
import configparser
34
import collections
35
from setuptools import setup, find_packages, Extension, Command
36
from distutils.errors import DistutilsError, CCompilerError
37
from distutils.command.build import build
Christoph Groth's avatar
Christoph Groth committed
38
39
from setuptools.command.sdist import sdist
from setuptools.command.build_ext import build_ext
40

Christoph Groth's avatar
Christoph Groth committed
41

42
STATIC_VERSION_PATH = ('kwant', '_kwant_version.py')
43

44
45
46
distr_root = os.path.dirname(os.path.abspath(__file__))


47
48
49
50
51
def configure_extensions(exts, aliases=(), build_summary=None):
    """Modify extension configuration according to the configuration file

    `exts` must be a dict of (name, kwargs) tuples that can be used like this:
    `Extension(name, **kwargs).  This function modifies the kwargs according to
52
53
54
    the configuration file.

    This function modifies `sys.argv`.
55
    """
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    global config_file, config_file_present

    #### Determine the name of the configuration file.
    config_file_option = '--configfile'
    # Handle command line option
    for i, opt in enumerate(sys.argv):
        if not opt.startswith(config_file_option):
            continue
        l, _, config_file = opt.partition('=')
        if l != config_file_option or not config_file:
            print('error: Expecting {}=PATH'.format(config_file_option),
                  file=sys.stderr)
            exit(1)
        sys.argv.pop(i)
        break
    else:
        config_file = 'build.conf'
73
74
75
76

    #### Read build configuration file.
    configs = configparser.ConfigParser()
    try:
77
        with open(config_file) as f:
78
79
80
81
82
83
84
85
86
87
88
            configs.read_file(f)
    except IOError:
        config_file_present = False
    else:
        config_file_present = True

    #### Handle section aliases.
    for short, long in aliases:
        if short in configs:
            if long in configs:
                print('Error: both {} and {} sections present in {}.'.format(
89
                    short, long, config_file))
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
                exit(1)
            configs[long] = configs[short]
            del configs[short]

    #### Apply config from file.  Use [DEFAULT] section for missing sections.
    defaultconfig = configs.defaults()
    for name, kwargs in exts.items():
        config = configs[name] if name in configs else defaultconfig
        for key, value in config.items():

            # Most, but not all, keys are lists of strings
            if key == 'language':
                pass
            elif key == 'optional':
                value = bool(int(value))
            else:
                value = value.split()

            if key == 'define_macros':
                value = [tuple(entry.split('=', maxsplit=1))
                         for entry in value]
                value = [(entry[0], None) if len(entry) == 1 else entry
                         for entry in value]

            if key in kwargs:
                msg = 'Caution: user config in file {} shadows {}.{}.'
                if build_summary is not None:
117
                    build_summary.append(msg.format(config_file, name, key))
118
119
            kwargs[key] = value

120
        kwargs.setdefault('depends', []).append(config_file)
121
122
123
124
125
126
        if config is not defaultconfig:
            del configs[name]

    unknown_sections = configs.sections()
    if unknown_sections:
        print('Error: Unknown sections in file {}: {}'.format(
127
            config_file, ', '.join(unknown_sections)))
128
129
130
131
132
        exit(1)

    return exts


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def get_version():
    global version, version_is_from_git

    # Let Kwant itself determine its own version.  We cannot simply import
    # kwant, as it is not built yet.
    _dont_write_bytecode_saved = sys.dont_write_bytecode
    sys.dont_write_bytecode = True
    _common = imp.load_source('_common', 'kwant/_common.py')
    sys.dont_write_bytecode = _dont_write_bytecode_saved

    version = _common.version
    version_is_from_git = _common.version_is_from_git


def init_cython():
148
149
150
151
152
153
154
155
156
157
158
159
160
161
    """Set the global variable `cythonize` (and other related globals).

    The variable `cythonize` can be in three states:

    * If Cython should be run and is ready, it contains the `cythonize()`
      function.

    * If Cython is not to be run, it contains `False`.

    * If Cython should, but cannot be run it contains `None`.  A help message
      on how to solve the problem is stored in `cython_help`.

    This function modifies `sys.argv`.
    """
162
    global cythonize, cython_help
163

164
    cython_option = '--cython'
165
    required_cython_version = (0, 22)
166
    try:
167
        sys.argv.remove(cython_option)
168
        cythonize = True
169
    except ValueError:
170
        cythonize = version_is_from_git
171

172
    if cythonize:
173
174
175
176
        try:
            import Cython
            from Cython.Build import cythonize
        except ImportError:
177
            cythonize = None
178
        else:
179
            #### Get Cython version.
180
181
182
183
184
185
186
187
            match = re.match('([0-9.]*)(.*)', Cython.__version__)
            cython_version = [int(n) for n in match.group(1).split('.')]
            # Decrease version if the version string contains a suffix.
            if match.group(2):
                while cython_version[-1] == 0:
                    cython_version.pop()
                cython_version[-1] -= 1
            cython_version = tuple(cython_version)
188

189
            if cython_version < required_cython_version:
190
191
                cythonize = None

192
193
194
195
196
        if cythonize is None:
            msg = ("Install Cython >= {0} or use"
                    " a source distribution (tarball) of Kwant.")
            ver = '.'.join(str(e) for e in required_cython_version)
            cython_help = msg.format(ver)
197
198
    else:
        msg = "Run setup.py with the {} option to enable Cython."
199
        cython_help = msg.format(cython_option)
200

201

202
203
204
def banner(title=''):
    starred = title.center(79, '*')
    return '\n' + starred if title else starred
205

Christoph Groth's avatar
Christoph Groth committed
206

Christoph Groth's avatar
Christoph Groth committed
207
208
class kwant_build_ext(build_ext):
    def run(self):
209
210
211
212
213
        if not config_file_present:
            # Create an empty config file if none is present so that the
            # extensions will not be rebuilt each time.  Only depending on the
            # config file if it is present would make it impossible to detect a
            # necessary rebuild due to a deleted config file.
214
215
216
            with open(config_file, 'w') as f:
                f.write('# Build configuration created by setup.py '
                        '- feel free to modify.\n')
217

Christoph Groth's avatar
Christoph Groth committed
218
219
220
        try:
            build_ext.run(self)
        except (DistutilsError, CCompilerError):
221
222
            error_msg = self.__error_msg.format(
                header=banner(' Error '), sep=banner())
223
            print(error_msg.format(file=config_file, summary=build_summary),
224
                  file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
225
            raise
226
        print(banner(' Build summary '), *build_summary, sep='\n')
227
        print(banner())
Christoph Groth's avatar
Christoph Groth committed
228

229
230
231
232
233
234
235
236
237
238
239
    __error_msg = """{header}
The compilation of Kwant has failed.  Please examine the error message
above and consult the installation instructions in README.rst.
You might have to customize {{file}}.

Build configuration was:

{{summary}}
{sep}
"""

240

241
class kwant_build_tut(Command):
242
243
244
245
246
247
248
249
250
251
    description = "build the tutorial scripts"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
252
253
254
255
256
        tut_dir = 'tutorial'
        if not os.path.exists(tut_dir):
            os.mkdir(tut_dir)
        for in_fname in glob.glob('doc/source/tutorial/*.py'):
            out_fname = os.path.join(tut_dir, os.path.basename(in_fname))
257
258
259
            with open(in_fname) as in_file:
                with open(out_fname, 'w') as out_file:
                    for line in in_file:
260
                        if not line.startswith('#HIDDEN'):
261
                            out_file.write(line)
262
263
264
265
266
267


# Our version of the "build" command also makes sure the tutorial is made.
# Even though the tutorial is not necessary for installation, and "build" is
# supposed to make everything needed to install, this is a robust way to ensure
# that the tutorial is present.
268
269
class kwant_build(build):
    sub_commands = [('build_tut', None)] + build.sub_commands
270

271
    def run(self):
272
        build.run(self)
273
274
        write_version(os.path.join(self.build_lib, *STATIC_VERSION_PATH))

275
276

def git_lsfiles():
277
    if not version_is_from_git:
278
279
        return

280
    try:
281
        p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
282
283
284
285
286
287
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return

    if p.wait() != 0:
        return
Joseph Weston's avatar
Joseph Weston committed
288
    return p.communicate()[0].decode().split('\n')[:-1]
289
290


291
292
293
294
# Make the command "sdist" depend on "build".  This verifies that the
# distribution in the current state actually builds.  It also makes sure that
# the Cython-made C files and the tutorial will be included in the source
# distribution and that they will be up-to-date.
295
296
class kwant_sdist(sdist):
    sub_commands = [('build', None)] + sdist.sub_commands
297
298

    def run(self):
Christoph Groth's avatar
Christoph Groth committed
299
300
301
302
303
304
305
306
307
        """
        Create MANIFEST.in from git if possible, otherwise check that MANIFEST.in
        is present.

        Right now (2015) generating MANIFEST.in seems to be the only way to
        include files in the source distribution that setuptools does not think
        should be there.  Setting include_package_data to True makes setuptools
        include *.pyx and other source files in the binary distribution.
        """
308
309
        manifest_in_file = 'MANIFEST.in'
        manifest = os.path.join(distr_root, manifest_in_file)
310
311
        names = git_lsfiles()
        if names is None:
Christoph Groth's avatar
Christoph Groth committed
312
            if not (os.path.isfile(manifest) and os.access(manifest, os.R_OK)):
313
                print("Error:", manifest_in_file,
Christoph Groth's avatar
Christoph Groth committed
314
315
                      "file is missing and Git is not available"
                      " to regenerate it.", file=sys.stderr)
316
317
                exit(1)
        else:
Christoph Groth's avatar
Christoph Groth committed
318
            with open(manifest, 'w') as f:
319
320
321
322
323
                for name in names:
                    a, sep, b = name.rpartition('/')
                    if b == '.gitignore':
                        continue
                    stem, dot, extension = b.rpartition('.')
Christoph Groth's avatar
Christoph Groth committed
324
                    f.write('include {}'.format(name))
325
                    if extension == 'pyx':
Christoph Groth's avatar
Christoph Groth committed
326
327
                        f.write(''.join([' ', a, sep, stem, dot, 'c']))
                    f.write('\n')
328

329
        sdist.run(self)
330
331

        if names is None:
332
333
            msg = ("Git was not available to generate the list of files to be "
                   "included in the\nsource distribution. The old {} was used.")
334
            msg = msg.format(manifest_in_file)
335
            print(banner(' Caution '), msg, banner(), sep='\n', file=sys.stderr)
336

337
    def make_release_tree(self, base_dir, files):
338
        sdist.make_release_tree(self, base_dir, files)
339
        write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
340

Christoph Groth's avatar
Christoph Groth committed
341

342
343
344
def write_version(fname):
    # This could be a hard link, so try to delete it first.  Is there any way
    # to do this atomically together with opening?
345
    try:
346
347
348
349
350
        os.remove(fname)
    except OSError:
        pass
    with open(fname, 'w') as f:
        f.write("# This file has been created by setup.py.\n")
Christoph Groth's avatar
Christoph Groth committed
351
        f.write("version = '{}'\n".format(version))
Christoph Groth's avatar
Christoph Groth committed
352
353


354
355
356
def long_description():
    text = []
    try:
357
        with open('README.rst') as f:
358
            for line in f:
359
                if line.startswith('See also in this directory:'):
360
361
                    break
                text.append(line.rstrip())
362
363
            while text[-1] == "":
                text.pop()
364
365
366
367
368
    except:
        return ''
    return '\n'.join(text)


369
370
371
372
373
374
375
376
377
378
379
380
def search_mumps():
    """Return the configuration for MUMPS if it is available in a known way.

    This is known to work with the MUMPS provided by the Debian package
    libmumps-scotch-dev."""

    libs = ['zmumps_scotch', 'mumps_common_scotch', 'pord', 'mpiseq_scotch',
            'gfortran']

    cmd = ['gcc']
    cmd.extend(['-l' + lib for lib in libs])
    cmd.extend(['-o/dev/null', '-xc', '-'])
Christoph Groth's avatar
Christoph Groth committed
381
    try:
382
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
Christoph Groth's avatar
Christoph Groth committed
383
384
385
    except OSError:
        pass
    else:
386
        p.communicate(input=b'int main() {}\n')
387
388
        if p.wait() == 0:
            return {'libraries': libs}
Christoph Groth's avatar
Christoph Groth committed
389
390
391
    return {}


392
393
394
395
def configure_special_extensions(exts, build_summary):
    #### Special config for LAPACK.
    lapack = exts['kwant.linalg.lapack']
    if 'libraries' in lapack:
Christoph Groth's avatar
Christoph Groth committed
396
397
        build_summary.append('User-configured LAPACK and BLAS')
    else:
398
        lapack['libraries'] = ['lapack', 'blas']
Christoph Groth's avatar
Christoph Groth committed
399
        build_summary.append('Default LAPACK and BLAS')
400
401
402
403

    #### Special config for MUMPS.
    mumps = exts['kwant.linalg._mumps']
    if 'libraries' in mumps:
Christoph Groth's avatar
Christoph Groth committed
404
405
        build_summary.append('User-configured MUMPS')
    else:
406
407
408
409
        kwargs = search_mumps()
        if kwargs:
            for key, value in kwargs.items():
                mumps.setdefault(key, []).extend(value)
410
            build_summary.append('Auto-configured MUMPS')
411
412
        else:
            mumps = None
413
            del exts['kwant.linalg._mumps']
414
            build_summary.append('No MUMPS support')
Christoph Groth's avatar
Christoph Groth committed
415

416
417
418
419
420
    if mumps:
        # Copy config from LAPACK.
        for key, value in lapack.items():
            if key not in ['sources', 'depends']:
                mumps.setdefault(key, []).extend(value)
Christoph Groth's avatar
Christoph Groth committed
421

422
    return exts
Christoph Groth's avatar
Christoph Groth committed
423

424
425

def maybe_cythonize(exts):
426
    """Prepare a list of `Extension` instances, ready for `setup()`.
427

428
429
    The argument `exts` must be a mapping of names to kwargs to be passed
    on to `Extension`.
430
431
432
433

    If Cython is to be run, create the extensions and calls `cythonize()` on
    them.  If Cython is not to be run, replace .pyx file with .c or .cpp,
    check timestamps, and create the extensions.
434
    """
435
    if cythonize:
436
437
438
        return cythonize([Extension(name, **kwargs)
                          for name, kwargs in exts.items()],
                         language_level=3,
439
                         compiler_directives={'linetrace': True})
440
441
442
443

    # Cython is not going to be run: replace pyx extension by that of
    # the shipped translated file.

Christoph Groth's avatar
Christoph Groth committed
444
    result = []
445
    problematic_files = []
446
447
    for name, kwargs in exts.items():
        language = kwargs.get('language')
448
449
450
451
452
453
454
455
456
457
458
459
        if language is None:
            ext = '.c'
        elif language == 'c':
            ext = '.c'
        elif language == 'c++':
            ext = '.cpp'
        else:
            print('Unknown language: {}'.format(language), file=sys.stderr)
            exit(1)

        pyx_files = []
        cythonized_files = []
460
461
        sources = []
        for f in kwargs['sources']:
462
463
464
465
            if f.endswith('.pyx'):
                pyx_files.append(f)
                f = f.rstrip('.pyx') + ext
                cythonized_files.append(f)
466
467
            sources.append(f)
        kwargs['sources'] = sources
468
469
470
471
472
473

        # Complain if cythonized files are older than Cython source files.
        try:
            cythonized_oldest = min(os.stat(f).st_mtime
                                    for f in cythonized_files)
        except OSError:
474
475
476
            msg = "Cython-generated file {} is missing."
            print(banner(" Error "), msg.format(f), "",
                  cython_help, banner(), sep="\n", file=sys.stderr)
477
478
            exit(1)

479
        for f in pyx_files + kwargs.get('depends', []):
480
            if f == config_file:
481
482
483
484
485
                # The config file is only a dependency for the compilation
                # of the cythonized file, not for the cythonization.
                continue
            if os.stat(f).st_mtime > cythonized_oldest:
                problematic_files.append(f)
Christoph Groth's avatar
Christoph Groth committed
486

487
        result.append(Extension(name, **kwargs))
Christoph Groth's avatar
Christoph Groth committed
488

489
    if problematic_files:
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
        msg = ("Some Cython source files are newer than files that have "
               "been derived from them:\n{}")
        msg = msg.format(", ".join(problematic_files))

        # Cython should be run but won't.  Signal an error if this is because
        # Cython *cannot* be run, warn otherwise.
        error = cythonize is None
        if cythonize is False:
            dontworry = ('(Do not worry about this if you are building Kwant '
                         'from unmodified sources,\n'
                         'e.g. with "pip install".)\n\n')
            msg = dontworry + msg

        print(banner(" Error " if error else " Caution "), msg, "",
              cython_help, banner(), sep="\n", file=sys.stderr)
        if error:
506
507
            exit(1)

Christoph Groth's avatar
Christoph Groth committed
508
509
    return result

510

Christoph Groth's avatar
Christoph Groth committed
511
def main():
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
    exts = collections.OrderedDict([
        ('kwant._system',
         dict(sources=['kwant/_system.pyx'],
              include_dirs=['kwant/graph'])),
        ('kwant.operator',
         dict(sources=['kwant/operator.pyx'],
              include_dirs=['kwant/graph'])),
        ('kwant.graph.core',
         dict(sources=['kwant/graph/core.pyx'],
              depends=['kwant/graph/core.pxd', 'kwant/graph/defs.h',
                       'kwant/graph/defs.pxd'])),
        ('kwant.graph.utils',
         dict(sources=['kwant/graph/utils.pyx'],
              depends=['kwant/graph/defs.h', 'kwant/graph/defs.pxd',
                       'kwant/graph/core.pxd'])),
        ('kwant.graph.slicer',
         dict(sources=['kwant/graph/slicer.pyx',
                       'kwant/graph/c_slicer/partitioner.cc',
                       'kwant/graph/c_slicer/slicer.cc'],
              depends=['kwant/graph/defs.h', 'kwant/graph/defs.pxd',
                       'kwant/graph/core.pxd', 'kwant/graph/c_slicer.pxd',
                       'kwant/graph/c_slicer/bucket_list.h',
                       'kwant/graph/c_slicer/graphwrap.h',
                       'kwant/graph/c_slicer/partitioner.h',
                       'kwant/graph/c_slicer/slicer.h'])),
        ('kwant.linalg.lapack',
         dict(sources=['kwant/linalg/lapack.pyx'],
              depends=['kwant/linalg/f_lapack.pxd'])),
        ('kwant.linalg._mumps',
         dict(sources=['kwant/linalg/_mumps.pyx'],
              depends=['kwant/linalg/cmumps.pxd']))])

544
545
546
547
548
549
550
551
552
553
554
    # Add NumPy header path to include_dirs of all the extensions.
    try:
        import numpy
    except ImportError:
        print(banner(' Caution '), 'NumPy header directory cannot be determined'
              ' ("import numpy" failed).', banner(), sep='\n', file=sys.stderr)
    else:
        numpy_include = numpy.get_include()
        for ext in exts.values():
            ext.setdefault('include_dirs', []).append(numpy_include)

555
556
    aliases = [('lapack', 'kwant.linalg.lapack'),
               ('mumps', 'kwant.linalg._mumps')]
557

558
559
560
    get_version()
    init_cython()

561
    global build_summary
562
    build_summary = []
563
    exts = configure_extensions(exts, aliases, build_summary)
564
565
566
    exts = configure_special_extensions(exts, build_summary)
    exts = maybe_cythonize(exts)

567
568
569
570
571
572
573
574
575
576
577
578
    classifiers = """\
        Development Status :: 5 - Production/Stable
        Intended Audience :: Science/Research
        Intended Audience :: Developers
        Programming Language :: Python :: 3 :: Only
        Topic :: Software Development
        Topic :: Scientific/Engineering
        Operating System :: POSIX
        Operating System :: Unix
        Operating System :: MacOS :: MacOS X
        Operating System :: Microsoft :: Windows"""

Christoph Groth's avatar
Christoph Groth committed
579
    setup(name='kwant',
580
          version=version,
581
582
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
583
          author_email='authors@kwant-project.org',
584
          description=("Package for numerical quantum transport calculations "
585
                       "(Python 3 version)"),
586
587
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
588
589
          url="http://kwant-project.org/",
          license="BSD",
590
          packages=find_packages('.'),
591
          cmdclass={'build': kwant_build,
592
                    'sdist': kwant_sdist,
593
                    'build_ext': kwant_build_ext,
Christoph Groth's avatar
Christoph Groth committed
594
                    'build_tut': kwant_build_tut},
595
          ext_modules=exts,
596
597
          setup_requires=['pytest-runner >= 2.7'],
          tests_require=['numpy > 1.6.1', 'pytest >= 2.6.3'],
598
          install_requires=['numpy > 1.6.1', 'scipy >= 0.11.0', 'tinyarray'],
Joseph Weston's avatar
Joseph Weston committed
599
          extras_require={'plotting': 'matplotlib >= 1.2'},
600
          classifiers=[c.strip() for c in classifiers.split('\n')])
601

Christoph Groth's avatar
Christoph Groth committed
602
603
if __name__ == '__main__':
    main()