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