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

3
# Copyright 2011-2013 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
Christoph Groth's avatar
Christoph Groth committed
17
18
import subprocess
import ConfigParser
Christoph Groth's avatar
Christoph Groth committed
19
20
21
22
from distutils.core import setup, Extension, Command
from distutils.util import get_platform
from distutils.errors import DistutilsError, DistutilsModuleError, \
    CCompilerError
23
from distutils.command.build import build as distutils_build
24
from distutils.command.sdist import sdist as distutils_sdist
25

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

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

Christoph Groth's avatar
Christoph Groth committed
39
40
41
42
43
try:
    import Cython
except:
    cython_version = ()
else:
44
45
46
47
48
49
50
51
    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)
52

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

59
60
61
62
63
64
try:
    sys.argv.remove(NO_GIT_OPTION)
    use_git = False
except ValueError:
    use_git = True

Christoph Groth's avatar
Christoph Groth committed
65
if cythonize and cython_version:
66
67
    from Cython.Distutils import build_ext
else:
Christoph Groth's avatar
Christoph Groth committed
68
69
    from distutils.command.build_ext import build_ext

70
distr_root = os.path.dirname(os.path.abspath(__file__))
71

72
73
74
75
76
77
78
79
80
81
82
83
def header(title=''):
    return title.center(79, '*')

error_msg = """{sep}
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}}.
{sep}
Build configuration was:
{{summary}}
{sep}"""
error_msg = error_msg.format(sep=header())
Christoph Groth's avatar
Christoph Groth committed
84
85
86

class kwant_build_ext(build_ext):
    def run(self):
87
88
89
90
91
92
93
94
        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
95
96
97
        try:
            build_ext.run(self)
        except (DistutilsError, CCompilerError):
98
99
            print(error_msg.format(file=CONFIG_FILE, summary=build_summary),
                  file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
100
            raise
101
102
        print(header(' Build summary '))
        print(build_summary)
Christoph Groth's avatar
Christoph Groth committed
103

104

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class build_tut(Command):
    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))
120
121
122
123
124
            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)
125
126
127
128
129
130
131
132


# 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.
class kwant_build(distutils_build):
    sub_commands = [('build_tut', None)] + distutils_build.sub_commands
133
134


Christoph Groth's avatar
Christoph Groth committed
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
class test(Command):
    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)
154
        print(header(' Tests '))
Christoph Groth's avatar
Christoph Groth committed
155
156
157
158
        if not run(argv=[__file__, '-v', lib_dir]):
            raise DistutilsError('at least one of the tests failed')


159
def git_lsfiles():
160
161
162
    if not use_git:
        return

163
    try:
164
        p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
165
166
167
168
169
170
171
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return

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


174
175
176
177
178
179
# 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.
class kwant_sdist(distutils_sdist):
    sub_commands = [('build', None)] + distutils_sdist.sub_commands
180
181
182
183
184
185
186
187

    def run(self):
        names = git_lsfiles()
        trustworthy = True
        if names is None:
            # Check that MANIFEST exists and has not been generated by
            # distutils.
            try:
188
                with open(distr_root + '/MANIFEST', 'r') as f:
189
190
                    line = f.read()
            except IOError:
191
192
                print("error: MANIFEST file is missing and Git is not"
                      " available to regenerate it.", file=sys.stderr)
193
194
195
196
                exit(1)
            trustworthy = not line.strip().startswith('#')
        else:
            # Generate MANIFEST file.
197
            with open(distr_root + '/MANIFEST', 'w') as f:
198
199
200
201
202
203
204
205
206
                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')
                f.write(STATIC_VERSION_FILE + '\n')
207
                f.write('MANIFEST\n')
208
209
210
211

        distutils_sdist.run(self)

        if names is None:
212
213
214
215
            print(header(' Warning '),
"""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.""",
                  sep='\n', file=sys.stderr)
216

217
218
219
220
221
        if not trustworthy:
            print(header(' Warning '),
"""The existing MANIFEST file seems to have been generated by distutils (it begins
with a comment).  It may well be incomplete.""",
                  sep='\n', file=sys.stderr)
222

Christoph Groth's avatar
Christoph Groth committed
223
224
225
# Other than the "if not use_git" clause in the beginning, this is an exact copy
# of the function from kwant/_common.py.  We can't import it here (because Kwant
# is not yet built when this scipt is run), so we just include a copy.
226
def get_version_from_git():
227
228
229
    if not use_git:
        return

230
    try:
231
232
        p = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'],
                             cwd=distr_root,
233
234
235
236
237
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return
    if p.wait() != 0:
        return
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    # TODO: use os.path.samefile once we depend on Python >= 3.3.
    if os.path.normpath(p.communicate()[0].rstrip('\n')) != distr_root:
        # The top-level directory of the current Git repository is not the same
        # as the root directory of the Kwant distribution: do not extract the
        # version from Git.
        return

    # git describe --first-parent does not take into account tags from branches
    # that were merged-in.
    for opts in [['--first-parent'], []]:
        try:
            p = subprocess.Popen(['git', 'describe'] + opts, cwd=distr_root,
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except OSError:
            return
        if p.wait() == 0:
            break
    else:
        return
    version = p.communicate()[0].rstrip('\n')
258
259
260
261
262

    if version[0] == 'v':
        version = version[1:]

    try:
263
        p = subprocess.Popen(['git', 'diff', '--quiet'], cwd=distr_root)
264
265
266
267
268
269
270
    except OSError:
        version += '-confused'  # This should never happen.
    else:
        if p.wait() == 1:
            version += '-dirty'
    return version

Christoph Groth's avatar
Christoph Groth committed
271

272
def read_static_version():
273
    """Return the version as recorded inside the source code."""
274
    try:
Christoph Groth's avatar
Christoph Groth committed
275
        with open(STATIC_VERSION_FILE) as f:
276
277
278
279
280
281
282
            contents = f.read()
            assert contents[:11] == "version = '"
            assert contents[-2:] == "'\n"
            return contents[11:-2]
    except:
        return None

Christoph Groth's avatar
Christoph Groth committed
283

284
285
286
287
288
289
def write_static_version(version):
    """Record the version so that it is available without version control."""
    with open(STATIC_VERSION_FILE, 'w') as f:
        f.write("version = '%s'\n" % version)


Christoph Groth's avatar
Christoph Groth committed
290
def version():
291
    """Determine the version of Kwant.  Return it and save it in a file."""
Christoph Groth's avatar
Christoph Groth committed
292
    git_version = get_version_from_git()
293
    static_version = read_static_version()
Christoph Groth's avatar
Christoph Groth committed
294
295
296
    if git_version is not None:
        version = git_version
        if static_version != git_version:
297
            write_static_version(version)
Christoph Groth's avatar
Christoph Groth committed
298
299
300
301
302
303
304
    elif static_version is not None:
        version = static_version
    else:
        version = 'unknown'
    return version


305
306
307
308
309
def long_description():
    text = []
    try:
        with open(README_FILE) as f:
            for line in f:
310
                if line.startswith(README_END_BEFORE):
311
312
                    break
                text.append(line.rstrip())
313
314
            while text[-1] == "":
                text.pop()
315
316
317
318
319
    except:
        return ''
    return '\n'.join(text)


320
321
322
323
324
325
def packages():
    return [root.replace('/', '.')
            for root, dnames, fnames in os.walk('kwant')
            if '__init__.py' in fnames or root.endswith('/tests')]


326
327
328
329
330
331
332
333
334
335
336
337
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
338
    try:
339
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
Christoph Groth's avatar
Christoph Groth committed
340
341
342
    except OSError:
        pass
    else:
343
344
345
        p.communicate(input='int main() {}\n')
        if p.wait() == 0:
            return {'libraries': libs}
Christoph Groth's avatar
Christoph Groth committed
346
347
348
349
350
351
352
353
    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."""

354
    global build_summary, config_file_present
Christoph Groth's avatar
Christoph Groth committed
355
356
    build_summary = []

357
    #### Add components of Kwant without external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
    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']})]

378
    #### Add components of Kwant with external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
379
380
381
382
383
    config = ConfigParser.ConfigParser()
    try:
        with open(CONFIG_FILE) as f:
            config.readfp(f)
    except IOError:
384
385
386
        config_file_present = False
    else:
        config_file_present = True
Christoph Groth's avatar
Christoph Groth committed
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411

    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:
412
        kwrds = search_mumps()
Christoph Groth's avatar
Christoph Groth committed
413
        if kwrds:
414
            build_summary.append('Auto-configured MUMPS')
Christoph Groth's avatar
Christoph Groth committed
415
416
417
418
419
420
421
422
423
424
425
426
427
428
    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


429
430
431
432
433
434
def complain_cython_unavailable():
    assert not cythonize or cython_version < REQUIRED_CYTHON_VERSION
    if cythonize:
        msg = "Install Cython {0} or newer so it can be made or use a source " \
            "distribution of Kwant."
        ver = '.'.join(str(e) for e in REQUIRED_CYTHON_VERSION)
435
        print(msg.format(ver), file=sys.stderr)
436
    else:
437
        print("Run setup.py without", NO_CYTHON_OPTION, file=sys.stderr)
438
439


Christoph Groth's avatar
Christoph Groth committed
440
441
442
443
def ext_modules(extensions):
    """Prepare the ext_modules argument for distutils' setup."""
    result = []
    for args, kwrds in extensions:
444
        if not cythonize or cython_version < REQUIRED_CYTHON_VERSION:
Christoph Groth's avatar
Christoph Groth committed
445
446
447
448
449
450
            if 'language' in kwrds:
                if kwrds['language'] == 'c':
                    ext = '.c'
                elif kwrds['language'] == 'c++':
                    ext = '.cpp'
                else:
451
                    print('Unknown language', file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
452
                    exit(1)
453
            else:
Christoph Groth's avatar
Christoph Groth committed
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
                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

            try:
                cythonized_oldest = min(os.stat(f).st_mtime
                                        for f in cythonized_files)
            except OSError:
470
471
                print("error: Cython-generated file {} is missing.".format(f),
                      file=sys.stderr)
472
                complain_cython_unavailable()
473
                exit(1)
Christoph Groth's avatar
Christoph Groth committed
474
            for f in pyx_files + kwrds.get('depends', []):
475
476
477
478
                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
479
                if os.stat(f).st_mtime > cythonized_oldest:
480
                    msg = "error: {} is newer than its source file, but "
481
482
483
484
                    if cythonize and not cython_version:
                        msg += "Cython is not installed."
                    elif cythonize:
                        msg += "the installed Cython is too old."
Christoph Groth's avatar
Christoph Groth committed
485
                    else:
486
                        msg += "Cython is not to be run."
487
                    print(msg.format(f), file=sys.stderr)
488
489
                    complain_cython_unavailable()
                    exit(1)
Christoph Groth's avatar
Christoph Groth committed
490
491
492
493
494
495
496
497
498

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

    return result


def main():
    setup(name='kwant',
          version=version(),
499
500
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
501
          author_email='authors@kwant-project.org',
Christoph Groth's avatar
Christoph Groth committed
502
          description="Package for numerical quantum transport calculations.",
503
504
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
505
506
          url="http://kwant-project.org/",
          license="BSD",
507
          packages=packages(),
508
          cmdclass={'build': kwant_build,
509
                    'sdist': kwant_sdist,
510
                    'build_ext': kwant_build_ext,
Christoph Groth's avatar
Christoph Groth committed
511
512
                    'build_tut': build_tut,
                    'test': test},
Christoph Groth's avatar
Christoph Groth committed
513
514
          ext_modules=ext_modules(extensions()),
          include_dirs=[numpy.get_include()])
515

Christoph Groth's avatar
Christoph Groth committed
516
517
if __name__ == '__main__':
    main()