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

3
# Copyright 2011-2015 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
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
import subprocess
19
import configparser
20
from setuptools import setup, find_packages, Extension, Command
Christoph Groth's avatar
Christoph Groth committed
21
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

28
29
30
31
32
33
try:
    import numpy
except ImportError:
    include_dirs = []
else:
    include_dirs = [numpy.get_include()]
Christoph Groth's avatar
Christoph Groth committed
34

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

57

58
59
60
61
62
63
64
65
66
67
# 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

Christoph Groth's avatar
Christoph Groth committed
68
try:
69
    sys.argv.remove(CYTHON_OPTION)
70
    use_cython = True
71
except ValueError:
72
    use_cython = version_is_from_git
Christoph Groth's avatar
Christoph Groth committed
73

74
if use_cython:
75
76
    try:
        import Cython
77
78
        from Cython.Build import cythonize
    except ImportError:
79
80
81
82
83
84
85
86
87
88
89
        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)

90
distr_root = os.path.dirname(os.path.abspath(__file__))
91

92
93
94
def banner(title=''):
    starred = title.center(79, '*')
    return '\n' + starred if title else starred
95

96
error_msg = """{header}
97
98
99
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}}.
100

101
Build configuration was:
102

103
{{summary}}
104
105
106
{sep}
"""
error_msg = error_msg.format(header=banner(' Error '), sep=banner())
Christoph Groth's avatar
Christoph Groth committed
107

Christoph Groth's avatar
Christoph Groth committed
108

Christoph Groth's avatar
Christoph Groth committed
109
110
class kwant_build_ext(build_ext):
    def run(self):
111
112
113
114
115
116
117
118
        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
119
120
121
        try:
            build_ext.run(self)
        except (DistutilsError, CCompilerError):
122
123
            print(error_msg.format(file=CONFIG_FILE, summary=build_summary),
                  file=sys.stderr)
Christoph Groth's avatar
Christoph Groth committed
124
            raise
125
        print(banner(' Build summary '))
126
        print(build_summary)
127
        print(banner())
Christoph Groth's avatar
Christoph Groth committed
128

129

130
class kwant_build_tut(Command):
131
132
133
134
135
136
137
138
139
140
141
142
143
144
    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))
145
146
147
148
149
            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)
150
151
152
153
154
155


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

159
    def run(self):
160
        build.run(self)
161
162
        write_version(os.path.join(self.build_lib, *STATIC_VERSION_PATH))

163
164

def git_lsfiles():
165
    if not version_is_from_git:
166
167
        return

168
    try:
169
        p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
170
171
172
173
174
175
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return

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


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

    def run(self):
Christoph Groth's avatar
Christoph Groth committed
187
188
189
190
191
192
193
194
195
196
        """
        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)
197
198
        names = git_lsfiles()
        if names is None:
Christoph Groth's avatar
Christoph Groth committed
199
200
201
202
            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)
203
204
                exit(1)
        else:
Christoph Groth's avatar
Christoph Groth committed
205
            with open(manifest, 'w') as f:
206
207
208
209
210
                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
211
                    f.write('include {}'.format(name))
212
                    if extension == 'pyx':
Christoph Groth's avatar
Christoph Groth committed
213
214
                        f.write(''.join([' ', a, sep, stem, dot, 'c']))
                    f.write('\n')
215

216
        sdist.run(self)
217
218

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

224
    def make_release_tree(self, base_dir, files):
225
        sdist.make_release_tree(self, base_dir, files)
226
        write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
227

Christoph Groth's avatar
Christoph Groth committed
228

229
230
231
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?
232
    try:
233
234
235
236
237
        os.remove(fname)
    except OSError:
        pass
    with open(fname, 'w') as f:
        f.write("# This file has been created by setup.py.\n")
Joseph Weston's avatar
Joseph Weston committed
238
        f.write("version = '{}'".format(version))
Christoph Groth's avatar
Christoph Groth committed
239
240


241
242
243
244
245
def long_description():
    text = []
    try:
        with open(README_FILE) as f:
            for line in f:
246
                if line.startswith(README_END_BEFORE):
247
248
                    break
                text.append(line.rstrip())
249
250
            while text[-1] == "":
                text.pop()
251
252
253
254
255
    except:
        return ''
    return '\n'.join(text)


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


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

282
    global build_summary, config_file_present
Christoph Groth's avatar
Christoph Groth committed
283
284
    build_summary = []

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

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

    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:
340
        kwrds = search_mumps()
Christoph Groth's avatar
Christoph Groth committed
341
        if kwrds:
342
            build_summary.append('Auto-configured MUMPS')
Christoph Groth's avatar
Christoph Groth committed
343
    if kwrds:
344
        for name, value in lapack.items():
Christoph Groth's avatar
Christoph Groth committed
345
346
347
348
349
350
351
352
353
354
355
356
            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


357
def complain_cython_unavailable():
358
359
    assert not use_cython or cython_version < REQUIRED_CYTHON_VERSION
    if use_cython:
360
361
        msg = ("Install Cython {0} or newer so it can be made\n"
               "or use a source distribution of Kwant.")
362
        ver = '.'.join(str(e) for e in REQUIRED_CYTHON_VERSION)
363
        print(msg.format(ver), file=sys.stderr)
364
    else:
365
        print("Run setup.py with the {} option.".format(CYTHON_OPTION),
366
              file=sys.stderr)
367
368


Christoph Groth's avatar
Christoph Groth committed
369
def ext_modules(extensions):
370
371
372
373
374
    """Prepare the ext_modules argument for setuptools.

    If Cython is not to be run, replace .pyx extensions with .c or .cpp, and
    check timestamps.
    """
375
376
377
378
379
380
381
    if use_cython and cython_version >= REQUIRED_CYTHON_VERSION:
        return cythonize([Extension(*args, **kwrds)
                          for args, kwrds in extensions])

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

Christoph Groth's avatar
Christoph Groth committed
382
    result = []
383
    problematic_files = []
Christoph Groth's avatar
Christoph Groth committed
384
    for args, kwrds in extensions:
385
386
        name, sources = args

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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
        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:
            print("error: Cython-generated file {} is missing.".format(f),
                  file=sys.stderr)
            complain_cython_unavailable()
            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
426

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

429
430
431
    if problematic_files:
        problematic_files = ", ".join(problematic_files)
        msg = ("Some Cython source files are newer than files that should have\n"
432
               "been derived from them, but {}.\n"
433
434
               "\n"
               "Affected files: {}")
435
        if use_cython:
436
437
438
439
440
441
442
443
444
445
            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:
446
447
448
449
450
            reason = "the option {} has not been given".format(CYTHON_OPTION)
            dontworry = ('(Do not worry about this if you are building Kwant\n'
                         'from unmodified sources, e.g. with "pip install".)\n')
            print(banner(" Caution "), dontworry,
                  msg.format(reason, problematic_files),
451
452
                  banner(), sep='\n', file=sys.stderr)

Christoph Groth's avatar
Christoph Groth committed
453
454
    return result

455

Christoph Groth's avatar
Christoph Groth committed
456
457
def main():
    setup(name='kwant',
458
          version=version,
459
460
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
461
          author_email='authors@kwant-project.org',
Christoph Groth's avatar
Christoph Groth committed
462
          description="Package for numerical quantum transport calculations.",
463
464
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
465
466
          url="http://kwant-project.org/",
          license="BSD",
467
          packages=find_packages('.'),
Christoph Groth's avatar
Christoph Groth committed
468
          test_suite = 'nose.collector',
469
          cmdclass={'build': kwant_build,
470
                    'sdist': kwant_sdist,
471
                    'build_ext': kwant_build_ext,
Christoph Groth's avatar
Christoph Groth committed
472
                    'build_tut': kwant_build_tut},
Christoph Groth's avatar
Christoph Groth committed
473
          ext_modules=ext_modules(extensions()),
474
          include_dirs=include_dirs,
Christoph Groth's avatar
Christoph Groth committed
475
476
          setup_requires=['numpy > 1.6.1', 'nose >= 1.0'],
          install_requires=['numpy > 1.6.1', 'scipy >= 0.9', 'tinyarray'],
Joseph Weston's avatar
Joseph Weston committed
477
478
          extras_require={'plotting': 'matplotlib >= 1.2'},
          classifiers=[c.strip() for c in CLASSIFIERS.split('\n')])
479

Christoph Groth's avatar
Christoph Groth committed
480
481
if __name__ == '__main__':
    main()