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