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