setup.py 16.5 KB
Newer Older
1
2
#!/usr/bin/env python

3
# Copyright 2011-2015 Kwant authors.
4
#
5
# This file is part of Kwant.  It is subject to the license terms in the
6
# LICENSE file 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
8
9
10
# the AUTHORS file at the top-level directory of this distribution and at
# http://kwant-project.org/authors.

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

import sys
14
import re
Christoph Groth's avatar
Christoph Groth committed
15
import os
16
import glob
17
import imp
Christoph Groth's avatar
Christoph Groth committed
18
19
import subprocess
import ConfigParser
Christoph Groth's avatar
Christoph Groth committed
20
21
22
23
from distutils.core import setup, Extension, Command
from distutils.util import get_platform
from distutils.errors import DistutilsError, DistutilsModuleError, \
    CCompilerError
24
25
from distutils.command.build import build
from distutils.command.sdist import sdist
26

Christoph Groth's avatar
Christoph Groth committed
27
28
import numpy

29
30
31
CONFIG_FILE = 'build.conf'
README_FILE = 'README.rst'
README_END_BEFORE = 'See also in this directory:'
32
STATIC_VERSION_PATH = ('kwant', '_kwant_version.py')
Christoph Groth's avatar
Christoph Groth committed
33
REQUIRED_CYTHON_VERSION = (0, 22)
34
35
36
37
38
NO_CYTHON_OPTION = '--no-cython'
TUT_DIR = 'tutorial'
TUT_GLOB = 'doc/source/tutorial/*.py'
TUT_HIDDEN_PREFIX = '#HIDDEN'

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 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
try:
    imp.load_source(STATIC_VERSION_PATH[-1].split('.')[0],
                    os.path.join(*STATIC_VERSION_PATH))
except IOError:
    pass
_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

Christoph Groth's avatar
Christoph Groth committed
54
55
56
57
58
59
try:
    sys.argv.remove(NO_CYTHON_OPTION)
    cythonize = False
except ValueError:
    cythonize = True

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
if cythonize:
    try:
        import Cython
    except:
        cython_version = ()
    else:
        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)

Christoph Groth's avatar
Christoph Groth committed
75
if cythonize and cython_version:
76
77
    from Cython.Distutils import build_ext
else:
Christoph Groth's avatar
Christoph Groth committed
78
79
    from distutils.command.build_ext import build_ext

80
distr_root = os.path.dirname(os.path.abspath(__file__))
81

82
83
84
def banner(title=''):
    starred = title.center(79, '*')
    return '\n' + starred if title else starred
85

86
error_msg = """{header}
87
88
89
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}}.
90

91
Build configuration was:
92

93
{{summary}}
94
95
96
{sep}
"""
error_msg = error_msg.format(header=banner(' Error '), sep=banner())
Christoph Groth's avatar
Christoph Groth committed
97
98
99

class kwant_build_ext(build_ext):
    def run(self):
100
101
102
103
104
105
106
107
        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
108
109
110
        try:
            build_ext.run(self)
        except (DistutilsError, CCompilerError):
111
112
            print(error_msg.format(file=CONFIG_FILE, summary=build_summary),
                  file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
113
            raise
114
        print(banner(' Build summary '))
115
        print(build_summary)
116
        print(banner())
Christoph Groth's avatar
Christoph Groth committed
117

118

119
class kwant_build_tut(Command):
120
121
122
123
124
125
126
127
128
129
130
131
132
133
    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))
134
135
136
137
138
            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)
139
140
141
142
143
144


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

148
    def run(self):
149
        build.run(self)
150
151
        write_version(os.path.join(self.build_lib, *STATIC_VERSION_PATH))

152

153
class kwant_test(Command):
Christoph Groth's avatar
Christoph Groth committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    description = "build, then run the unit tests"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        try:
            from nose.core import run
        except ImportError:
            raise DistutilsModuleError('nose <http://nose.readthedocs.org/> '
                                       'is needed to run the tests')
        self.run_command('build')
        major, minor = sys.version_info[:2]
        lib_dir = "build/lib.{0}-{1}.{2}".format(get_platform(), major, minor)
172
        print()
Christoph Groth's avatar
Christoph Groth committed
173
174
175
176
        if not run(argv=[__file__, '-v', lib_dir]):
            raise DistutilsError('at least one of the tests failed')


177
def git_lsfiles():
178
    if not version_is_from_git:
179
180
        return

181
    try:
182
        p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
183
184
185
186
187
188
189
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return

    if p.wait() != 0:
        return
    return p.communicate()[0].split('\n')[:-1]
190
191


192
193
194
195
# 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.
196
197
class kwant_sdist(sdist):
    sub_commands = [('build', None)] + sdist.sub_commands
198
199
200
201
202
203
204
205

    def run(self):
        names = git_lsfiles()
        trustworthy = True
        if names is None:
            # Check that MANIFEST exists and has not been generated by
            # distutils.
            try:
206
                with open(distr_root + '/MANIFEST', 'r') as f:
207
208
                    line = f.read()
            except IOError:
209
                print("Error: MANIFEST file is missing and Git is not"
210
                      " available to regenerate it.", file=sys.stderr)
211
212
213
214
                exit(1)
            trustworthy = not line.strip().startswith('#')
        else:
            # Generate MANIFEST file.
215
            with open(distr_root + '/MANIFEST', 'w') as f:
216
217
218
219
220
221
222
223
                for name in names:
                    a, sep, b = name.rpartition('/')
                    if b == '.gitignore':
                        continue
                    stem, dot, extension = b.rpartition('.')
                    if extension == 'pyx':
                        f.write(''.join([a, sep, stem, dot, 'c', '\n']))
                    f.write(name + '\n')
224
                f.write('MANIFEST\n')
225

226
        sdist.run(self)
227
228

        if names is None:
229
            print(banner(' Warning '),
230
231
"""Git was not available for re-generating the MANIFEST file (the list of file
names to be included in the source distribution).  The old MANIFEST was used.""",
232
                  banner(),
233
                  sep='\n', file=sys.stderr)
234

235
        if not trustworthy:
236
            print(banner(' Warning '),
237
238
"""The existing MANIFEST file seems to have been generated by distutils (it begins
with a comment).  It may well be incomplete.""",
239
                  banner(),
240
                  sep='\n', file=sys.stderr)
241

242
    def make_release_tree(self, base_dir, files):
243
        sdist.make_release_tree(self, base_dir, files)
244
        write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
245

Christoph Groth's avatar
Christoph Groth committed
246

247
248
249
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?
250
    try:
251
252
253
254
255
256
        os.remove(fname)
    except OSError:
        pass
    with open(fname, 'w') as f:
        f.write("# This file has been created by setup.py.\n")
        f.write("version = '{}'\n".format(version))
Christoph Groth's avatar
Christoph Groth committed
257
258


259
260
261
262
263
def long_description():
    text = []
    try:
        with open(README_FILE) as f:
            for line in f:
264
                if line.startswith(README_END_BEFORE):
265
266
                    break
                text.append(line.rstrip())
267
268
            while text[-1] == "":
                text.pop()
269
270
271
272
273
    except:
        return ''
    return '\n'.join(text)


274
275
276
277
278
279
def packages():
    return [root.replace('/', '.')
            for root, dnames, fnames in os.walk('kwant')
            if '__init__.py' in fnames or root.endswith('/tests')]


280
281
282
283
284
285
286
287
288
289
290
291
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
292
    try:
293
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
Christoph Groth's avatar
Christoph Groth committed
294
295
296
    except OSError:
        pass
    else:
297
298
299
        p.communicate(input='int main() {}\n')
        if p.wait() == 0:
            return {'libraries': libs}
Christoph Groth's avatar
Christoph Groth committed
300
301
302
303
304
305
306
307
    return {}


def extensions():
    """Return a list of tuples (args, kwrds) to be passed to
    Extension. possibly after replacing ".pyx" with ".c" if Cython is not to be
    used."""

308
    global build_summary, config_file_present
Christoph Groth's avatar
Christoph Groth committed
309
310
    build_summary = []

311
    #### Add components of Kwant without external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
    result = [
        (['kwant._system', ['kwant/_system.pyx']],
         {'include_dirs': ['kwant/graph']}),
        (['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']})]

332
    #### Add components of Kwant with external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
333
334
335
336
337
    config = ConfigParser.ConfigParser()
    try:
        with open(CONFIG_FILE) as f:
            config.readfp(f)
    except IOError:
338
339
340
        config_file_present = False
    else:
        config_file_present = True
Christoph Groth's avatar
Christoph Groth committed
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365

    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:
366
        kwrds = search_mumps()
Christoph Groth's avatar
Christoph Groth committed
367
        if kwrds:
368
            build_summary.append('Auto-configured MUMPS')
Christoph Groth's avatar
Christoph Groth committed
369
370
371
372
373
374
375
376
377
378
379
380
381
382
    if kwrds:
        for name, value in lapack.iteritems():
            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


383
384
385
def complain_cython_unavailable():
    assert not cythonize or cython_version < REQUIRED_CYTHON_VERSION
    if cythonize:
386
387
        msg = ("Install Cython {0} or newer so it can be made\n"
               "or use a source distribution of Kwant.")
388
        ver = '.'.join(str(e) for e in REQUIRED_CYTHON_VERSION)
389
        print(msg.format(ver), file=sys.stderr)
390
    else:
391
392
        print("Run setup.py without {}.".format(NO_CYTHON_OPTION),
              file=sys.stderr)
393
394


Christoph Groth's avatar
Christoph Groth committed
395
396
397
def ext_modules(extensions):
    """Prepare the ext_modules argument for distutils' setup."""
    result = []
398
    problematic_files = []
Christoph Groth's avatar
Christoph Groth committed
399
    for args, kwrds in extensions:
400
        if not cythonize or cython_version < REQUIRED_CYTHON_VERSION:
401
402
            # Cython is not going to be run: replace pyx extension by that of
            # the shipped translated file.
Christoph Groth's avatar
Christoph Groth committed
403
404
405
406
407
408
            if 'language' in kwrds:
                if kwrds['language'] == 'c':
                    ext = '.c'
                elif kwrds['language'] == 'c++':
                    ext = '.cpp'
                else:
409
                    print('Unknown language', file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
410
                    exit(1)
411
            else:
Christoph Groth's avatar
Christoph Groth committed
412
413
414
415
416
417
418
419
420
421
422
423
                ext = '.c'
            pyx_files = []
            cythonized_files = []
            sources = []
            for f in args[1]:
                if f[-4:] == '.pyx':
                    pyx_files.append(f)
                    f = f[:-4] + ext
                    cythonized_files.append(f)
                sources.append(f)
            args[1] = sources

424
            # Complain if cythonized files are older than Cython source files.
Christoph Groth's avatar
Christoph Groth committed
425
426
427
428
            try:
                cythonized_oldest = min(os.stat(f).st_mtime
                                        for f in cythonized_files)
            except OSError:
429
430
                print("error: Cython-generated file {} is missing.".format(f),
                      file=sys.stderr)
431
                complain_cython_unavailable()
432
                exit(1)
Christoph Groth's avatar
Christoph Groth committed
433
            for f in pyx_files + kwrds.get('depends', []):
434
435
436
437
                if f == CONFIG_FILE:
                    # The config file is only a dependency for the compilation
                    # of the cythonized file, not for the cythonization.
                    continue
Christoph Groth's avatar
Christoph Groth committed
438
                if os.stat(f).st_mtime > cythonized_oldest:
439
                    problematic_files.append(f)
Christoph Groth's avatar
Christoph Groth committed
440
441
442

        result.append(Extension(*args, **kwrds))

443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    if problematic_files:
        problematic_files = ", ".join(problematic_files)
        msg = ("Some Cython source files are newer than files that should have\n"
               " been derived from them, but {}.\n"
               "\n"
               "Affected files: {}")
        if cythonize:
            if not cython_version:
                reason = "Cython is not installed"
            else:
                reason = "the installed Cython is too old"
            print(banner(" Error "), msg.format(reason, problematic_files),
                  banner(), sep="\n", file=sys.stderr)
            print()
            complain_cython_unavailable()
            exit(1)
        else:
            reason = "the option --no-cython has been given"
            print(banner(" Warning "), msg.format(reason, problematic_files),
                  banner(), sep='\n', file=sys.stderr)

Christoph Groth's avatar
Christoph Groth committed
464
465
466
467
468
    return result


def main():
    setup(name='kwant',
469
          version=version,
470
471
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
472
          author_email='authors@kwant-project.org',
Christoph Groth's avatar
Christoph Groth committed
473
          description="Package for numerical quantum transport calculations.",
474
475
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
476
477
          url="http://kwant-project.org/",
          license="BSD",
478
          packages=packages(),
479
          cmdclass={'build': kwant_build,
480
                    'sdist': kwant_sdist,
481
                    'build_ext': kwant_build_ext,
482
483
                    'build_tut': kwant_build_tut,
                    'test': kwant_test},
Christoph Groth's avatar
Christoph Groth committed
484
485
          ext_modules=ext_modules(extensions()),
          include_dirs=[numpy.get_include()])
486

Christoph Groth's avatar
Christoph Groth committed
487
488
if __name__ == '__main__':
    main()