xref: /petsc/src/binding/petsc4py/setup.py (revision fb83dddadea1072c893d178467655cbacad931ac)
1#!/usr/bin/env python
2# Author:  Lisandro Dalcin
3# Contact: dalcinl@gmail.com
4
5"""
6PETSc for Python
7"""
8
9import re
10import os
11import sys
12
13try:
14    import setuptools
15except ImportError:
16    setuptools = None
17
18pyver = sys.version_info[:2]
19if pyver < (2, 6) or (3, 0) <= pyver < (3, 2):
20    raise RuntimeError("Python version 2.6, 2.7 or >= 3.2 required")
21if pyver == (2, 6) or pyver == (3, 2):
22    sys.stderr.write(
23        "WARNING: Python %d.%d is not supported.\n" % pyver)
24
25# python-3.11+ requires cython 0.29.32+
26if pyver >= (3, 11):
27  CYTHON = '0.29.32'
28else:
29  CYTHON = '0.24'
30
31# --------------------------------------------------------------------
32# Metadata
33# --------------------------------------------------------------------
34
35topdir = os.path.abspath(os.path.dirname(__file__))
36sys.path.insert(0, topdir)
37
38def name():
39    return 'petsc4py'
40
41def version():
42    with open(os.path.join(topdir, 'src', '__init__.py')) as f:
43        m = re.search(r"__version__\s*=\s*'(.*)'", f.read())
44        return m.groups()[0]
45
46def description():
47    return __doc__.strip()
48
49def long_description():
50    with open(os.path.join(topdir, 'DESCRIPTION.rst')) as f:
51        return f.read()
52
53name     = name()
54version  = version()
55
56url      = 'https://gitlab.com/petsc/petsc'
57pypiroot = 'https://pypi.io/packages/source/%s/%s/' % (name[0], name)
58download = pypiroot + '%(name)s-%(version)s.tar.gz' % vars()
59
60classifiers = """
61License :: OSI Approved :: BSD License
62Operating System :: POSIX
63Intended Audience :: Developers
64Intended Audience :: Science/Research
65Programming Language :: C
66Programming Language :: C++
67Programming Language :: Cython
68Programming Language :: Python
69Programming Language :: Python :: 2
70Programming Language :: Python :: 3
71Programming Language :: Python :: Implementation :: CPython
72Topic :: Scientific/Engineering
73Topic :: Software Development :: Libraries :: Python Modules
74Development Status :: 5 - Production/Stable
75"""
76
77keywords = """
78scientific computing
79parallel computing
80PETSc
81MPI
82"""
83
84platforms = """
85POSIX
86Linux
87macOS
88FreeBSD
89"""
90
91metadata = {
92    'name'             : name,
93    'version'          : version,
94    'description'      : description(),
95    'long_description' : long_description(),
96    'url'              : url,
97    'download_url'     : download,
98    'classifiers'      : classifiers.strip().split('\n'),
99    'keywords'         : keywords.strip().split('\n'),
100    'license'          : 'BSD-2-Clause',
101    'platforms'        : platforms.split('\n'),
102    'author'           : 'Lisandro Dalcin',
103    'author_email'     : 'dalcinl@gmail.com',
104    'maintainer'       : 'PETSc Team',
105    'maintainer_email' : 'petsc-maint@mcs.anl.gov',
106}
107metadata.update({
108    'requires': ['numpy'],
109})
110
111metadata_extra = {
112    'long_description_content_type': 'text/rst',
113}
114
115# --------------------------------------------------------------------
116# Extension modules
117# --------------------------------------------------------------------
118
119def extensions():
120    from os import walk
121    from glob import glob
122    from os.path import join
123    glob_join = lambda *args: glob(join(*args))
124    depends = []
125    for pth, dirs, files in walk('src'):
126        depends += glob_join(pth, '*.h')
127        depends += glob_join(pth, '*.c')
128    if 'PETSC_DIR' in os.environ:
129        pd = os.environ['PETSC_DIR']
130        pa = os.environ.get('PETSC_ARCH', '')
131        depends += glob_join(pd, 'include', '*.h')
132        depends += glob_join(pd, 'include', 'petsc', 'private', '*.h')
133        depends += glob_join(pd, pa, 'include', 'petscconf.h')
134    numpy_include = os.environ.get('NUMPY_INCLUDE')
135    if numpy_include is not None:
136        numpy_includes = [numpy_include]
137    else:
138        try:
139            import numpy
140            numpy_includes = [numpy.get_include()]
141        except ImportError:
142            numpy_includes = []
143    PETSc = dict(
144        name='petsc4py.lib.PETSc',
145        sources=['src/PETSc.c'],
146        depends=depends,
147        include_dirs=[
148            'src/include',
149        ] + numpy_includes,
150        define_macros=[
151            ('MPICH_SKIP_MPICXX', 1),
152            ('OMPI_SKIP_MPICXX', 1),
153        ],
154    )
155    return [PETSc]
156
157# --------------------------------------------------------------------
158# Setup
159# --------------------------------------------------------------------
160
161from conf.petscconf import setup, Extension
162from conf.petscconf import config, build, build_src, build_ext, install
163from conf.petscconf import clean, sdist
164
165def get_release():
166    release = 1
167    if topdir.endswith(os.path.join(os.path.sep, 'src', 'binding', name)):
168        topname = name.replace('4py', '')
169        rootdir = os.path.abspath(os.path.join(topdir, *[os.path.pardir]*3))
170        version_h = os.path.join(rootdir, 'include', '%sversion.h' % topname)
171        release_macro = '%s_VERSION_RELEASE' % topname.upper()
172        version_re = re.compile(r"#define\s+%s\s+([-]*\d+)" % release_macro)
173        if os.path.exists(version_h) and os.path.isfile(version_h):
174            with open(version_h, 'r') as f:
175                release = int(version_re.search(f.read()).groups()[0])
176    return bool(release)
177
178def requires(pkgname, major, minor, release=True):
179    minor = minor + int(not release)
180    devel = '' if release else '.dev0'
181    vmin = "%s.%s%s" % (major, minor, devel)
182    vmax = "%s.%s" % (major, minor + 1)
183    return "%s>=%s,<%s" % (pkgname, vmin, vmax)
184
185def run_setup():
186    setup_args = metadata.copy()
187    vstr = setup_args['version'].split('.')[:2]
188    x, y = tuple(map(int, vstr))
189    release = get_release()
190    if not release:
191        setup_args['version'] = "%d.%d.0.dev0" %(x, y+1)
192    if setuptools:
193        setup_args['zip_safe'] = False
194        setup_args['install_requires'] = ['numpy']
195        PETSC_DIR = os.environ.get('PETSC_DIR')
196        if not (PETSC_DIR and os.path.isdir(PETSC_DIR)):
197            petsc = requires('petsc', x, y, release)
198            setup_args['install_requires'] += [petsc]
199        setup_args.update(metadata_extra)
200    if setuptools:
201        src = os.path.join('src', 'petsc4py.PETSc.c')
202        has_src = os.path.exists(os.path.join(topdir, src))
203        has_git = os.path.isdir(os.path.join(topdir, '.git'))
204        has_hg  = os.path.isdir(os.path.join(topdir, '.hg'))
205        suffix = os.path.join('src', 'binding', 'petsc4py')
206        in_petsc = topdir.endswith(os.path.sep + suffix)
207        if not has_src or has_git or has_hg or in_petsc:
208            setup_args['setup_requires'] = ['Cython>='+CYTHON]
209    #
210    setup(
211        packages=[
212            'petsc4py',
213            'petsc4py.lib',
214        ],
215        package_dir={
216            'petsc4py'     : 'src',
217            'petsc4py.lib' : 'src/lib',
218        },
219        package_data={
220            'petsc4py': [
221                'include/petsc4py/*.h',
222                'include/petsc4py/*.i',
223                'include/petsc4py/*.pxd',
224                'include/petsc4py/*.pxi',
225                'include/petsc4py/*.pyx',
226                'PETSc.pxd',
227            ],
228            'petsc4py.lib': [
229                'petsc.cfg',
230            ],
231        },
232        ext_modules=[Extension(**ext) for ext in extensions()],
233        cmdclass={
234            'config':     config,
235            'build':      build,
236            'build_src':  build_src,
237            'build_ext':  build_ext,
238            'install':    install,
239            'clean':      clean,
240            'sdist':      sdist,
241        },
242        **setup_args,
243    )
244
245# --------------------------------------------------------------------
246
247def cython_req():
248    return CYTHON
249
250def cython_chk(VERSION, verbose=True):
251    from conf.baseconf import log
252    from conf.baseconf import Version
253    from conf.baseconf import LegacyVersion
254    if verbose:
255        warn = lambda msg='': sys.stderr.write(msg+'\n')
256    else:
257        warn = lambda msg='': None
258    #
259    try:
260        import Cython
261    except ImportError:
262        warn("*"*80)
263        warn()
264        warn(" You need Cython to generate C source files.\n")
265        warn("   $ python -m pip install cython")
266        warn()
267        warn("*"*80)
268        return False
269    #
270    REQUIRED = VERSION
271    CYTHON_VERSION = Cython.__version__
272    if VERSION is not None:
273        m = re.match(r"(\d+\.\d+(?:\.\d+)?).*", CYTHON_VERSION)
274        if m:
275            REQUIRED  = Version(VERSION)
276            AVAILABLE = Version(m.groups()[0])
277        else:
278            REQUIRED  = LegacyVersion(VERSION)
279            AVAILABLE = LegacyVersion(CYTHON_VERSION)
280        if AVAILABLE < REQUIRED:
281            warn("*"*80)
282            warn()
283            warn(" You need Cython >= {0} (you have version {1}).\n"
284                 .format(REQUIRED, CYTHON_VERSION))
285            warn("   $ python -m pip install --upgrade cython")
286            warn()
287            warn("*"*80)
288            return False
289    #
290    if verbose:
291        log.info("using Cython version %s" % CYTHON_VERSION)
292    return True
293
294def cython_run(
295    source, target=None,
296    depends=(), includes=(),
297    destdir_c=None, destdir_h=None,
298    workdir=None, force=False,
299    VERSION=None,
300):
301    from glob import glob
302    from conf.baseconf import log
303    from conf.baseconf import dep_util
304    from conf.baseconf import DistutilsError
305    if target is None:
306        target = os.path.splitext(source)[0]+'.c'
307    cwd = os.getcwd()
308    try:
309        if workdir:
310            os.chdir(workdir)
311        alldeps = [source]
312        for dep in depends:
313            alldeps += glob(dep)
314        if not (force or dep_util.newer_group(alldeps, target)):
315            log.debug("skipping '%s' -> '%s' (up-to-date)",
316                      source, target)
317            return
318    finally:
319        os.chdir(cwd)
320    #
321    require = 'Cython'
322    if VERSION is not None:
323        require += '>=%s' % VERSION
324    if not cython_chk(VERSION, verbose=False):
325        pkgname = re.compile(r'cython(\.|$)', re.IGNORECASE)
326        for modname in list(sys.modules.keys()):
327            if pkgname.match(modname):
328                del sys.modules[modname]
329        try:
330            import warnings
331            import setuptools
332            install_setup_requires = setuptools._install_setup_requires
333            with warnings.catch_warnings():
334                category = setuptools.SetuptoolsDeprecationWarning
335                warnings.simplefilter('ignore', category)
336                log.info("fetching build requirement %s" % require)
337                install_setup_requires(dict(setup_requires=[require]))
338        except Exception:
339            log.info("failed to fetch build requirement %s" % require)
340    if not cython_chk(VERSION):
341        raise DistutilsError("requires Cython>=%s" % VERSION)
342    #
343    log.info("cythonizing '%s' -> '%s'", source, target)
344    from conf.cythonize import cythonize
345    err = cythonize(
346        source, target,
347        includes=includes,
348        destdir_c=destdir_c,
349        destdir_h=destdir_h,
350        workdir=workdir,
351    )
352    if err:
353        raise DistutilsError(
354            "Cython failure: '%s' -> '%s'" % (source, target))
355
356def build_sources(cmd):
357    from os.path import exists, isdir, join
358    # petsc4py.PETSc
359    source = 'petsc4py.PETSc.pyx'
360    target = 'petsc4py.PETSc.c'
361    depends = [
362        'include/*/*.pxd',
363        'PETSc/*.pyx',
364        'PETSc/*.pxi',
365    ]
366    includes = ['include']
367    destdir_h = os.path.join('include', 'petsc4py')
368    cython_run(
369        source, target,
370        depends=depends,
371        includes=includes,
372        destdir_c=None,
373        destdir_h=destdir_h,
374        workdir='src',
375        force=cmd.force,
376        VERSION=cython_req(),
377    )
378
379build_src.run = build_sources
380
381# --------------------------------------------------------------------
382
383def main():
384    run_setup()
385
386if __name__ == '__main__':
387    main()
388
389# --------------------------------------------------------------------
390