setup.py 15.8 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
from setuptools import setup, Extension, Command
from sysconfig import get_platform
Christoph Groth's avatar
Christoph Groth committed
22
23
from distutils.errors import DistutilsError, DistutilsModuleError, \
    CCompilerError
24
from distutils.command.build import build
Christoph Groth's avatar
Christoph Groth committed
25
26
from setuptools.command.sdist import sdist
from setuptools.command.build_ext import build_ext
27

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

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

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 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
56
57
58
59
60
61
try:
    sys.argv.remove(NO_CYTHON_OPTION)
    cythonize = False
except ValueError:
    cythonize = True

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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)

77
distr_root = os.path.dirname(os.path.abspath(__file__))
78

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

83
error_msg = """{header}
84
85
86
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}}.
87

88
Build configuration was:
89

90
{{summary}}
91
92
93
{sep}
"""
error_msg = error_msg.format(header=banner(' Error '), sep=banner())
Christoph Groth's avatar
Christoph Groth committed
94

Christoph Groth's avatar
Christoph Groth committed
95

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

116

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


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

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

150
151

def git_lsfiles():
152
    if not version_is_from_git:
153
154
        return

155
    try:
156
        p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
157
158
159
160
161
162
163
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return

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


166
167
168
169
# 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.
170
171
class kwant_sdist(sdist):
    sub_commands = [('build', None)] + sdist.sub_commands
172
173

    def run(self):
Christoph Groth's avatar
Christoph Groth committed
174
175
176
177
178
179
180
181
182
183
        """
        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)
184
185
        names = git_lsfiles()
        if names is None:
Christoph Groth's avatar
Christoph Groth committed
186
187
188
189
            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)
190
191
                exit(1)
        else:
Christoph Groth's avatar
Christoph Groth committed
192
            with open(manifest, 'w') as f:
193
194
195
196
197
                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
198
                    f.write('include {}'.format(name))
199
                    if extension == 'pyx':
Christoph Groth's avatar
Christoph Groth committed
200
201
                        f.write(''.join([' ', a, sep, stem, dot, 'c']))
                    f.write('\n')
202

203
        sdist.run(self)
204
205

        if names is None:
206
            print(banner(' Warning '),
207

Christoph Groth's avatar
Christoph Groth committed
208
209
        """Git was not available to generate the list of files to be included in the
source distribution.  The old MANIFEST.in was used.""",
210
                  banner(),
211
                  sep='\n', file=sys.stderr)
212

213
    def make_release_tree(self, base_dir, files):
214
        sdist.make_release_tree(self, base_dir, files)
215
        write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
216

Christoph Groth's avatar
Christoph Groth committed
217

218
219
220
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?
221
    try:
222
223
224
225
226
227
        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
228
229


230
231
232
233
234
def long_description():
    text = []
    try:
        with open(README_FILE) as f:
            for line in f:
235
                if line.startswith(README_END_BEFORE):
236
237
                    break
                text.append(line.rstrip())
238
239
            while text[-1] == "":
                text.pop()
240
241
242
243
244
    except:
        return ''
    return '\n'.join(text)


245
246
247
248
249
250
def packages():
    return [root.replace('/', '.')
            for root, dnames, fnames in os.walk('kwant')
            if '__init__.py' in fnames or root.endswith('/tests')]


251
252
253
254
255
256
257
258
259
260
261
262
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
263
    try:
264
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
Christoph Groth's avatar
Christoph Groth committed
265
266
267
    except OSError:
        pass
    else:
268
269
270
        p.communicate(input='int main() {}\n')
        if p.wait() == 0:
            return {'libraries': libs}
Christoph Groth's avatar
Christoph Groth committed
271
272
273
274
    return {}


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

277
    global build_summary, config_file_present
Christoph Groth's avatar
Christoph Groth committed
278
279
    build_summary = []

280
    #### Add components of Kwant without external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
    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']})]

301
    #### Add components of Kwant with external compile-time dependencies.
Christoph Groth's avatar
Christoph Groth committed
302
303
304
305
306
    config = ConfigParser.ConfigParser()
    try:
        with open(CONFIG_FILE) as f:
            config.readfp(f)
    except IOError:
307
308
309
        config_file_present = False
    else:
        config_file_present = True
Christoph Groth's avatar
Christoph Groth committed
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334

    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:
335
        kwrds = search_mumps()
Christoph Groth's avatar
Christoph Groth committed
336
        if kwrds:
337
            build_summary.append('Auto-configured MUMPS')
Christoph Groth's avatar
Christoph Groth committed
338
339
340
341
342
343
344
345
346
347
348
349
350
351
    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


352
353
354
def complain_cython_unavailable():
    assert not cythonize or cython_version < REQUIRED_CYTHON_VERSION
    if cythonize:
355
356
        msg = ("Install Cython {0} or newer so it can be made\n"
               "or use a source distribution of Kwant.")
357
        ver = '.'.join(str(e) for e in REQUIRED_CYTHON_VERSION)
358
        print(msg.format(ver), file=sys.stderr)
359
    else:
360
361
        print("Run setup.py without {}.".format(NO_CYTHON_OPTION),
              file=sys.stderr)
362
363


Christoph Groth's avatar
Christoph Groth committed
364
365
366
def ext_modules(extensions):
    """Prepare the ext_modules argument for distutils' setup."""
    result = []
367
    problematic_files = []
Christoph Groth's avatar
Christoph Groth committed
368
    for args, kwrds in extensions:
369
        if not cythonize or cython_version < REQUIRED_CYTHON_VERSION:
370
371
            # Cython is not going to be run: replace pyx extension by that of
            # the shipped translated file.
Christoph Groth's avatar
Christoph Groth committed
372
373
374
375
376
377
            if 'language' in kwrds:
                if kwrds['language'] == 'c':
                    ext = '.c'
                elif kwrds['language'] == 'c++':
                    ext = '.cpp'
                else:
378
                    print('Unknown language', file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
379
                    exit(1)
380
            else:
Christoph Groth's avatar
Christoph Groth committed
381
382
383
384
385
386
387
388
389
390
391
392
                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

393
            # Complain if cythonized files are older than Cython source files.
Christoph Groth's avatar
Christoph Groth committed
394
395
396
397
            try:
                cythonized_oldest = min(os.stat(f).st_mtime
                                        for f in cythonized_files)
            except OSError:
398
399
                print("error: Cython-generated file {} is missing.".format(f),
                      file=sys.stderr)
400
                complain_cython_unavailable()
401
                exit(1)
Christoph Groth's avatar
Christoph Groth committed
402
            for f in pyx_files + kwrds.get('depends', []):
403
404
405
406
                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
407
                if os.stat(f).st_mtime > cythonized_oldest:
408
                    problematic_files.append(f)
Christoph Groth's avatar
Christoph Groth committed
409
410
411

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

412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
    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
433
434
435
436
437
    return result


def main():
    setup(name='kwant',
438
          version=version,
439
440
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
441
          author_email='authors@kwant-project.org',
Christoph Groth's avatar
Christoph Groth committed
442
          description="Package for numerical quantum transport calculations.",
443
444
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
445
446
          url="http://kwant-project.org/",
          license="BSD",
447
          packages=packages(),
Christoph Groth's avatar
Christoph Groth committed
448
          test_suite = 'nose.collector',
449
          cmdclass={'build': kwant_build,
450
                    'sdist': kwant_sdist,
451
                    'build_ext': kwant_build_ext,
Christoph Groth's avatar
Christoph Groth committed
452
                    'build_tut': kwant_build_tut},
Christoph Groth's avatar
Christoph Groth committed
453
          ext_modules=ext_modules(extensions()),
Christoph Groth's avatar
Christoph Groth committed
454
455
456
457
          include_dirs=[numpy.get_include()],
          setup_requires=['numpy > 1.6.1', 'nose >= 1.0'],
          install_requires=['numpy > 1.6.1', 'scipy >= 0.9', 'tinyarray'],
          extras_require={'plotting': 'matplotlib >= 1.2'})
458

Christoph Groth's avatar
Christoph Groth committed
459
460
if __name__ == '__main__':
    main()