setup.py 9.47 KB
Newer Older
Christoph Groth's avatar
Christoph Groth committed
1
2
#!/usr/bin/env python

3
# Copyright 2012-2016 Tinyarray authors.
Christoph Groth's avatar
Christoph Groth committed
4
5
#
# This file is part of Tinyarray.  It is subject to the license terms in the
6
7
8
9
10
# file LICENSE.rst found in the top-level directory of this distribution and
# at https://gitlab.kwant-project.org/kwant/tinyarray/blob/master/LICENSE.rst.
# A list of Tinyarray authors can be found in the README.rst file at the
# top-level directory of this distribution and at
# https://gitlab.kwant-project.org/kwant/tinyarray.
Christoph Groth's avatar
Christoph Groth committed
11

12
13
from __future__ import print_function

Christoph Groth's avatar
Christoph Groth committed
14
15
import subprocess
import os
16
import sys
17
import collections
Christoph Groth's avatar
Christoph Groth committed
18
19
from setuptools import setup, Extension, Command
from sysconfig import get_platform
20
from distutils.errors import DistutilsError, DistutilsModuleError
Christoph Groth's avatar
Christoph Groth committed
21
22
from setuptools.command.build_ext import build_ext
from setuptools.command.sdist import sdist
Christoph Groth's avatar
Christoph Groth committed
23

Joseph Weston's avatar
Joseph Weston committed
24
25
26
27
28
if sys.version_info.major == 2
    import ConfigParser as configparser
else:
    import configparser

29
30
31
32
33
34
35
36
37
38
39
40
41
42
try:
    from os.path import samefile
except ImportError:
    # This code path will be taken on Windows for Python < 3.2.
    # TODO: remove this once we require Python 3.2.

    def _getfinalpathname(f):
        return os.path.normcase(os.path.abspath(f))

    # This simple mockup should do in practice.
    def samefile(f1, f2):
        return _getfinalpathname(f1) == _getfinalpathname(f2)


43
SAVED_VERSION_FILE = 'version'
44

Christoph Groth's avatar
Christoph Groth committed
45

46
distr_root = os.path.dirname(os.path.abspath(__file__))
Christoph Groth's avatar
Christoph Groth committed
47

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

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
    the configuration file.

    This function modifies `sys.argv`.
    """
    global config_file, config_file_present

    #### Determine the name of the configuration file.
    config_file_option = '--configfile'
    # Handle command line option
    for i, opt in enumerate(sys.argv):
        if not opt.startswith(config_file_option):
            continue
        l, _, config_file = opt.partition('=')
        if l != config_file_option or not config_file:
            print('error: Expecting {}=PATH'.format(config_file_option),
                  file=sys.stderr)
            exit(1)
        sys.argv.pop(i)
        break
    else:
        config_file = 'build.conf'

    #### Read build configuration file.
    configs = configparser.ConfigParser()
    try:
        with open(config_file) as f:
            configs.read_file(f)
    except IOError:
        config_file_present = False
    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(
                    short, long, config_file))
                exit(1)
            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('=', 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:
                    build_summary.append(msg.format(config_file, name, key))
            kwargs[key] = value

        kwargs.setdefault('depends', []).append(config_file)
        if config is not defaultconfig:
            del configs[name]

    unknown_sections = configs.sections()
    if unknown_sections:
        print('Error: Unknown sections in file {}: {}'.format(
            config_file, ', '.join(unknown_sections)))
        exit(1)

    return exts


Christoph Groth's avatar
Christoph Groth committed
135
136
def get_version_from_git():
    try:
137
138
        p = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'],
                             cwd=distr_root,
Christoph Groth's avatar
Christoph Groth committed
139
140
141
142
143
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError:
        return
    if p.wait() != 0:
        return
144
    if not samefile(p.communicate()[0].decode().rstrip('\n'), distr_root):
145
146
147
148
        # The top-level directory of the current Git repository is not the same
        # as the root directory of the source distribution: do not extract the
        # version from Git.
        return
Christoph Groth's avatar
Christoph Groth committed
149

150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
    # 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', '--long'] + opts,
                                 cwd=distr_root,
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except OSError:
            return
        if p.wait() == 0:
            break
    else:
        return
    description = p.communicate()[0].decode().strip('v').rstrip('\n')

    release, dev, git = description.rsplit('-', 2)
    version = [release]
    labels = []
    if dev != "0":
        version.append(".dev{}".format(dev))
        labels.append(git)
Christoph Groth's avatar
Christoph Groth committed
171
172

    try:
173
        p = subprocess.Popen(['git', 'diff', '--quiet'], cwd=distr_root)
Christoph Groth's avatar
Christoph Groth committed
174
    except OSError:
175
        labels.append('confused') # This should never happen.
Christoph Groth's avatar
Christoph Groth committed
176
177
    else:
        if p.wait() == 1:
178
            labels.append('dirty')
Christoph Groth's avatar
Christoph Groth committed
179

180
181
182
    if labels:
        version.append('+')
        version.append(".".join(labels))
Christoph Groth's avatar
Christoph Groth committed
183

184
185
186
187
188
189
190
191
192
193
194
    return "".join(version)


with open(os.path.join(SAVED_VERSION_FILE), 'r') as f:
    for line in f:
        line = line.strip()
        if line.startswith('#'):
            continue
        else:
            version = line
            break
Christoph Groth's avatar
Christoph Groth committed
195
    else:
196
197
198
199
200
201
        raise RuntimeError("Saved version file does not contain version.")
version_is_from_git = (version == "__use_git__")
if version_is_from_git:
    version = get_version_from_git()
    if not version:
        version = "unknown"
Christoph Groth's avatar
Christoph Groth committed
202
203
204
205
206
207


def long_description():
    text = []
    skip = True
    try:
208
        with open('README.rst') as f:
Christoph Groth's avatar
Christoph Groth committed
209
210
211
212
213
            for line in f:
                if line == "\n":
                    if skip:
                        skip = False
                        continue
214
215
                    elif text[-1] == '\n':
                        text.pop()
Christoph Groth's avatar
Christoph Groth committed
216
217
                        break
                if not skip:
218
                    text.append(line)
Christoph Groth's avatar
Christoph Groth committed
219
220
    except:
        return ''
221
222
    text[-1] = text[-1].rstrip()
    return ''.join(text)
Christoph Groth's avatar
Christoph Groth committed
223
224


225
226
class our_build_ext(build_ext):
    def run(self):
227
        with open(os.path.join('src', 'version.hh'), 'w') as f:
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
            f.write("// This file has been generated by setup.py.\n")
            f.write("// It is not included in source distributions.\n")
            f.write('#define VERSION "{}"\n'.format(version))
        build_ext.run(self)


class our_sdist(sdist):
    def make_release_tree(self, base_dir, files):
        sdist.make_release_tree(self, base_dir, files)

        fname = os.path.join(base_dir, SAVED_VERSION_FILE)
        # This could be a hard link, so try to delete it first.  Is there any way
        # to do this atomically together with opening?
        try:
            os.remove(fname)
        except OSError:
            pass
        with open(fname, 'w') as f:
            f.write("# This file has been generated by setup.py.\n{}\n"
                    .format(version))


Christoph Groth's avatar
Christoph Groth committed
250
def main():
251
252
253
254
255
256
257
258
259
    exts = collections.OrderedDict([
        ('tinyarray',
         dict(language='c++',
              sources=['src/arithmetic.cc', 'src/array.cc',
                       'src/functions.cc'],
              depends=['src/arithmetic.hh', 'src/array.hh',
                       'src/conversion.hh', 'src/functions.hh']))])

    exts = configure_extensions(exts)
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275

    classifiers = """\
    Development Status :: 5 - Production/Stable
    Intended Audience :: Science/Research
    Intended Audience :: Developers
    License :: OSI Approved :: BSD License
    Programming Language :: Python :: 2
    Programming Language :: Python :: 3
    Programming Language :: C++
    Topic :: Software Development
    Topic :: Scientific/Engineering
    Operating System :: POSIX
    Operating System :: Unix
    Operating System :: MacOS
    Operating System :: Microsoft :: Windows"""

Christoph Groth's avatar
Christoph Groth committed
276
    setup(name='tinyarray',
277
          version=version,
Christoph Groth's avatar
Christoph Groth committed
278
          author='Christoph Groth (CEA) and others',
Christoph Groth's avatar
Christoph Groth committed
279
280
281
          author_email='christoph.groth@cea.fr',
          description="Arrays of numbers for Python, optimized for small sizes",
          long_description=long_description(),
282
          url="https://gitlab.kwant-project.org/kwant/tinyarray",
Christoph Groth's avatar
Christoph Groth committed
283
284
          download_url="http://downloads.kwant-project.org/tinyarray/",
          license="Simplified BSD license",
Christoph Groth's avatar
Christoph Groth committed
285
          platforms=["Unix", "Linux", "Mac OS-X", "Windows"],
286
          classifiers=classifiers.split('\n'),
287
          cmdclass={'build_ext': our_build_ext,
Christoph Groth's avatar
Christoph Groth committed
288
                    'sdist': our_sdist},
289
290
          ext_modules=[Extension(name, **kwargs)
                       for name, kwargs in exts.items()],
291
292
          setup_requires=['pytest-runner'],
          tests_require=['pytest'])
Christoph Groth's avatar
Christoph Groth committed
293
294
295
296


if __name__ == '__main__':
    main()