7ed34389 |
#!/usr/bin/env python3
|
e6ae9762 |
|
cec7d4a2 |
# Copyright 2011-2018 Kwant authors.
|
e001ab42 |
#
|
c2b5f339 |
# 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
|
fa012f86 |
# http://kwant-project.org/license. A list of Kwant authors can be found in
|
c2b5f339 |
# the file AUTHORS.rst at the top-level directory of this distribution and at
|
e001ab42 |
# http://kwant-project.org/authors.
|
554d0fb1 |
from __future__ import print_function
|
43faf533 |
import sys
|
12fe5156 |
|
707bfa20 |
import re
|
43faf533 |
import os
|
1a077ad2 |
import glob
|
2fbec07b |
import importlib
|
43faf533 |
import subprocess
|
3d73b19c |
import configparser
|
16f4d000 |
import collections
|
1d45a300 |
from setuptools import setup, find_packages, Extension, Command
|
f837ca97 |
from distutils.errors import DistutilsError, CCompilerError
|
06484062 |
from distutils.command.build import build as build_orig
from setuptools.command.sdist import sdist as sdist_orig
from setuptools.command.build_ext import build_ext as build_ext_orig
|
04edb898 |
from setuptools.command.test import test as test_orig
|
c2ac7c90 |
|
43faf533 |
|
20171914 |
STATIC_VERSION_PATH = ('kwant', '_kwant_version.py')
|
c2ac7c90 |
|
cd3242d1 |
distr_root = os.path.dirname(os.path.abspath(__file__))
|
16f4d000 |
def configure_extensions(exts, aliases=(), build_summary=None):
"""Modify extension configuration according to the configuration file
`exts` must be a dict of (name, kwargs) tuples that can be used like this:
`Extension(name, **kwargs). This function modifies the kwargs according to
|
092f8c28 |
the configuration file.
This function modifies `sys.argv`.
|
16f4d000 |
"""
|
092f8c28 |
global config_file, config_file_present
#### Determine the name of the configuration file.
config_file_option = '--configfile'
|
e21b2c93 |
config_file_option_present = False
|
092f8c28 |
# Handle command line option
for i, opt in enumerate(sys.argv):
if not opt.startswith(config_file_option):
continue
|
e21b2c93 |
config_file_option_present = True
|
092f8c28 |
l, _, config_file = opt.partition('=')
if l != config_file_option or not config_file:
|
d256d076 |
print('Error: Expecting {}=PATH'.format(config_file_option),
|
092f8c28 |
file=sys.stderr)
|
c867f67a |
sys.exit(1)
|
092f8c28 |
sys.argv.pop(i)
break
else:
config_file = 'build.conf'
|
16f4d000 |
#### Read build configuration file.
configs = configparser.ConfigParser()
try:
|
092f8c28 |
with open(config_file) as f:
|
16f4d000 |
configs.read_file(f)
except IOError:
config_file_present = False
|
e21b2c93 |
if config_file_option_present:
print("Error: '{}' option was provided, but '{}' does not exist"
.format(config_file_option, config_file))
sys.exit(1)
|
16f4d000 |
else:
config_file_present = True
#### Handle section aliases.
for short, long in aliases:
if short in configs:
if long in configs:
print('Error: both {} and {} sections present in {}.'.format(
|
092f8c28 |
short, long, config_file))
|
c867f67a |
sys.exit(1)
|
16f4d000 |
configs[long] = configs[short]
del configs[short]
#### Apply config from file. Use [DEFAULT] section for missing sections.
defaultconfig = configs.defaults()
for name, kwargs in exts.items():
config = configs[name] if name in configs else defaultconfig
for key, value in config.items():
# Most, but not all, keys are lists of strings
if key == 'language':
pass
elif key == 'optional':
value = bool(int(value))
else:
value = value.split()
if key == 'define_macros':
value = [tuple(entry.split('=', maxsplit=1))
for entry in value]
value = [(entry[0], None) if len(entry) == 1 else entry
for entry in value]
if key in kwargs:
msg = 'Caution: user config in file {} shadows {}.{}.'
if build_summary is not None:
|
092f8c28 |
build_summary.append(msg.format(config_file, name, key))
|
16f4d000 |
kwargs[key] = value
|
092f8c28 |
kwargs.setdefault('depends', []).append(config_file)
|
16f4d000 |
if config is not defaultconfig:
del configs[name]
unknown_sections = configs.sections()
if unknown_sections:
print('Error: Unknown sections in file {}: {}'.format(
|
092f8c28 |
config_file, ', '.join(unknown_sections)))
|
c867f67a |
sys.exit(1)
|
16f4d000 |
return exts
|
7fc080d5 |
def check_python_version(min_version):
installed_version = sys.version_info[:3]
if installed_version < min_version:
print('Error: Python {} required, but {} is installed'.format(
'.'.join(map(str, min_version)),
'.'.join(map(str, installed_version)))
)
sys.exit(1)
|
874b6c29 |
def check_versions():
|
cd3242d1 |
global version, version_is_from_git
# Let Kwant itself determine its own version. We cannot simply import
# kwant, as it is not built yet.
|
2fbec07b |
spec = importlib.util.spec_from_file_location('version', 'kwant/version.py')
version_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(version_module)
|
cd3242d1 |
|
874b6c29 |
version_module.ensure_python()
version = version_module.version
version_is_from_git = version_module.version_is_from_git
|
cd3242d1 |
def init_cython():
|
eba071b8 |
"""Set the global variable `cythonize` (and other related globals).
The variable `cythonize` can be in three states:
* If Cython should be run and is ready, it contains the `cythonize()`
function.
* If Cython is not to be run, it contains `False`.
* If Cython should, but cannot be run it contains `None`. A help message
on how to solve the problem is stored in `cython_help`.
This function modifies `sys.argv`.
"""
|
10faf93a |
global cythonize, cython_help
|
ff6b2b54 |
|
55b46a70 |
cython_option = '--cython'
|
60f02546 |
required_cython_version = (0, 24)
|
557f595d |
try:
|
55b46a70 |
sys.argv.remove(cython_option)
|
eba071b8 |
cythonize = True
|
cd3242d1 |
except ValueError:
|
eba071b8 |
cythonize = version_is_from_git
|
cd3242d1 |
|
eba071b8 |
if cythonize:
|
cd3242d1 |
try:
import Cython
from Cython.Build import cythonize
except ImportError:
|
eba071b8 |
cythonize = None
|
cd3242d1 |
else:
|
eba071b8 |
#### Get Cython version.
|
cd3242d1 |
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)
|
557f595d |
|
55b46a70 |
if cython_version < required_cython_version:
|
eba071b8 |
cythonize = None
|
f837ca97 |
if cythonize is None:
msg = ("Install Cython >= {0} or use"
" a source distribution (tarball) of Kwant.")
ver = '.'.join(str(e) for e in required_cython_version)
cython_help = msg.format(ver)
|
eba071b8 |
else:
msg = "Run setup.py with the {} option to enable Cython."
|
55b46a70 |
cython_help = msg.format(cython_option)
|
eba071b8 |
|
e4830940 |
|
aa7fcf46 |
def banner(title=''):
starred = title.center(79, '*')
return '\n' + starred if title else starred
|
c2ac7c90 |
|
9997ecc2 |
|
06484062 |
class build_ext(build_ext_orig):
|
43faf533 |
def run(self):
|
7ee7b487 |
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.
|
092f8c28 |
with open(config_file, 'w') as f:
f.write('# Build configuration created by setup.py '
'- feel free to modify.\n')
|
7ee7b487 |
|
43faf533 |
try:
|
d36b3aa3 |
super().run()
|
43faf533 |
except (DistutilsError, CCompilerError):
|
7327aa5d |
error_msg = self.__error_msg.format(
header=banner(' Error '), sep=banner())
|
092f8c28 |
print(error_msg.format(file=config_file, summary=build_summary),
|
c2ac7c90 |
file=sys.stderr)
|
43faf533 |
raise
|
16f4d000 |
print(banner(' Build summary '), *build_summary, sep='\n')
|
aa7fcf46 |
print(banner())
|
43faf533 |
|
7327aa5d |
__error_msg = """{header}
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}}.
Build configuration was:
{{summary}}
{sep}
"""
|
e6ae9762 |
|
06484062 |
class build(build_orig):
|
20171914 |
def run(self):
|
d36b3aa3 |
super().run()
|
20171914 |
write_version(os.path.join(self.build_lib, *STATIC_VERSION_PATH))
|
e4830940 |
def git_lsfiles():
|
20171914 |
if not version_is_from_git:
|
a9f326a9 |
return
|
e4830940 |
try:
|
cf363628 |
p = subprocess.Popen(['git', 'ls-files'], cwd=distr_root,
|
e4830940 |
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError:
return
if p.wait() != 0:
return
|
a215c8cf |
return p.communicate()[0].decode().split('\n')[:-1]
|
1a077ad2 |
|
6c683464 |
# Make the command "sdist" depend on "build". This verifies that the
# distribution in the current state actually builds. It also makes sure that
|
4c4442e5 |
# the Cython-made C files will be up-to-date and included in the source.
|
06484062 |
class sdist(sdist_orig):
sub_commands = [('build', None)] + sdist_orig.sub_commands
|
e4830940 |
def run(self):
|
cf77f221 |
"""Create MANIFEST.in from git if possible, otherwise check that
MANIFEST.in is present.
|
9997ecc2 |
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.
"""
|
55b46a70 |
manifest_in_file = 'MANIFEST.in'
manifest = os.path.join(distr_root, manifest_in_file)
|
e4830940 |
names = git_lsfiles()
if names is None:
|
9997ecc2 |
if not (os.path.isfile(manifest) and os.access(manifest, os.R_OK)):
|
55b46a70 |
print("Error:", manifest_in_file,
|
9997ecc2 |
"file is missing and Git is not available"
" to regenerate it.", file=sys.stderr)
|
c867f67a |
sys.exit(1)
|
e4830940 |
else:
|
9997ecc2 |
with open(manifest, 'w') as f:
|
e4830940 |
for name in names:
a, sep, b = name.rpartition('/')
if b == '.gitignore':
continue
stem, dot, extension = b.rpartition('.')
|
9997ecc2 |
f.write('include {}'.format(name))
|
e4830940 |
if extension == 'pyx':
|
9997ecc2 |
f.write(''.join([' ', a, sep, stem, dot, 'c']))
f.write('\n')
|
e4830940 |
|
d36b3aa3 |
super().run()
|
e4830940 |
if names is None:
|
4b0ba762 |
msg = ("Git was not available to generate the list of files to be "
"included in the\nsource distribution. The old {} was used.")
|
55b46a70 |
msg = msg.format(manifest_in_file)
|
4b0ba762 |
print(banner(' Caution '), msg, banner(), sep='\n', file=sys.stderr)
|
6c683464 |
|
20171914 |
def make_release_tree(self, base_dir, files):
|
d36b3aa3 |
super().make_release_tree(base_dir, files)
|
20171914 |
write_version(os.path.join(base_dir, *STATIC_VERSION_PATH))
|
e6ae9762 |
|
43faf533 |
|
d36b3aa3 |
# The following class is based on a recipe in
# http://doc.pytest.org/en/latest/goodpractices.html#manual-integration.
|
04edb898 |
class test(test_orig):
|
d36b3aa3 |
user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")]
def initialize_options(self):
super().initialize_options()
self.pytest_args = ''
def run_tests(self):
import shlex
|
04edb898 |
try:
|
d36b3aa3 |
import pytest
except:
print('The Python package "pytest" is required to run tests.',
|
04edb898 |
file=sys.stderr)
|
c867f67a |
sys.exit(1)
|
d36b3aa3 |
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
|
04edb898 |
|
20171914 |
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?
|
e6ae9762 |
try:
|
20171914 |
os.remove(fname)
except OSError:
pass
with open(fname, 'w') as f:
f.write("# This file has been created by setup.py.\n")
|
40b9b37b |
f.write("version = '{}'\n".format(version))
|
43faf533 |
|
8ee281a9 |
def long_description():
text = []
try:
|
79efdfee |
with open('README.rst', encoding='utf8') as f:
|
8ee281a9 |
for line in f:
|
55b46a70 |
if line.startswith('See also in this directory:'):
|
8ee281a9 |
break
text.append(line.rstrip())
|
c6e82aa6 |
while text[-1] == "":
text.pop()
|
8ee281a9 |
except:
return ''
return '\n'.join(text)
|
2f82b693 |
def search_libs(libs):
|
783253a5 |
cmd = ['gcc']
cmd.extend(['-l' + lib for lib in libs])
cmd.extend(['-o/dev/null', '-xc', '-'])
|
43faf533 |
try:
|
783253a5 |
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
|
43faf533 |
except OSError:
pass
else:
|
3d73b19c |
p.communicate(input=b'int main() {}\n')
|
783253a5 |
if p.wait() == 0:
|
2f82b693 |
return libs
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 and the MUMPS binaries in the conda-forge channel."""
lib_sets = [
# Debian
|
014df638 |
['zmumps_scotch', 'mumps_common_scotch', 'mpiseq_scotch',
'pord', 'gfortran'],
|
2f82b693 |
# Conda (via conda-forge).
|
014df638 |
['zmumps_seq', 'mumps_common_seq'],
|
2f82b693 |
]
for libs in lib_sets:
|
014df638 |
found_libs = search_libs(libs)
|
2f82b693 |
if found_libs:
return found_libs
return []
|
16f4d000 |
def configure_special_extensions(exts, build_summary):
#### Special config for MUMPS.
mumps = exts['kwant.linalg._mumps']
if 'libraries' in mumps:
|
43faf533 |
build_summary.append('User-configured MUMPS')
else:
|
2f82b693 |
mumps_libs = search_mumps()
if mumps_libs:
mumps['libraries'] = mumps_libs
|
783253a5 |
build_summary.append('Auto-configured MUMPS')
|
16f4d000 |
else:
mumps = None
|
f837ca97 |
del exts['kwant.linalg._mumps']
|
16f4d000 |
build_summary.append('No MUMPS support')
|
43faf533 |
|
16f4d000 |
return exts
|
43faf533 |
|
16f4d000 |
def maybe_cythonize(exts):
|
e315de5a |
"""Prepare a list of `Extension` instances, ready for `setup()`.
|
50a8240f |
|
16f4d000 |
The argument `exts` must be a mapping of names to kwargs to be passed
on to `Extension`.
|
e315de5a |
If Cython is to be run, create the extensions and calls `cythonize()` on
them. If Cython is not to be run, replace .pyx file with .c or .cpp,
check timestamps, and create the extensions.
|
50a8240f |
"""
|
eba071b8 |
if cythonize:
|
16f4d000 |
return cythonize([Extension(name, **kwargs)
for name, kwargs in exts.items()],
language_level=3,
|
10faf93a |
compiler_directives={'linetrace': True})
|
b4b6375d |
# Cython is not going to be run: replace pyx extension by that of
# the shipped translated file.
|
43faf533 |
result = []
|
557f595d |
problematic_files = []
|
16f4d000 |
for name, kwargs in exts.items():
language = kwargs.get('language')
|
b4b6375d |
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)
|
c867f67a |
sys.exit(1)
|
b4b6375d |
pyx_files = []
cythonized_files = []
|
16f4d000 |
sources = []
for f in kwargs['sources']:
|
b4b6375d |
if f.endswith('.pyx'):
pyx_files.append(f)
f = f.rstrip('.pyx') + ext
cythonized_files.append(f)
|
16f4d000 |
sources.append(f)
kwargs['sources'] = sources
|
b4b6375d |
# 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:
|
eba071b8 |
msg = "Cython-generated file {} is missing."
print(banner(" Error "), msg.format(f), "",
cython_help, banner(), sep="\n", file=sys.stderr)
|
c867f67a |
sys.exit(1)
|
b4b6375d |
|
16f4d000 |
for f in pyx_files + kwargs.get('depends', []):
|
092f8c28 |
if f == config_file:
|
b4b6375d |
# 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)
|
43faf533 |
|
16f4d000 |
result.append(Extension(name, **kwargs))
|
43faf533 |
|
557f595d |
if problematic_files:
|
eba071b8 |
msg = ("Some Cython source files are newer than files that have "
"been derived from them:\n{}")
msg = msg.format(", ".join(problematic_files))
# Cython should be run but won't. Signal an error if this is because
# Cython *cannot* be run, warn otherwise.
error = cythonize is None
if cythonize is False:
dontworry = ('(Do not worry about this if you are building Kwant '
'from unmodified sources,\n'
'e.g. with "pip install".)\n\n')
msg = dontworry + msg
print(banner(" Error " if error else " Caution "), msg, "",
cython_help, banner(), sep="\n", file=sys.stderr)
if error:
|
c867f67a |
sys.exit(1)
|
557f595d |
|
43faf533 |
return result
|
b4b6375d |
|
b9c2b0c2 |
def maybe_add_numpy_include(exts):
# Add NumPy header path to include_dirs of all the extensions.
try:
import numpy
except ImportError:
print(banner(' Caution '), 'NumPy header directory cannot be determined'
' ("import numpy" failed).', banner(), sep='\n', file=sys.stderr)
else:
numpy_include = numpy.get_include()
for ext in exts.values():
ext.setdefault('include_dirs', []).append(numpy_include)
return exts
|
43faf533 |
def main():
|
39b65acd |
check_python_version((3, 6))
|
874b6c29 |
check_versions()
|
55b46a70 |
exts = collections.OrderedDict([
('kwant._system',
dict(sources=['kwant/_system.pyx'],
include_dirs=['kwant/graph'])),
('kwant.operator',
dict(sources=['kwant/operator.pyx'],
include_dirs=['kwant/graph'])),
('kwant.graph.core',
dict(sources=['kwant/graph/core.pyx'],
depends=['kwant/graph/core.pxd', 'kwant/graph/defs.h',
'kwant/graph/defs.pxd'])),
|
9ad36a5c |
('kwant.graph.dijkstra',
dict(sources=['kwant/graph/dijkstra.pyx'])),
|
55b46a70 |
('kwant.linalg.lapack',
|
3d8db694 |
dict(sources=['kwant/linalg/lapack.pyx'])),
|
55b46a70 |
('kwant.linalg._mumps',
dict(sources=['kwant/linalg/_mumps.pyx'],
depends=['kwant/linalg/cmumps.pxd']))])
|
3d8db694 |
aliases = [('mumps', 'kwant.linalg._mumps')]
|
16f4d000 |
|
cd3242d1 |
init_cython()
|
55b46a70 |
global build_summary
|
16f4d000 |
build_summary = []
|
55b46a70 |
exts = configure_extensions(exts, aliases, build_summary)
|
16f4d000 |
exts = configure_special_extensions(exts, build_summary)
|
b9c2b0c2 |
exts = maybe_add_numpy_include(exts)
|
16f4d000 |
exts = maybe_cythonize(exts)
|
55b46a70 |
classifiers = """\
Development Status :: 5 - Production/Stable
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"""
|
e9557cf7 |
packages = find_packages('.')
|
43faf533 |
setup(name='kwant',
|
20171914 |
version=version,
|
c2ac7c90 |
author='C. W. Groth (CEA), M. Wimmer, '
'A. R. Akhmerov, X. Waintal (CEA), and others',
|
b8ec32a7 |
author_email='authors@kwant-project.org',
|
8f5e866d |
description=("Package for numerical quantum transport calculations "
|
ec006432 |
"(Python 3 version)"),
|
8ee281a9 |
long_description=long_description(),
platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
|
e001ab42 |
url="http://kwant-project.org/",
license="BSD",
|
e9557cf7 |
packages=packages,
package_data={p: ['*.pxd', '*.h'] for p in packages},
|
06484062 |
cmdclass={'build': build,
'sdist': sdist,
'build_ext': build_ext,
|
04edb898 |
'test': test},
|
16f4d000 |
ext_modules=exts,
|
39b65acd |
install_requires=['numpy >= 1.13.3', 'scipy >= 0.19.1',
|
6eb2c201 |
'tinyarray >= 1.2'],
|
7cb76722 |
extras_require={
|
42dd420d |
# The oldest versions between: Debian stable, Ubuntu LTS
|
245d4985 |
'plotting': ['matplotlib >= 2.1.1',
'plotly >= 2.2.2'],
|
39b65acd |
'continuum': 'sympy >= 1.1.1',
|
cec7d4a2 |
# qsymm is only packaged on PyPI
|
8c0f0a9b |
'qsymm': 'qsymm >= 1.2.6',
|
7cb76722 |
},
|
55b46a70 |
classifiers=[c.strip() for c in classifiers.split('\n')])
|
a1e88e83 |
|
43faf533 |
if __name__ == '__main__':
main()
|