setup.py 18.1 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

v = sys.version_info
16
17
18
19
if v[:2] < (3, 4):
    error = "This version of Kwant requires Python 3.4 or above.\n"
    if v[0] == 2:
        error += "Kwant 1.1 is the last version to support Python 2."
20
21
22
    print(error, file=sys.stderr)
    sys.exit(1)

23
import re
Christoph Groth's avatar
Christoph Groth committed
24
import os
25
import glob
26
import imp
Christoph Groth's avatar
Christoph Groth committed
27
import subprocess
28
import configparser
29
from setuptools import setup, find_packages, Extension, Command
Christoph Groth's avatar
Christoph Groth committed
30
from sysconfig import get_platform
Christoph Groth's avatar
Christoph Groth committed
31
32
from distutils.errors import DistutilsError, DistutilsModuleError, \
    CCompilerError
33
from distutils.command.build import build
Christoph Groth's avatar
Christoph Groth committed
34
35
from setuptools.command.sdist import sdist
from setuptools.command.build_ext import build_ext
36

Christoph Groth's avatar
Christoph Groth committed
37

38
39
CONFIG_FILE = 'build.conf'
README_FILE = 'README.rst'
Christoph Groth's avatar
Christoph Groth committed
40
MANIFEST_IN_FILE = 'MANIFEST.in'
41
README_END_BEFORE = 'See also in this directory:'
42
STATIC_VERSION_PATH = ('kwant', '_kwant_version.py')
Christoph Groth's avatar
Christoph Groth committed
43
REQUIRED_CYTHON_VERSION = (0, 22)
44
CYTHON_OPTION = '--cython'
45
CYTHON_TRACE_OPTION = '--cython-trace'
46
47
48
TUT_DIR = 'tutorial'
TUT_GLOB = 'doc/source/tutorial/*.py'
TUT_HIDDEN_PREFIX = '#HIDDEN'
Joseph Weston's avatar
Joseph Weston committed
49
CLASSIFIERS = """\
50
    Development Status :: 5 - Production/Stable
Joseph Weston's avatar
Joseph Weston committed
51
52
53
54
55
56
57
58
59
60
    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"""

61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
distr_root = os.path.dirname(os.path.abspath(__file__))


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():
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    """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`.
    """
    global cythonize, cython_help, trace_cython
95

96
    try:
97
        sys.argv.remove(CYTHON_OPTION)
98
        cythonize = True
99
    except ValueError:
100
        cythonize = version_is_from_git
101
102
103
104

    try:
        sys.argv.remove(CYTHON_TRACE_OPTION)
        trace_cython = True
105
106
        if not cythonize:
            print('Error: --cython-trace provided, but Cython will not be run.',
107
108
109
110
111
                  file=sys.stderr)
            exit(1)
    except ValueError:
        trace_cython = False

112
    if cythonize:
113
114
115
116
        try:
            import Cython
            from Cython.Build import cythonize
        except ImportError:
117
            cythonize = None
118
        else:
119
            #### Get Cython version.
120
121
122
123
124
125
126
127
            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)
128

129
130
131
132
133
134
135
136
137
138
139
140
            if cython_version < REQUIRED_CYTHON_VERSION:
                cythonize = None

            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)
    else:
        msg = "Run setup.py with the {} option to enable Cython."
        cython_help = msg.format(CYTHON_OPTION)

141

142
143
144
def banner(title=''):
    starred = title.center(79, '*')
    return '\n' + starred if title else starred
145

Christoph Groth's avatar
Christoph Groth committed
146

Christoph Groth's avatar
Christoph Groth committed
147
148
class kwant_build_ext(build_ext):
    def run(self):
149
150
151
152
153
154
155
156
        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.
            with open(CONFIG_FILE, 'w') as f:
                f.write('# Created by setup.py - feel free to modify.\n')

Christoph Groth's avatar
Christoph Groth committed
157
158
159
        try:
            build_ext.run(self)
        except (DistutilsError, CCompilerError):
160
161
            error_msg = self.__error_msg.format(
                header=banner(' Error '), sep=banner())
162
163
            print(error_msg.format(file=CONFIG_FILE, summary=build_summary),
                  file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
164
            raise
165
        print(banner(' Build summary '))
166
        print(build_summary)
167
        print(banner())
Christoph Groth's avatar
Christoph Groth committed
168

169
170
171
172
173
174
175
176
177
178
179
    __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}
"""

180

181
class kwant_build_tut(Command):
182
183
184
185
186
187
188
189
190
191
192
193
194
195
    description = "build the tutorial scripts"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        if not os.path.exists(TUT_DIR):
            os.mkdir(TUT_DIR)
        for in_fname in glob.glob(TUT_GLOB):
            out_fname = os.path.join(TUT_DIR, os.path.basename(in_fname))
196
197
198
199
200
            with open(in_fname) as in_file:
                with open(out_fname, 'w') as out_file:
                    for line in in_file:
                        if not line.startswith(TUT_HIDDEN_PREFIX):
                            out_file.write(line)
201
202
203
204
205
206


# 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.
207
208
class kwant_build(build):
    sub_commands = [('build_tut', None)] + build.sub_commands
209

210
    def run(self):
211
        build.run(self)
212
213
        write_version(os.path.join(self.build_lib, *STATIC_VERSION_PATH))

214
215

def git_lsfiles():
216
    if not version_is_from_git:
217
218
        return

219
    try:
220
        p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
221
222
223
224
225
226
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return

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


230
231
232
233
# 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.
234
235
class kwant_sdist(sdist):
    sub_commands = [('build', None)] + sdist.sub_commands
236
237

    def run(self):
Christoph Groth's avatar
Christoph Groth committed
238
239
240
241
242
243
244
245
246
247
        """
        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.
        """
        manifest = os.path.join(distr_root, MANIFEST_IN_FILE)
248
249
        names = git_lsfiles()
        if names is None:
Christoph Groth's avatar
Christoph Groth committed
250
251
252
253
            if not (os.path.isfile(manifest) and os.access(manifest, os.R_OK)):
                print("Error:", MANIFEST_IN_FILE,
                      "file is missing and Git is not available"
                      " to regenerate it.", file=sys.stderr)
254
255
                exit(1)
        else:
Christoph Groth's avatar
Christoph Groth committed
256
            with open(manifest, 'w') as f:
257
258
259
260
261
                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
262
                    f.write('include {}'.format(name))
263
                    if extension == 'pyx':
Christoph Groth's avatar
Christoph Groth committed
264
265
                        f.write(''.join([' ', a, sep, stem, dot, 'c']))
                    f.write('\n')
266

267
        sdist.run(self)
268
269

        if names is None:
270
            print(banner(' Caution '),
Christoph Groth's avatar
Christoph Groth committed
271
        """Git was not available to generate the list of files to be included in the
272
273
source distribution.  The old {} was used.""".format(MANIFEST_IN_FILE),
                  banner(), sep='\n', file=sys.stderr)
274

275
    def make_release_tree(self, base_dir, files):
276
        sdist.make_release_tree(self, base_dir, files)
277
        write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
278

Christoph Groth's avatar
Christoph Groth committed
279

280
281
282
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?
283
    try:
284
285
286
287
288
        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
289
        f.write("version = '{}'\n".format(version))
Christoph Groth's avatar
Christoph Groth committed
290
291


292
293
294
295
296
def long_description():
    text = []
    try:
        with open(README_FILE) as f:
            for line in f:
297
                if line.startswith(README_END_BEFORE):
298
299
                    break
                text.append(line.rstrip())
300
301
            while text[-1] == "":
                text.pop()
302
303
304
305
306
    except:
        return ''
    return '\n'.join(text)


307
308
309
310
311
312
313
314
315
316
317
318
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
319
    try:
320
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
Christoph Groth's avatar
Christoph Groth committed
321
322
323
    except OSError:
        pass
    else:
324
        p.communicate(input=b'int main() {}\n')
325
326
        if p.wait() == 0:
            return {'libraries': libs}
Christoph Groth's avatar
Christoph Groth committed
327
328
329
330
    return {}


def extensions():
Christoph Groth's avatar
Christoph Groth committed
331
    """Return a list of tuples (args, kwrds) to be passed to Extension."""
Christoph Groth's avatar
Christoph Groth committed
332

333
    global build_summary, config_file_present
Christoph Groth's avatar
Christoph Groth committed
334
335
    build_summary = []

336
    #### Add components of Kwant without external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
337
338
339
    result = [
        (['kwant._system', ['kwant/_system.pyx']],
         {'include_dirs': ['kwant/graph']}),
Joseph Weston's avatar
Joseph Weston committed
340
341
        (['kwant.operator', ['kwant/operator.pyx']],
         {'include_dirs': ['kwant/graph']}),
Christoph Groth's avatar
Christoph Groth committed
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
        (['kwant.graph.core', ['kwant/graph/core.pyx']],
         {'depends': ['kwant/graph/core.pxd', 'kwant/graph/defs.h',
                      'kwant/graph/defs.pxd']}),
        (['kwant.graph.utils', ['kwant/graph/utils.pyx']],
         {'depends': ['kwant/graph/defs.h', 'kwant/graph/defs.pxd',
                      'kwant/graph/core.pxd']}),
        (['kwant.graph.slicer', ['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']})]

359
360
361
362
363
364
365
    #### Add cython tracing macro
    if trace_cython:
        for args, kwargs in result:
            macros = kwargs.get('define_macros', [])
            macros.append(('CYTHON_TRACE', '1'))
            kwargs['define_macros'] = macros

366
    #### Add components of Kwant with external compile-time dependencies.
367
    config = configparser.ConfigParser()
Christoph Groth's avatar
Christoph Groth committed
368
369
    try:
        with open(CONFIG_FILE) as f:
370
            config.read_file(f)
Christoph Groth's avatar
Christoph Groth committed
371
    except IOError:
372
373
374
        config_file_present = False
    else:
        config_file_present = True
Christoph Groth's avatar
Christoph Groth committed
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399

    kwrds_by_section = {}
    for section in config.sections():
        kwrds_by_section[section] = kwrds = {}
        for name, value in config.items(section):
            kwrds[name] = value.split()

    # Setup LAPACK.
    lapack = kwrds_by_section.get('lapack')
    if lapack:
        build_summary.append('User-configured LAPACK and BLAS')
    else:
        lapack = {'libraries': ['lapack', 'blas']}
        build_summary.append('Default LAPACK and BLAS')
    kwrds = lapack.copy()
    kwrds.setdefault('depends', []).extend(
        [CONFIG_FILE, 'kwant/linalg/f_lapack.pxd'])
    result.append((['kwant.linalg.lapack', ['kwant/linalg/lapack.pyx']],
                   kwrds))

    # Setup MUMPS.
    kwrds = kwrds_by_section.get('mumps')
    if kwrds:
        build_summary.append('User-configured MUMPS')
    else:
400
        kwrds = search_mumps()
Christoph Groth's avatar
Christoph Groth committed
401
        if kwrds:
402
            build_summary.append('Auto-configured MUMPS')
Christoph Groth's avatar
Christoph Groth committed
403
    if kwrds:
404
        for name, value in lapack.items():
Christoph Groth's avatar
Christoph Groth committed
405
406
407
408
409
410
411
412
413
414
415
416
            kwrds.setdefault(name, []).extend(value)
        kwrds.setdefault('depends', []).extend(
            [CONFIG_FILE, 'kwant/linalg/cmumps.pxd'])
        result.append((['kwant.linalg._mumps', ['kwant/linalg/_mumps.pyx']],
                       kwrds))
    else:
        build_summary.append('No MUMPS support')

    build_summary = '\n'.join(build_summary)
    return result


417
418
def maybe_cythonize(extensions):
    """Prepare a list of `Extension` instances, ready for `setup()`.
419

420
421
422
423
424
425
    The argument `extensions` must be a sequence of (args, kwrds) to be passed on
    to `Extension`.

    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.
426
    """
427
    if cythonize:
428
        return cythonize([Extension(*args, **kwrds)
429
                          for args, kwrds in extensions], language_level=3,
Christoph Groth's avatar
Christoph Groth committed
430
                         compiler_directives={'linetrace': trace_cython})
431
432
433
434

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

Christoph Groth's avatar
Christoph Groth committed
435
    result = []
436
    problematic_files = []
Christoph Groth's avatar
Christoph Groth committed
437
    for args, kwrds in extensions:
438
439
        name, sources = args

440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
        language = kwrds.get('language')
        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 = []
        new_sources = []
        for f in sources:
            if f.endswith('.pyx'):
                pyx_files.append(f)
                f = f.rstrip('.pyx') + ext
                cythonized_files.append(f)
            new_sources.append(f)
        sources = new_sources

        # 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:
467
468
469
            msg = "Cython-generated file {} is missing."
            print(banner(" Error "), msg.format(f), "",
                  cython_help, banner(), sep="\n", file=sys.stderr)
470
471
472
473
474
475
476
477
478
            exit(1)

        for f in pyx_files + kwrds.get('depends', []):
            if f == CONFIG_FILE:
                # 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
479

480
        result.append(Extension(name, sources, **kwrds))
Christoph Groth's avatar
Christoph Groth committed
481

482
    if problematic_files:
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
        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:
499
500
            exit(1)

Christoph Groth's avatar
Christoph Groth committed
501
502
    return result

503

Christoph Groth's avatar
Christoph Groth committed
504
def main():
505
506
507
    get_version()
    init_cython()

Christoph Groth's avatar
Christoph Groth committed
508
    setup(name='kwant',
509
          version=version,
510
511
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
512
          author_email='authors@kwant-project.org',
513
          description=("Package for numerical quantum transport calculations "
514
                       "(Python 3 version)"),
515
516
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
517
518
          url="http://kwant-project.org/",
          license="BSD",
519
          packages=find_packages('.'),
520
          cmdclass={'build': kwant_build,
521
                    'sdist': kwant_sdist,
522
                    'build_ext': kwant_build_ext,
Christoph Groth's avatar
Christoph Groth committed
523
                    'build_tut': kwant_build_tut},
524
          ext_modules=maybe_cythonize(extensions()),
525
526
          setup_requires=['pytest-runner >= 2.7'],
          tests_require=['numpy > 1.6.1', 'pytest >= 2.6.3'],
Christoph Groth's avatar
Christoph Groth committed
527
          install_requires=['numpy > 1.6.1', 'scipy >= 0.9', 'tinyarray'],
Joseph Weston's avatar
Joseph Weston committed
528
529
          extras_require={'plotting': 'matplotlib >= 1.2'},
          classifiers=[c.strip() for c in CLASSIFIERS.split('\n')])
530

Christoph Groth's avatar
Christoph Groth committed
531
532
if __name__ == '__main__':
    main()