1#!/usr/bin/env python 2 3# Bootstrap setuptools installation. We require setuptools >= 3.2 because of a 4# bug in earlier versions regarding C++ sources generated with Cython. See: 5# https://pypi.python.org/pypi/setuptools/3.6#id171 6try: 7 import pkg_resources 8 9 pkg_resources.require("setuptools >= 3.2") 10except pkg_resources.ResolutionError: 11 from ez_setup import use_setuptools 12 13 use_setuptools() 14 15import os 16import errno 17import fnmatch 18import sys 19import shlex 20from distutils.sysconfig import get_config_var, get_config_vars 21from subprocess import check_output, CalledProcessError, check_call 22from setuptools import setup 23from setuptools.dist import Distribution 24from distutils.command.build_clib import build_clib 25from distutils.errors import DistutilsExecError 26from distutils.dir_util import mkpath 27from distutils.file_util import copy_file 28from distutils import log 29 30# Apple switched default C++ standard libraries (from gcc's libstdc++ to 31# clang's libc++), but some pre-packaged Python environments such as Anaconda 32# are built against the old C++ standard library. Luckily, we don't have to 33# actually detect which C++ standard library was used to build the Python 34# interpreter. We just have to propagate MACOSX_DEPLOYMENT_TARGET from the 35# configuration variables to the environment. 36# 37# This workaround fixes <https://github.com/healpy/healpy/issues/151>. 38if ( 39 get_config_var("MACOSX_DEPLOYMENT_TARGET") 40 and not "MACOSX_DEPLOYMENT_TARGET" in os.environ 41): 42 os.environ["MACOSX_DEPLOYMENT_TARGET"] = get_config_var("MACOSX_DEPLOYMENT_TARGET") 43 44 45# If the Cython-generated C++ files are absent, then fetch and install Cython 46# as an egg. If the Cython-generated files are present, then only use Cython if 47# a sufficiently new version of Cython is already present on the system. 48cython_require = "Cython >= 0.16" 49log.info("checking if Cython-generated files have been built") 50try: 51 open("healpy/src/_query_disc.cpp") 52except IOError: 53 log.info("Cython-generated files are absent; installing Cython locally") 54 Distribution().fetch_build_eggs(cython_require) 55else: 56 log.info("Cython-generated files are present") 57try: 58 log.info("Checking for %s", cython_require) 59 pkg_resources.require(cython_require) 60except pkg_resources.ResolutionError: 61 log.info("%s is not installed; not using Cython") 62 from setuptools.command.build_ext import build_ext 63 from setuptools import Extension 64else: 65 log.info("%s is installed; using Cython") 66 from Cython.Distutils import build_ext, Extension 67 68 69class build_external_clib(build_clib): 70 """Subclass of Distutils' standard build_clib subcommand. Adds support for 71 libraries that are installed externally and detected with pkg-config, with 72 an optional fallback to build from a local configure-make-install style 73 distribution.""" 74 75 def __init__(self, dist): 76 build_clib.__init__(self, dist) 77 self.build_args = {} 78 79 def autotools_path(self): 80 """ 81 Install Autotools locally if we are building on Read The Docs, because 82 we will be building from git and need to generate the healpix_cxx build 83 system. 84 """ 85 on_rtd = os.environ.get("READTHEDOCS", None) == "True" 86 if not on_rtd: 87 return None 88 89 log.info("checking if autotools is installed") 90 try: 91 check_output(["autoreconf", "--version"]) 92 except OSError as e: 93 if e.errno != errno.ENOENT: 94 raise 95 log.info("autotools is not installed") 96 else: 97 log.info("autotools is already installed") 98 return None 99 100 urls = [ 101 "https://ftp.gnu.org/gnu/m4/m4-1.4.17.tar.gz", 102 "https://ftp.gnu.org/gnu/libtool/libtool-2.4.6.tar.gz", 103 "https://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz", 104 "https://ftp.gnu.org/gnu/automake/automake-1.15.tar.gz", 105 "https://pkg-config.freedesktop.org/releases/pkg-config-0.29.1.tar.gz", 106 ] 107 108 # Use a subdirectory of build_temp as the build directory. 109 build_temp = os.path.realpath(self.build_temp) 110 prefix = os.path.join(build_temp, "autotools") 111 mkpath(prefix) 112 113 env = dict(os.environ) 114 path = os.path.join(prefix, "bin") 115 try: 116 path += ":" + env["PATH"] 117 except KeyError: 118 pass 119 env["PATH"] = path 120 121 # Check again if autotools is available, now that we have added the 122 # temporary path. 123 try: 124 check_output(["autoreconf", "--version"], env=env) 125 except OSError as e: 126 if e.errno != errno.ENOENT: 127 raise 128 log.info("building autotools from source") 129 else: 130 log.info("using autotools built from source") 131 return path 132 133 # Otherwise, build from source. 134 for url in urls: 135 _, _, tarball = url.rpartition("/") 136 pkg_version = tarball.replace(".tar.gz", "") 137 log.info("downloading %s", url) 138 check_call(["curl", "-O", url], cwd=build_temp) 139 log.info("extracting %s", tarball) 140 check_call(["tar", "-xzf", tarball], cwd=build_temp) 141 cwd = os.path.join(build_temp, pkg_version) 142 log.info("configuring %s", pkg_version) 143 check_call( 144 ["./configure", "--prefix", prefix, "--with-internal-glib"], 145 env=env, 146 cwd=cwd, 147 ) 148 log.info("making %s", pkg_version) 149 check_call(["make", "install"], env=env, cwd=cwd) 150 return path 151 152 def env(self): 153 """Construct an environment dictionary suitable for having pkg-config 154 pick up .pc files in the build_clib directory.""" 155 # Test if pkg-config is present. If not, fall back to pykg-config. 156 try: 157 env = self._env 158 except AttributeError: 159 env = dict(os.environ) 160 161 path = self.autotools_path() 162 if path is not None: 163 env["PATH"] = path 164 165 try: 166 check_output(["pkg-config", "--version"]) 167 except OSError as e: 168 if e.errno != errno.ENOENT: 169 raise 170 log.warn("pkg-config is not installed, falling back to pykg-config") 171 env["PKG_CONFIG"] = ( 172 sys.executable + " " + os.path.abspath("run_pykg_config.py") 173 ) 174 else: 175 env["PKG_CONFIG"] = "pkg-config" 176 177 build_clib = os.path.realpath(self.build_clib) 178 pkg_config_path = ( 179 os.path.join(build_clib, "lib64", "pkgconfig") 180 + ":" 181 + os.path.join(build_clib, "lib", "pkgconfig") 182 ) 183 try: 184 pkg_config_path += ":" + env["PKG_CONFIG_PATH"] 185 except KeyError: 186 pass 187 env["PKG_CONFIG_PATH"] = pkg_config_path 188 189 self._env = env 190 return env 191 192 def pkgconfig(self, *packages): 193 env = self.env() 194 PKG_CONFIG = tuple(shlex.split(env["PKG_CONFIG"], posix=(os.sep == "/"))) 195 kw = {} 196 index_key_flag = ( 197 (2, "--cflags-only-I", ("include_dirs",)), 198 (0, "--cflags-only-other", ("extra_compile_args", "extra_link_args")), 199 (2, "--libs-only-L", ("library_dirs", "runtime_library_dirs")), 200 (2, "--libs-only-l", ("libraries",)), 201 (0, "--libs-only-other", ("extra_link_args",)), 202 ) 203 for index, flag, keys in index_key_flag: 204 cmd = PKG_CONFIG + (flag,) + tuple(packages) 205 log.debug("%s", " ".join(cmd)) 206 args = [ 207 token[index:].decode() for token in check_output(cmd, env=env).split() 208 ] 209 if args: 210 for key in keys: 211 kw.setdefault(key, []).extend(args) 212 return kw 213 214 def finalize_options(self): 215 """Run 'autoreconf -i' for any bundled libraries to generate the 216 configure script.""" 217 build_clib.finalize_options(self) 218 env = self.env() 219 220 for lib_name, build_info in self.libraries: 221 if "sources" not in build_info: 222 log.info( 223 "checking if configure script for library '%s' exists", lib_name 224 ) 225 if not os.path.exists( 226 os.path.join(build_info["local_source"], "configure") 227 ): 228 log.info("running 'autoreconf -i' for library '%s'", lib_name) 229 check_call( 230 ["autoreconf", "-i"], cwd=build_info["local_source"], env=env 231 ) 232 233 def build_library( 234 self, 235 library, 236 pkg_config_name, 237 local_source=None, 238 supports_non_srcdir_builds=True, 239 ): 240 log.info("checking if library '%s' is installed", library) 241 try: 242 build_args = self.pkgconfig(pkg_config_name) 243 log.info("found '%s' installed, using it", library) 244 except CalledProcessError: 245 246 # If local_source is not specified, then immediately fail. 247 if local_source is None: 248 raise DistutilsExecError("library '%s' is not installed", library) 249 250 log.info("building library '%s' from source", library) 251 252 env = self.env() 253 254 # Determine which compilers we are to use, and what flags. 255 # This is based on what distutils.sysconfig.customize_compiler() 256 # does, but that function has a problem that it doesn't produce 257 # necessary (e.g. architecture) flags for C++ compilers. 258 cc, cxx, opt, cflags = get_config_vars("CC", "CXX", "OPT", "CFLAGS") 259 cxxflags = cflags 260 261 if "CC" in env: 262 cc = env["CC"] 263 if "CXX" in env: 264 cxx = env["CXX"] 265 if "CFLAGS" in env: 266 cflags = opt + " " + env["CFLAGS"] 267 if "CXXFLAGS" in env: 268 cxxflags = opt + " " + env["CXXFLAGS"] 269 270 # Use a subdirectory of build_temp as the build directory. 271 build_temp = os.path.realpath(os.path.join(self.build_temp, library)) 272 273 # Destination for headers and libraries is build_clib. 274 build_clib = os.path.realpath(self.build_clib) 275 276 # Create build directories if they do not yet exist. 277 mkpath(build_temp) 278 mkpath(build_clib) 279 280 if not supports_non_srcdir_builds: 281 self._stage_files_recursive(local_source, build_temp) 282 283 # Run configure. 284 cmd = [ 285 "/bin/sh", 286 os.path.join(os.path.realpath(local_source), "configure"), 287 "--prefix=" + build_clib, 288 "--disable-shared", 289 "--with-pic", 290 "--disable-maintainer-mode", 291 ] 292 293 log.info("%s", " ".join(cmd)) 294 check_call( 295 cmd, 296 cwd=build_temp, 297 env=dict(env, CC=cc, CXX=cxx, CFLAGS=cflags, CXXFLAGS=cxxflags), 298 ) 299 300 # Run make install. 301 cmd = ["make", "install"] 302 log.info("%s", " ".join(cmd)) 303 check_call(cmd, cwd=build_temp, env=env) 304 305 build_args = self.pkgconfig(pkg_config_name) 306 307 return build_args 308 # Done! 309 310 @staticmethod 311 def _list_files_recursive(path, skip=(".*", "*.o", "autom4te.cache")): 312 """Yield paths to all of the files contained within the given path, 313 following symlinks. If skip is a tuple of fnmatch()-style wildcard 314 strings, skip any directory or filename matching any of the patterns in 315 skip.""" 316 for dirpath, dirnames, filenames in os.walk(path, followlinks=True): 317 if not any( 318 any(fnmatch.fnmatch(p, s) for s in skip) for p in dirpath.split(os.sep) 319 ): 320 for filename in filenames: 321 if not any(fnmatch.fnmatch(filename, s) for s in skip): 322 yield os.path.join(dirpath, filename) 323 324 @staticmethod 325 def _stage_files_recursive(src, dest, skip=None): 326 """Hard link or copy all of the files in the path src into the path dest. 327 Subdirectories are created as needed, and files in dest are overwritten.""" 328 # Use hard links if they are supported on this system. 329 if hasattr(os, "link"): 330 link = "hard" 331 elif hasattr(os, "symlink"): 332 link = "sym" 333 else: 334 link = None 335 336 for dirpath, dirnames, filenames in os.walk(src, followlinks=True): 337 if not any(p.startswith(".") for p in dirpath.split(os.sep)): 338 dest_dirpath = os.path.join( 339 dest, dirpath.split(src, 1)[1].lstrip(os.sep) 340 ) 341 mkpath(dest_dirpath) 342 for filename in filenames: 343 if not filename.startswith("."): 344 src_path = os.path.join(dirpath, filename) 345 dest_path = os.path.join(dest_dirpath, filename) 346 if not os.path.exists(dest_path): 347 copy_file( 348 os.path.join(dirpath, filename), 349 os.path.join(dest_dirpath, filename), 350 ) 351 352 def get_source_files(self): 353 """Copied from Distutils' own build_clib, but modified so that it is not 354 an error for a build_info dictionary to lack a 'sources' key. If there 355 is no 'sources' key, then all files contained within the path given by 356 the 'local_sources' value are returned.""" 357 self.check_library_list(self.libraries) 358 filenames = [] 359 for (lib_name, build_info) in self.libraries: 360 sources = build_info.get("sources") 361 if sources is None or not isinstance(sources, (list, tuple)): 362 sources = list(self._list_files_recursive(build_info["local_source"])) 363 364 filenames.extend(sources) 365 return filenames 366 367 def build_libraries(self, libraries): 368 # Build libraries that have no 'sources' key, accumulating the output 369 # from pkg-config. 370 for lib_name, build_info in libraries: 371 if "sources" not in build_info: 372 for key, value in self.build_library(lib_name, **build_info).items(): 373 if key in self.build_args: 374 self.build_args[key].extend(value) 375 else: 376 self.build_args[key] = value 377 378 # Use parent method to build libraries that have a 'sources' key. 379 build_clib.build_libraries( 380 self, 381 ( 382 (lib_name, build_info) 383 for lib_name, build_info in libraries 384 if "sources" in build_info 385 ), 386 ) 387 388 389class custom_build_ext(build_ext): 390 391 def finalize_options(self): 392 build_ext.finalize_options(self) 393 394 # Make sure that Numpy is importable 395 # (does the same thing as setup_requires=['numpy']) 396 self.distribution.fetch_build_eggs("numpy") 397 # Prevent numpy from thinking it is still in its setup process: 398 # See http://stackoverflow.com/questions/19919905 399 from six.moves import builtins 400 401 builtins.__NUMPY_SETUP__ = False 402 403 # Add Numpy header search path path 404 import numpy 405 406 self.include_dirs.append(numpy.get_include()) 407 408 def run(self): 409 # If we were asked to build any C/C++ libraries, add the directory 410 # where we built them to the include path. (It's already on the library 411 # path.) 412 if self.distribution.has_c_libraries(): 413 self.run_command("build_clib") 414 build_clib = self.get_finalized_command("build_clib") 415 for key, value in build_clib.build_args.items(): 416 for ext in self.extensions: 417 if not hasattr(ext, key) or getattr(ext, key) is None: 418 setattr(ext, key, value) 419 else: 420 getattr(ext, key).extend(value) 421 build_ext.run(self) 422 423 424exec(open("healpy/version.py").read()) 425 426def readme(): 427 with open('README.rst') as f: 428 return f.read() 429 430 431setup( 432 name="healpy", 433 version=__version__, 434 description="Healpix tools package for Python", 435 long_description=readme(), 436 classifiers=[ 437 "Development Status :: 5 - Production/Stable", 438 "Environment :: Console", 439 "Intended Audience :: Science/Research", 440 "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", 441 "Operating System :: POSIX", 442 "Programming Language :: C++", 443 "Programming Language :: Python :: 2.7", 444 "Programming Language :: Python :: 3.4", 445 "Programming Language :: Python :: 3.5", 446 "Programming Language :: Python :: 3.6", 447 "Programming Language :: Python :: 3.7", 448 "Topic :: Scientific/Engineering :: Astronomy", 449 "Topic :: Scientific/Engineering :: Visualization", 450 ], 451 author="C. Rosset, A. Zonca", 452 author_email="cyrille.rosset@apc.univ-paris-diderot.fr", 453 url="http://github.com/healpy", 454 packages=["healpy", "healpy.test"], 455 libraries=[ 456 ( 457 "cfitsio", 458 { 459 "pkg_config_name": "cfitsio", 460 "local_source": "cfitsio", 461 "supports_non_srcdir_builds": False, 462 }, 463 ), 464 ( 465 "healpix_cxx", 466 { 467 "pkg_config_name": "healpix_cxx >= 3.40.0", 468 "local_source": "healpixsubmodule/src/cxx/autotools", 469 }, 470 ), 471 ], 472 py_modules=[ 473 "healpy.pixelfunc", 474 "healpy.sphtfunc", 475 "healpy.visufunc", 476 "healpy.fitsfunc", 477 "healpy.projector", 478 "healpy.rotator", 479 "healpy.projaxes", 480 "healpy.version", 481 ], 482 cmdclass={"build_ext": custom_build_ext, "build_clib": build_external_clib}, 483 ext_modules=[ 484 Extension( 485 "healpy._healpy_pixel_lib", 486 sources=["healpy/src/_healpy_pixel_lib.cc"], 487 language="c++", 488 ), 489 Extension( 490 "healpy._healpy_sph_transform_lib", 491 sources=["healpy/src/_healpy_sph_transform_lib.cc"], 492 language="c++", 493 ), 494 Extension( 495 "healpy._query_disc", 496 ["healpy/src/_query_disc.pyx"], 497 language="c++", 498 cython_directives=dict(embedsignature=True), 499 ), 500 Extension( 501 "healpy._sphtools", 502 ["healpy/src/_sphtools.pyx"], 503 language="c++", 504 cython_directives=dict(embedsignature=True), 505 ), 506 Extension( 507 "healpy._pixelfunc", 508 ["healpy/src/_pixelfunc.pyx"], 509 language="c++", 510 cython_directives=dict(embedsignature=True), 511 ), 512 ], 513 package_data={ 514 "healpy": [ 515 "data/*.fits", 516 "data/totcls.dat", 517 "test/data/*.fits", 518 "test/data/*.fits.gz", 519 "test/data/*.sh", 520 ] 521 }, 522 install_requires=["matplotlib", "numpy>=1.13", "six", "astropy", "scipy"], 523 setup_requires=["pytest-runner", "six"], 524 tests_require=["pytest", "pytest-cython"], 525 test_suite="healpy", 526 license="GPLv2", 527) 528