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