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