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