xref: /petsc/src/binding/petsc4py/setup.py (revision bfe80ac4a46d58cb7760074b25f5e81b2f541d8a)
1#!/usr/bin/env python3
2# Author:  Lisandro Dalcin
3# Contact: dalcinl@gmail.com
4
5import re
6import os
7import sys
8
9try:
10    import setuptools
11except ImportError:
12    setuptools = None
13
14topdir = os.path.abspath(os.path.dirname(__file__))
15sys.path.insert(0, os.path.join(topdir, 'conf'))
16
17pyver = sys.version_info[:2]
18if pyver < (2, 6) or (3, 0) <= pyver < (3, 2):
19    raise RuntimeError('Python version 2.6, 2.7 or >= 3.2 required')
20if pyver == (2, 6) or pyver == (3, 2):
21    sys.stderr.write('WARNING: Python %d.%d is not supported.\n' % pyver)
22
23PNAME = 'PETSc'
24EMAIL = 'petsc-maint@mcs.anl.gov'
25PLIST = [PNAME]
26
27# --------------------------------------------------------------------
28# Metadata
29# --------------------------------------------------------------------
30
31
32def F(string):
33    return string.format(
34        Name=PNAME,
35        name=PNAME.lower(),
36        pyname=PNAME.lower() + '4py',
37    )
38
39
40def get_name():
41    return F('{pyname}')
42
43
44def get_version():
45    try:
46        return get_version.result
47    except AttributeError:
48        pass
49    pkg_init_py = os.path.join(F('{pyname}'), '__init__.py')
50    with open(os.path.join(topdir, 'src', pkg_init_py)) as f:
51        m = re.search(r"__version__\s*=\s*'(.*)'", f.read())
52    version = m.groups()[0]
53    get_version.result = version
54    return version
55
56
57def description():
58    return F('{Name} for Python')
59
60
61def long_description():
62    with open(os.path.join(topdir, 'DESCRIPTION.rst')) as f:
63        return f.read()
64
65
66url = F('https://gitlab.com/{name}/{name}')
67pypiroot = F('https://pypi.io/packages/source')
68pypislug = F('{pyname}')[0] + F('/{pyname}')
69tarball = F('{pyname}-%s.tar.gz' % get_version())
70download = '/'.join([pypiroot, pypislug, tarball])
71
72classifiers = """
73Operating System :: POSIX
74Intended Audience :: Developers
75Intended Audience :: Science/Research
76Programming Language :: C
77Programming Language :: C++
78Programming Language :: Cython
79Programming Language :: Python
80Programming Language :: Python :: 2
81Programming Language :: Python :: 3
82Programming Language :: Python :: Implementation :: CPython
83Topic :: Scientific/Engineering
84Topic :: Software Development :: Libraries :: Python Modules
85Development Status :: 5 - Production/Stable
86""".strip().split('\n')
87
88keywords = """
89scientific computing
90parallel computing
91MPI
92""".strip().split('\n')
93
94platforms = """
95POSIX
96Linux
97macOS
98FreeBSD
99""".strip().split('\n')
100
101metadata = {
102    'name': get_name(),
103    'version': get_version(),
104    'description': description(),
105    'long_description': long_description(),
106    'url': url,
107    'download_url': download,
108    'classifiers': classifiers,
109    'keywords': keywords + PLIST,
110    'license': 'BSD-2-Clause',
111    'platforms': platforms,
112    'author': 'Lisandro Dalcin',
113    'author_email': 'dalcinl@gmail.com',
114    'maintainer': F('{Name} Team'),
115    'maintainer_email': EMAIL,
116}
117metadata.update(
118    {
119        'requires': ['numpy'],
120    }
121)
122
123metadata_extra = {
124    'long_description_content_type': 'text/x-rst',
125}
126
127# --------------------------------------------------------------------
128# Extension modules
129# --------------------------------------------------------------------
130
131
132def sources():
133    src = {
134        'source': F('{pyname}/{Name}.pyx'),
135        'depends': [
136            F('{pyname}/*.pyx'),
137            F('{pyname}/*.pxd'),
138            F('{pyname}/{Name}/*.pyx'),
139            F('{pyname}/{Name}/*.pxd'),
140            F('{pyname}/{Name}/*.pxi'),
141        ],
142        'workdir': 'src',
143    }
144    return [src]
145
146
147def extensions():
148    from os import walk
149    from glob import glob
150    from os.path import join
151
152    #
153    depends = []
154    glob_join = lambda *args: glob(join(*args))
155    for pth, _, _ in walk('src'):
156        depends += glob_join(pth, '*.h')
157        depends += glob_join(pth, '*.c')
158    for pkg in map(str.lower, reversed(PLIST)):
159        if (pkg.upper() + '_DIR') in os.environ:
160            pd = os.environ[pkg.upper() + '_DIR']
161            pa = os.environ.get('PETSC_ARCH', '')
162            depends += glob_join(pd, 'include', '*.h')
163            depends += glob_join(pd, 'include', pkg, 'private', '*.h')
164            depends += glob_join(pd, pa, 'include', '%sconf.h' % pkg)
165    #
166    include_dirs = []
167    numpy_include = os.environ.get('NUMPY_INCLUDE')
168    if numpy_include is not None:
169        numpy_includes = [numpy_include]
170    else:
171        try:
172            import numpy
173
174            numpy_includes = [numpy.get_include()]
175        except ImportError:
176            numpy_includes = []
177    include_dirs.extend(numpy_includes)
178    if F('{pyname}') != 'petsc4py':
179        try:
180            import petsc4py
181
182            petsc4py_includes = [petsc4py.get_include()]
183        except ImportError:
184            petsc4py_includes = []
185        include_dirs.extend(petsc4py_includes)
186    #
187    ext = {
188        'name': F('{pyname}.lib.{Name}'),
189        'sources': [F('src/{pyname}/{Name}.c')],
190        'depends': depends,
191        'include_dirs': [
192            'src',
193            F('src/{pyname}/include'),
194        ]
195        + include_dirs,
196        'define_macros': [
197            ('MPICH_SKIP_MPICXX', 1),
198            ('OMPI_SKIP_MPICXX', 1),
199            ('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION'),
200        ],
201    }
202    return [ext]
203
204
205# --------------------------------------------------------------------
206# Setup
207# --------------------------------------------------------------------
208
209
210def get_release():
211    suffix = os.path.join('src', 'binding', F('{pyname}'))
212    if not topdir.endswith(os.path.join(os.path.sep, suffix)):
213        return True
214    release = 1
215    rootdir = os.path.abspath(os.path.join(topdir, *[os.path.pardir] * 3))
216    version_h = os.path.join(rootdir, 'include', F('{name}version.h'))
217    release_macro = '%s_VERSION_RELEASE' % F('{name}').upper()
218    version_re = re.compile(r'#define\s+%s\s+([-]*\d+)' % release_macro)
219    if os.path.exists(version_h) and os.path.isfile(version_h):
220        with open(version_h, 'r') as f:
221            release = int(version_re.search(f.read()).groups()[0])
222    return bool(release)
223
224
225def requires(pkgname, major, minor, release=True):
226    minor = minor + int(not release)
227    devel = '' if release else '.dev0'
228    vmin = f'{major}.{minor}{devel}'
229    vmax = f'{major}.{minor+1}'
230    return f'{pkgname}>={vmin},<{vmax}'
231
232
233def run_setup():
234    is_sdist = 'sdist' in sys.argv
235    setup_args = metadata.copy()
236    vstr = setup_args['version'].split('.')[:2]
237    x, y = tuple(map(int, vstr))
238    release = get_release()
239    if not release:
240        setup_args['version'] = '%d.%d.0.dev0' % (x, y + 1)
241    if setuptools:
242        setup_args['zip_safe'] = False
243        numpy_pin = 'numpy'
244        if not is_sdist:
245            try:
246                import numpy
247
248                major = int(numpy.__version__.partition('.')[0])
249                numpy_pin = 'numpy>=1.19' if major >= 2 else 'numpy<2'
250            except ImportError:
251                pass
252        setup_args['setup_requires'] = ['numpy']
253        setup_args['install_requires'] = [numpy_pin]
254        for pkg in map(str.lower, PLIST):
255            PKG_DIR = os.environ.get(pkg.upper() + '_DIR')
256            if not (PKG_DIR and os.path.isdir(PKG_DIR)):
257                package = requires(pkg, x, y, release)
258                setup_args['setup_requires'] += [package]
259                setup_args['install_requires'] += [package]
260        if F('{pyname}') != 'petsc4py':
261            package = requires('petsc4py', x, y, release)
262            setup_args['setup_requires'] += [package]
263            setup_args['install_requires'] += [package]
264        setup_args.update(metadata_extra)
265    #
266    conf = __import__(F('conf{name}'))
267    conf.setup(
268        packages=[
269            F('{pyname}'),
270            F('{pyname}.lib'),
271            F('{pyname}.lib._pytypes'),
272            F('{pyname}.lib._pytypes.viewer'),
273        ],
274        package_dir={'': 'src'},
275        package_data={
276            F('{pyname}'): [
277                F('{Name}.pxd'),
278                F('{Name}*.h'),
279                F('include/{pyname}/*.h'),
280                F('include/{pyname}/*.i'),
281                'py.typed',
282                '*.pyi',
283                '*/*.pyi',
284            ],
285            F('{pyname}.lib'): [
286                F('{name}.cfg'),
287            ],
288        },
289        cython_sources=[src for src in sources()],  # noqa: C416
290        ext_modules=[conf.Extension(**ext) for ext in extensions()],
291        **setup_args,
292    )
293
294
295# --------------------------------------------------------------------
296
297
298def main():
299    run_setup()
300
301
302if __name__ == '__main__':
303    main()
304
305# --------------------------------------------------------------------
306