xref: /petsc/src/binding/petsc4py/setup.py (revision ccfb0f9f40a0131988d7995ed9679700dae2a75a)
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
30py_limited_api = (3, 10)
31
32
33def F(string):
34    return string.format(
35        Name=PNAME,
36        name=PNAME.lower(),
37        pyname=PNAME.lower() + '4py',
38    )
39
40
41def get_name():
42    return F('{pyname}')
43
44
45def get_version():
46    try:
47        return get_version.result
48    except AttributeError:
49        pass
50    pkg_init_py = os.path.join(F('{pyname}'), '__init__.py')
51    with open(os.path.join(topdir, 'src', pkg_init_py)) as f:
52        m = re.search(r"__version__\s*=\s*'(.*)'", f.read())
53    version = m.groups()[0]
54    get_version.result = version
55    return version
56
57
58def description():
59    return F('{Name} for Python')
60
61
62def long_description():
63    with open(os.path.join(topdir, 'DESCRIPTION.rst')) as f:
64        return f.read()
65
66
67url = F('https://gitlab.com/{name}/{name}')
68pypiroot = F('https://pypi.io/packages/source')
69pypislug = F('{pyname}')[0] + F('/{pyname}')
70tarball = F('{pyname}-%s.tar.gz' % get_version())
71download = '/'.join([pypiroot, pypislug, tarball])
72
73classifiers = """
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 :: 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
127def get_build_pysabi():
128    abi = os.environ.get("PETSC4PY_BUILD_PYSABI", "").lower()
129    if abi and sys.implementation.name == "cpython":
130        if abi in {"false", "no", "off", "n", "0"}:
131            return None
132        if abi in {"true", "yes", "on", "y", "1"} | {"abi3"}:
133            return py_limited_api
134        if abi.startswith("cp"):
135            abi = abi[2:]
136        if "." in abi:
137            x, y = abi.split(".")
138        else:
139            x, y = abi[0], abi[1:]
140        return (int(x), int(y))
141    return None
142
143# --------------------------------------------------------------------
144# Extension modules
145# --------------------------------------------------------------------
146
147
148def sources():
149    src = {
150        'source': F('{pyname}/{Name}.pyx'),
151        'depends': [
152            F('{pyname}/*.pyx'),
153            F('{pyname}/*.pxd'),
154            F('{pyname}/{Name}/*.pyx'),
155            F('{pyname}/{Name}/*.pxd'),
156            F('{pyname}/{Name}/*.pxi'),
157        ],
158        'workdir': 'src',
159    }
160    return [src]
161
162
163def extensions():
164    from os import walk
165    from glob import glob
166    from os.path import join
167
168    #
169    depends = []
170    glob_join = lambda *args: glob(join(*args))
171    for pth, _, _ in walk('src'):
172        depends += glob_join(pth, '*.h')
173        depends += glob_join(pth, '*.c')
174    for pkg in map(str.lower, reversed(PLIST)):
175        if (pkg.upper() + '_DIR') in os.environ:
176            pd = os.environ[pkg.upper() + '_DIR']
177            pa = os.environ.get('PETSC_ARCH', '')
178            depends += glob_join(pd, 'include', '*.h')
179            depends += glob_join(pd, 'include', pkg, 'private', '*.h')
180            depends += glob_join(pd, pa, 'include', '%sconf.h' % pkg)
181    #
182    include_dirs = []
183    numpy_include = os.environ.get('NUMPY_INCLUDE')
184    if numpy_include is not None:
185        numpy_includes = [numpy_include]
186    else:
187        try:
188            import numpy
189
190            numpy_includes = [numpy.get_include()]
191        except ImportError:
192            numpy_includes = []
193    include_dirs.extend(numpy_includes)
194    if F('{pyname}') != 'petsc4py':
195        try:
196            import petsc4py
197
198            petsc4py_includes = [petsc4py.get_include()]
199        except ImportError:
200            petsc4py_includes = []
201        include_dirs.extend(petsc4py_includes)
202    #
203    ext = {
204        'name': F('{pyname}.lib.{Name}'),
205        'sources': [F('src/{pyname}/{Name}.c')],
206        'depends': depends,
207        'include_dirs': [
208            'src',
209            F('src/{pyname}/include'),
210        ]
211        + include_dirs,
212        'define_macros': [
213            ('MPICH_SKIP_MPICXX', 1),
214            ('OMPI_SKIP_MPICXX', 1),
215            ('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION'),
216        ],
217    }
218    return [ext]
219
220
221# --------------------------------------------------------------------
222# Setup
223# --------------------------------------------------------------------
224
225
226def get_release():
227    suffix = os.path.join('src', 'binding', F('{pyname}'))
228    if not topdir.endswith(os.path.join(os.path.sep, suffix)):
229        return True
230    release = 1
231    rootdir = os.path.abspath(os.path.join(topdir, *[os.path.pardir] * 3))
232    version_h = os.path.join(rootdir, 'include', F('{name}version.h'))
233    release_macro = '%s_VERSION_RELEASE' % F('{name}').upper()
234    version_re = re.compile(r'#define\s+%s\s+([-]*\d+)' % release_macro)
235    if os.path.exists(version_h) and os.path.isfile(version_h):
236        with open(version_h, 'r') as f:
237            release = int(version_re.search(f.read()).groups()[0])
238    return bool(release)
239
240
241def requires(pkgname, major, minor, release=True):
242    minor = minor + int(not release)
243    devel = '' if release else '.dev0'
244    vmin = f'{major}.{minor}{devel}'
245    vmax = f'{major}.{minor+1}'
246    return f'{pkgname}>={vmin},<{vmax}'
247
248
249def run_setup():
250    is_sdist = 'sdist' in sys.argv
251    setup_args = metadata.copy()
252    vstr = setup_args['version'].split('.')[:2]
253    x, y = tuple(map(int, vstr))
254    release = get_release()
255    if not release:
256        setup_args['version'] = '%d.%d.0.dev0' % (x, y + 1)
257    if setuptools:
258        warnings.filterwarnings(
259            'ignore', message=r'.*fetch_build_eggs', module='setuptools'
260        )
261        setup_args['zip_safe'] = False
262        numpy_pin = 'numpy'
263        if not is_sdist:
264            try:
265                import numpy
266
267                major = int(numpy.__version__.partition('.')[0])
268                numpy_pin = 'numpy>=1.19' if major >= 2 else 'numpy<2'
269            except ImportError:
270                pass
271        setup_args['setup_requires'] = ['numpy']
272        setup_args['install_requires'] = [numpy_pin]
273        for pkg in map(str.lower, PLIST):
274            PKG_DIR = os.environ.get(pkg.upper() + '_DIR')
275            if not (PKG_DIR and os.path.isdir(PKG_DIR)):
276                package = requires(pkg, x, y, release)
277                setup_args['setup_requires'] += [package]
278                setup_args['install_requires'] += [package]
279        if F('{pyname}') != 'petsc4py':
280            package = requires('petsc4py', x, y, release)
281            setup_args['setup_requires'] += [package]
282            setup_args['install_requires'] += [package]
283        setup_args.update(metadata_extra)
284    #
285    conf = __import__(F('conf{name}'))
286    cython_sources = [src for src in sources()]  # noqa: C416
287    ext_modules = [conf.Extension(**ext) for ext in extensions()]
288    #
289    sabi = get_build_pysabi()
290    if sabi and setuptools:
291        api_tag = "cp{}{}".format(*sabi)
292        options = {"bdist_wheel": {"py_limited_api": api_tag}}
293        setup_args["options"] = options
294        api_ver = "0x{:02X}{:02X}0000".format(*sabi)
295        defines = [("Py_LIMITED_API", api_ver)]
296        for ext in ext_modules:
297            ext.define_macros.extend(defines)
298            ext.py_limited_api = True
299    #
300    conf.setup(
301        packages=[
302            F('{pyname}'),
303            F('{pyname}.lib'),
304            F('{pyname}.lib._pytypes'),
305            F('{pyname}.lib._pytypes.viewer'),
306        ],
307        package_dir={'': 'src'},
308        package_data={
309            F('{pyname}'): [
310                F('{Name}.pxd'),
311                F('{Name}*.h'),
312                F('include/{pyname}/*.h'),
313                F('include/{pyname}/*.i'),
314                'py.typed',
315                '*.pyi',
316                '*/*.pyi',
317            ],
318            F('{pyname}.lib'): [
319                F('{name}.cfg'),
320            ],
321        },
322        cython_sources=cython_sources,
323        ext_modules=ext_modules,
324        **setup_args,
325    )
326
327
328# --------------------------------------------------------------------
329
330
331def main():
332    run_setup()
333
334
335if __name__ == '__main__':
336    main()
337
338# --------------------------------------------------------------------
339