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

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
45
TUT_DIR = 'tutorial'
TUT_GLOB = 'doc/source/tutorial/*.py'
TUT_HIDDEN_PREFIX = '#HIDDEN'

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 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
61
try:
62
    sys.argv.remove(CYTHON_OPTION)
63
    use_cython = True
64
except ValueError:
65
    use_cython = version_is_from_git
Christoph Groth's avatar
Christoph Groth committed
66

67
if use_cython:
68
69
    try:
        import Cython
70
71
        from Cython.Build import cythonize
    except ImportError:
72
73
74
75
76
77
78
79
80
81
82
        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)

83
distr_root = os.path.dirname(os.path.abspath(__file__))
84

85
86
87
def banner(title=''):
    starred = title.center(79, '*')
    return '\n' + starred if title else starred
88

89
error_msg = """{header}
90
91
92
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}}.
93

94
Build configuration was:
95

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

Christoph Groth's avatar
Christoph Groth committed
101

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

122

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


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

152
    def run(self):
153
        build.run(self)
154
155
        write_version(os.path.join(self.build_lib, *STATIC_VERSION_PATH))

156
157

def git_lsfiles():
158
    if not version_is_from_git:
159
160
        return

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

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


172
173
174
175
# 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.
176
177
class kwant_sdist(sdist):
    sub_commands = [('build', None)] + sdist.sub_commands
178
179

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

209
        sdist.run(self)
210
211

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

217
    def make_release_tree(self, base_dir, files):
218
        sdist.make_release_tree(self, base_dir, files)
219
        write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
220

Christoph Groth's avatar
Christoph Groth committed
221

222
223
224
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?
225
    try:
226
227
228
229
230
231
        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
232
233


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


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


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

275
    global build_summary, config_file_present
Christoph Groth's avatar
Christoph Groth committed
276
277
    build_summary = []

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

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

    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:
333
        kwrds = search_mumps()
Christoph Groth's avatar
Christoph Groth committed
334
        if kwrds:
335
            build_summary.append('Auto-configured MUMPS')
Christoph Groth's avatar
Christoph Groth committed
336
    if kwrds:
337
        for name, value in lapack.items():
Christoph Groth's avatar
Christoph Groth committed
338
339
340
341
342
343
344
345
346
347
348
349
            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


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


Christoph Groth's avatar
Christoph Groth committed
362
def ext_modules(extensions):
363
364
365
366
367
    """Prepare the ext_modules argument for setuptools.

    If Cython is not to be run, replace .pyx extensions with .c or .cpp, and
    check timestamps.
    """
368
369
370
371
372
373
374
    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
375
    result = []
376
    problematic_files = []
Christoph Groth's avatar
Christoph Groth committed
377
    for args, kwrds in extensions:
378
379
        name, sources = args

380
381
382
383
384
385
386
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
        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
419

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

422
423
424
    if problematic_files:
        problematic_files = ", ".join(problematic_files)
        msg = ("Some Cython source files are newer than files that should have\n"
425
               "been derived from them, but {}.\n"
426
427
               "\n"
               "Affected files: {}")
428
        if use_cython:
429
430
431
432
433
434
435
436
437
438
            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:
439
440
441
442
443
            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),
444
445
                  banner(), sep='\n', file=sys.stderr)

Christoph Groth's avatar
Christoph Groth committed
446
447
    return result

448

Christoph Groth's avatar
Christoph Groth committed
449
450
def main():
    setup(name='kwant',
451
          version=version,
452
453
          author='C. W. Groth (CEA), M. Wimmer, '
                 'A. R. Akhmerov, X. Waintal (CEA), and others',
454
          author_email='authors@kwant-project.org',
Christoph Groth's avatar
Christoph Groth committed
455
          description="Package for numerical quantum transport calculations.",
456
457
          long_description=long_description(),
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
458
459
          url="http://kwant-project.org/",
          license="BSD",
460
          packages=find_packages('.'),
Christoph Groth's avatar
Christoph Groth committed
461
          test_suite = 'nose.collector',
462
          cmdclass={'build': kwant_build,
463
                    'sdist': kwant_sdist,
464
                    'build_ext': kwant_build_ext,
Christoph Groth's avatar
Christoph Groth committed
465
                    'build_tut': kwant_build_tut},
Christoph Groth's avatar
Christoph Groth committed
466
          ext_modules=ext_modules(extensions()),
467
          include_dirs=include_dirs,
Christoph Groth's avatar
Christoph Groth committed
468
469
470
          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'})
471

Christoph Groth's avatar
Christoph Groth committed
472
473
if __name__ == '__main__':
    main()