diff --git a/buildutils/__init__.py b/buildutils/__init__.py new file mode 100644 index 0000000..1da698d --- /dev/null +++ b/buildutils/__init__.py @@ -0,0 +1,12 @@ +"""utilities for building pyzmq. + +Largely adapted from h5py +""" + +from .msg import * +from .config import * +from .detect import * +from .bundle import * +from .misc import * +from .patch import * +from .build import * diff --git a/buildutils/build.py b/buildutils/build.py new file mode 100644 index 0000000..bb345b7 --- /dev/null +++ b/buildutils/build.py @@ -0,0 +1,24 @@ +'Build the bundled capnp distribution' + +import subprocess +import os +import tempfile + +def build_libcapnp(bundle_dir, build_dir, verbose=False): + bundle_dir = os.path.abspath(bundle_dir) + capnp_dir = os.path.join(bundle_dir, 'capnproto-c++') + build_dir = os.path.abspath(build_dir) + + with tempfile.TemporaryFile() as f: + stdout = f + if verbose: + stdout = None + conf = subprocess.Popen(['./configure', '--disable-shared', '--prefix', build_dir], cwd=capnp_dir, stdout=stdout) + returncode = conf.wait() + if returncode != 0: + raise RuntimeError('Configure failed') + + make = subprocess.Popen(['make', '-j4', 'install'], cwd=capnp_dir, stdout=stdout) + returncode = make.wait() + if returncode != 0: + raise RuntimeError('Make failed') diff --git a/buildutils/bundle.py b/buildutils/bundle.py new file mode 100644 index 0000000..15e6dde --- /dev/null +++ b/buildutils/bundle.py @@ -0,0 +1,165 @@ +"""utilities for fetching build dependencies.""" + +#----------------------------------------------------------------------------- +# Copyright (C) PyZMQ Developers +# Distributed under the terms of the Modified BSD License. +# +# This bundling code is largely adapted from pyzmq-static's get.sh by +# Brandon Craig-Rhodes, which is itself BSD licensed. +#----------------------------------------------------------------------------- +# +# Adapted for use in pycapnp from pyzmq. See https://github.com/zeromq/pyzmq +# for original project. + + +import os +import shutil +import stat +import sys +import tarfile +from glob import glob +from subprocess import Popen, PIPE + +try: + # py2 + from urllib2 import urlopen +except ImportError: + # py3 + from urllib.request import urlopen + +from .msg import fatal, debug, info, warn + +pjoin = os.path.join + +#----------------------------------------------------------------------------- +# Constants +#----------------------------------------------------------------------------- + +bundled_version = (0,4,1) +libcapnp = "capnproto-c++-%i.%i.%i.tar.gz" % (bundled_version) +libcapnp_url = "https://capnproto.org/" + libcapnp + +HERE = os.path.dirname(__file__) +ROOT = os.path.dirname(HERE) + +#----------------------------------------------------------------------------- +# Utilities +#----------------------------------------------------------------------------- + + +def untgz(archive): + return archive.replace('.tar.gz', '') + +def localpath(*args): + """construct an absolute path from a list relative to the root pycapnp directory""" + plist = [ROOT] + list(args) + return os.path.abspath(pjoin(*plist)) + +def fetch_archive(savedir, url, fname, force=False): + """download an archive to a specific location""" + dest = pjoin(savedir, fname) + if os.path.exists(dest) and not force: + info("already have %s" % fname) + return dest + info("fetching %s into %s" % (url, savedir)) + if not os.path.exists(savedir): + os.makedirs(savedir) + req = urlopen(url) + with open(dest, 'wb') as f: + f.write(req.read()) + return dest + +#----------------------------------------------------------------------------- +# libcapnp +#----------------------------------------------------------------------------- + +def fetch_libcapnp(savedir): + """download and extract libcapnp""" + dest = pjoin(savedir, 'capnproto-c++') + if os.path.exists(dest): + info("already have %s" % dest) + return + fname = fetch_archive(savedir, libcapnp_url, libcapnp) + tf = tarfile.open(fname) + with_version = pjoin(savedir, tf.firstmember.path) + tf.extractall(savedir) + tf.close() + # remove version suffix: + shutil.move(with_version, dest) + +def stage_platform_hpp(capnproot): + """stage platform.hpp into libcapnp sources + + Tries ./configure first (except on Windows), + then falls back on included platform.hpp previously generated. + """ + + platform_hpp = pjoin(capnproot, 'src', 'platform.hpp') + if os.path.exists(platform_hpp): + info("already have platform.hpp") + return + if os.name == 'nt': + # stage msvc platform header + platform_dir = pjoin(capnproot, 'builds', 'msvc') + else: + info("attempting ./configure to generate platform.hpp") + + p = Popen('./configure', cwd=capnproot, shell=True, + stdout=PIPE, stderr=PIPE, + ) + o,e = p.communicate() + if p.returncode: + warn("failed to configure libcapnp:\n%s" % e) + if sys.platform == 'darwin': + platform_dir = pjoin(HERE, 'include_darwin') + elif sys.platform.startswith('freebsd'): + platform_dir = pjoin(HERE, 'include_freebsd') + elif sys.platform.startswith('linux-armv'): + platform_dir = pjoin(HERE, 'include_linux-armv') + else: + platform_dir = pjoin(HERE, 'include_linux') + else: + return + + info("staging platform.hpp from: %s" % platform_dir) + shutil.copy(pjoin(platform_dir, 'platform.hpp'), platform_hpp) + + +def copy_and_patch_libcapnp(capnp, libcapnp): + """copy libcapnp into source dir, and patch it if necessary. + + This command is necessary prior to running a bdist on Linux or OS X. + """ + if sys.platform.startswith('win'): + return + # copy libcapnp into capnp for bdist + local = localpath('capnp',libcapnp) + if not capnp and not os.path.exists(local): + fatal("Please specify capnp prefix via `setup.py configure --capnp=/path/to/capnp` " + "or copy libcapnp into capnp/ manually prior to running bdist.") + try: + # resolve real file through symlinks + lib = os.path.realpath(pjoin(capnp, 'lib', libcapnp)) + print ("copying %s -> %s"%(lib, local)) + shutil.copy(lib, local) + except Exception: + if not os.path.exists(local): + fatal("Could not copy libcapnp into capnp/, which is necessary for bdist. " + "Please specify capnp prefix via `setup.py configure --capnp=/path/to/capnp` " + "or copy libcapnp into capnp/ manually.") + + if sys.platform == 'darwin': + # chmod u+w on the lib, + # which can be user-read-only for some reason + mode = os.stat(local).st_mode + os.chmod(local, mode | stat.S_IWUSR) + # patch install_name on darwin, instead of using rpath + cmd = ['install_name_tool', '-id', '@loader_path/../%s'%libcapnp, local] + try: + p = Popen(cmd, stdout=PIPE,stderr=PIPE) + except OSError: + fatal("install_name_tool not found, cannot patch libcapnp for bundling.") + out,err = p.communicate() + if p.returncode: + fatal("Could not patch bundled libcapnp install_name: %s"%err, p.returncode) + diff --git a/buildutils/config.py b/buildutils/config.py new file mode 100644 index 0000000..c674655 --- /dev/null +++ b/buildutils/config.py @@ -0,0 +1,157 @@ +"""Config functions""" +#----------------------------------------------------------------------------- +# Copyright (C) PyZMQ Developers +# +# This file is part of pyzmq, copied and adapted from h5py. +# h5py source used under the New BSD license +# +# h5py: +# +# Distributed under the terms of the New BSD License. The full license is in +# the file COPYING.BSD, distributed as part of this software. +#----------------------------------------------------------------------------- + +import sys +import os +import json + +try: + from configparser import ConfigParser +except: + from ConfigParser import ConfigParser + +pjoin = os.path.join +from .msg import debug, fatal, warn + +#----------------------------------------------------------------------------- +# Utility functions (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- + + +def load_config(name, base='conf'): + """Load config dict from JSON""" + fname = pjoin(base, name + '.json') + if not os.path.exists(fname): + return {} + try: + with open(fname) as f: + cfg = json.load(f) + except Exception as e: + warn("Couldn't load %s: %s" % (fname, e)) + cfg = {} + return cfg + + +def save_config(name, data, base='conf'): + """Save config dict to JSON""" + if not os.path.exists(base): + os.mkdir(base) + fname = pjoin(base, name+'.json') + with open(fname, 'w') as f: + json.dump(data, f, indent=2) + + +def v_str(v_tuple): + """turn (2,0,1) into '2.0.1'.""" + return ".".join(str(x) for x in v_tuple) + +def get_eargs(): + """ Look for options in environment vars """ + + settings = {} + + zmq = os.environ.get("ZMQ_PREFIX", None) + if zmq is not None: + debug("Found environ var ZMQ_PREFIX=%s" % zmq) + settings['zmq_prefix'] = zmq + + return settings + +def cfg2dict(cfg): + """turn a ConfigParser into a nested dict + + because ConfigParser objects are dumb. + """ + d = {} + for section in cfg.sections(): + d[section] = dict(cfg.items(section)) + return d + +def get_cfg_args(): + """ Look for options in setup.cfg """ + + if not os.path.exists('setup.cfg'): + return {} + cfg = ConfigParser() + cfg.read('setup.cfg') + cfg = cfg2dict(cfg) + + g = cfg.setdefault('global', {}) + # boolean keys: + for key in ['libzmq_extension', + 'bundle_libzmq_dylib', + 'no_libzmq_extension', + 'have_sys_un_h', + 'skip_check_zmq', + ]: + if key in g: + g[key] = eval(g[key]) + + # globals go to top level + cfg.update(cfg.pop('global')) + return cfg + +def config_from_prefix(prefix): + """Get config from zmq prefix""" + settings = {} + if prefix.lower() in ('default', 'auto', ''): + settings['zmq_prefix'] = '' + settings['libzmq_extension'] = False + settings['no_libzmq_extension'] = False + elif prefix.lower() in ('bundled', 'extension'): + settings['zmq_prefix'] = '' + settings['libzmq_extension'] = True + settings['no_libzmq_extension'] = False + else: + settings['zmq_prefix'] = prefix + settings['libzmq_extension'] = False + settings['no_libzmq_extension'] = True + return settings + +def merge(into, d): + """merge two containers + + into is updated, d has priority + """ + if isinstance(into, dict): + for key in d.keys(): + if key not in into: + into[key] = d[key] + else: + into[key] = merge(into[key], d[key]) + return into + elif isinstance(into, list): + return into + d + else: + return d + +def discover_settings(conf_base=None): + """ Discover custom settings for ZMQ path""" + settings = { + 'zmq_prefix': '', + 'libzmq_extension': False, + 'no_libzmq_extension': False, + 'skip_check_zmq': False, + 'build_ext': {}, + 'bdist_egg': {}, + } + if sys.platform.startswith('win'): + settings['have_sys_un_h'] = False + + if conf_base: + # lowest priority + merge(settings, load_config('config', conf_base)) + merge(settings, get_cfg_args()) + merge(settings, get_eargs()) + + return settings diff --git a/buildutils/constants.py b/buildutils/constants.py new file mode 100644 index 0000000..e98c650 --- /dev/null +++ b/buildutils/constants.py @@ -0,0 +1,83 @@ +""" +script for generating files that involve repetitive updates for zmq constants. + +Run this after updating utils/constant_names + +Currently generates the following files from templates: + +- constant_enums.pxi +- constants.pxi +- zmq_constants.h + +""" + +# Copyright (C) PyZMQ Developers +# Distributed under the terms of the Modified BSD License. + +import os +import sys + +from . import info +pjoin = os.path.join + +root = os.path.abspath(pjoin(os.path.dirname(__file__), os.path.pardir)) + +sys.path.insert(0, pjoin(root, 'zmq', 'utils')) +from constant_names import all_names, no_prefix + +ifndef_t = """#ifndef {0} + #define {0} (_PYZMQ_UNDEFINED) +#endif +""" + +def cython_enums(): + """generate `enum: ZMQ_CONST` block for constant_enums.pxi""" + lines = [] + for name in all_names: + if no_prefix(name): + lines.append('enum: ZMQ_{0} "{0}"'.format(name)) + else: + lines.append('enum: ZMQ_{0}'.format(name)) + + return dict(ZMQ_ENUMS='\n '.join(lines)) + +def ifndefs(): + """generate `#ifndef ZMQ_CONST` block for zmq_constants.h""" + lines = ['#define _PYZMQ_UNDEFINED (-9999)'] + for name in all_names: + if not no_prefix(name): + name = 'ZMQ_%s' % name + lines.append(ifndef_t.format(name)) + return dict(ZMQ_IFNDEFS='\n'.join(lines)) + +def constants_pyx(): + """generate CONST = ZMQ_CONST and __all__ for constants.pxi""" + all_lines = [] + assign_lines = [] + for name in all_names: + if name == "NULL": + # avoid conflict with NULL in Cython + assign_lines.append("globals()['NULL'] = ZMQ_NULL") + else: + assign_lines.append('{0} = ZMQ_{0}'.format(name)) + all_lines.append(' "{0}",'.format(name)) + return dict(ASSIGNMENTS='\n'.join(assign_lines), ALL='\n'.join(all_lines)) + +def generate_file(fname, ns_func, dest_dir="."): + """generate a constants file from its template""" + with open(pjoin(root, 'buildutils', 'templates', '%s' % fname), 'r') as f: + tpl = f.read() + out = tpl.format(**ns_func()) + dest = pjoin(dest_dir, fname) + info("generating %s from template" % dest) + with open(dest, 'w') as f: + f.write(out) + +def render_constants(): + """render generated constant files from templates""" + generate_file("constant_enums.pxi", cython_enums, pjoin(root, 'zmq', 'backend', 'cython')) + generate_file("constants.pxi", constants_pyx, pjoin(root, 'zmq', 'backend', 'cython')) + generate_file("zmq_constants.h", ifndefs, pjoin(root, 'zmq', 'utils')) + +if __name__ == '__main__': + render_constants() diff --git a/buildutils/detect.py b/buildutils/detect.py new file mode 100644 index 0000000..7500abd --- /dev/null +++ b/buildutils/detect.py @@ -0,0 +1,166 @@ +"""Detect zmq version""" +#----------------------------------------------------------------------------- +# Copyright (C) PyZMQ Developers +# +# This file is part of pyzmq, copied and adapted from h5py. +# h5py source used under the New BSD license +# +# h5py: +# +# Distributed under the terms of the New BSD License. The full license is in +# the file COPYING.BSD, distributed as part of this software. +#----------------------------------------------------------------------------- +# +# Adapted for use in pycapnp from pyzmq. See https://github.com/zeromq/pyzmq +# for original project. + +import shutil +import sys +import os +import logging +import platform +from distutils import ccompiler +from distutils.ccompiler import get_default_compiler +from subprocess import Popen, PIPE +import tempfile + +from .misc import get_compiler, get_output_error +from .patch import patch_lib_paths + +pjoin = os.path.join + +#----------------------------------------------------------------------------- +# Utility functions (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- + +def test_compilation(cfile, compiler=None, **compiler_attrs): + """Test simple compilation with given settings""" + cc = get_compiler(compiler, **compiler_attrs) + + efile, ext = os.path.splitext(cfile) + + cpreargs = lpreargs = [] + if sys.platform == 'darwin': + # use appropriate arch for compiler + if platform.architecture()[0]=='32bit': + if platform.processor() == 'powerpc': + cpu = 'ppc' + else: + cpu = 'i386' + cpreargs = ['-arch', cpu] + lpreargs = ['-arch', cpu, '-undefined', 'dynamic_lookup'] + else: + # allow for missing UB arch, since it will still work: + lpreargs = ['-undefined', 'dynamic_lookup'] + if sys.platform == 'sunos5': + if platform.architecture()[0]=='32bit': + lpreargs = ['-m32'] + else: + lpreargs = ['-m64'] + extra = compiler_attrs.get('extra_compile_args', []) + extra += ['--std=c++11'] + + objs = cc.compile([cfile], extra_preargs=cpreargs, extra_postargs=extra) + cc.link_executable(objs, efile, extra_preargs=lpreargs) + return efile + +def compile_and_run(basedir, src, compiler=None, **compiler_attrs): + if not os.path.exists(basedir): + os.makedirs(basedir) + cfile = pjoin(basedir, os.path.basename(src)) + shutil.copy(src, cfile) + try: + cc = get_compiler(compiler, **compiler_attrs) + efile = test_compilation(cfile, compiler=cc) + patch_lib_paths(efile, cc.library_dirs) + result = Popen(efile, stdout=PIPE, stderr=PIPE) + so, se = result.communicate() + # for py3k: + so = so.decode() + se = se.decode() + finally: + shutil.rmtree(basedir) + + return result.returncode, so, se + + +def detect_version(basedir, compiler=None, **compiler_attrs): + """Compile, link & execute a test program, in empty directory `basedir`. + + The C compiler will be updated with any keywords given via setattr. + + Parameters + ---------- + + basedir : path + The location where the test program will be compiled and run + compiler : str + The distutils compiler key (e.g. 'unix', 'msvc', or 'mingw32') + **compiler_attrs : dict + Any extra compiler attributes, which will be set via ``setattr(cc)``. + + Returns + ------- + + A dict of properties for zmq compilation, with the following two keys: + + vers : tuple + The ZMQ version as a tuple of ints, e.g. (2,2,0) + settings : dict + The compiler options used to compile the test function, e.g. `include_dirs`, + `library_dirs`, `libs`, etc. + """ + if compiler is None: + compiler = get_default_compiler() + cfile = pjoin(basedir, 'vers.cpp') + shutil.copy(pjoin(os.path.dirname(__file__), 'vers.cpp'), cfile) + + # check if we need to link against Realtime Extensions library + if sys.platform.startswith('linux'): + cc = ccompiler.new_compiler(compiler=compiler) + cc.output_dir = basedir + if not cc.has_function('timer_create'): + compiler_attrs['libraries'].append('rt') + + cc = get_compiler(compiler=compiler, **compiler_attrs) + efile = test_compilation(cfile, compiler=cc) + patch_lib_paths(efile, cc.library_dirs) + + rc, so, se = get_output_error([efile]) + if rc: + msg = "Error running version detection script:\n%s\n%s" % (so,se) + logging.error(msg) + raise IOError(msg) + + handlers = {'vers': lambda val: tuple(int(v) for v in val.split('.'))} + + props = {} + for line in (x for x in so.split('\n') if x): + key, val = line.split(':') + props[key] = handlers[key](val) + + return props + + +def test_build(): + """do a test build of libcapnp""" + tmp_dir = tempfile.mkdtemp() + + # line() + # info("Configure: Autodetecting Cap'n Proto settings...") + # info(" Custom Cap'n Proto dir: %s" % prefix) + try: + detected = detect_version(tmp_dir) + finally: + erase_dir(tmp_dir) + + # info(" Cap'n Proto version detected: %s" % v_str(detected['vers'])) + + return detected + + +def erase_dir(dir): + try: + shutil.rmtree(dir) + except Exception: + pass diff --git a/buildutils/misc.py b/buildutils/misc.py new file mode 100644 index 0000000..72d01b7 --- /dev/null +++ b/buildutils/misc.py @@ -0,0 +1,65 @@ +"""misc build utility functions""" + +# Copyright (c) PyZMQ Developers +# Distributed under the terms of the Modified BSD License. + +import os +import sys +import logging +from distutils import ccompiler +from distutils.sysconfig import customize_compiler +from pipes import quote +from subprocess import Popen, PIPE + +pjoin = os.path.join + + +if sys.version_info[0] >= 3: + u = lambda x: x +else: + u = lambda x: x.decode('utf8', 'replace') + + +def customize_mingw(cc): + # strip -mno-cygwin from mingw32 (Python Issue #12641) + for cmd in [cc.compiler, cc.compiler_cxx, cc.compiler_so, cc.linker_exe, cc.linker_so]: + if '-mno-cygwin' in cmd: + cmd.remove('-mno-cygwin') + + # remove problematic msvcr90 + if 'msvcr90' in cc.dll_libraries: + cc.dll_libraries.remove('msvcr90') + + +def get_compiler(compiler, **compiler_attrs): + """get and customize a compiler""" + if compiler is None or isinstance(compiler, str): + cc = ccompiler.new_compiler(compiler=compiler) + # customize_compiler(cc) + if cc.compiler_type == 'mingw32': + customize_mingw(cc) + else: + cc = compiler + + for name, val in compiler_attrs.items(): + setattr(cc, name, val) + + return cc + + +def get_output_error(cmd): + """Return the exit status, stdout, stderr of a command""" + if not isinstance(cmd, list): + cmd = [cmd] + logging.debug("Running: %s", ' '.join(map(quote, cmd))) + try: + result = Popen(cmd, stdout=PIPE, stderr=PIPE) + except IOError as e: + return -1, u(''), u('Failed to run %r: %r' % (cmd, e)) + so, se = result.communicate() + # unicode: + so = so.decode('utf8', 'replace') + se = se.decode('utf8', 'replace') + + return result.returncode, so, se + diff --git a/buildutils/msg.py b/buildutils/msg.py new file mode 100644 index 0000000..70cd716 --- /dev/null +++ b/buildutils/msg.py @@ -0,0 +1,39 @@ +"""logging""" + +# Copyright (c) PyZMQ Developers. +# Distributed under the terms of the Modified BSD License. + +from __future__ import division + +import os +import sys +import logging + +#----------------------------------------------------------------------------- +# Logging (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- + + +logger = logging.getLogger() +if os.environ.get('DEBUG'): + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) +logger.addHandler(logging.StreamHandler(sys.stderr)) + +def debug(msg): + logger.debug(msg) + +def info(msg): + logger.info(msg) + +def fatal(msg, code=1): + logger.error("Fatal: " + msg) + exit(code) + +def warn(msg): + logger.error("Warning: " + msg) + +def line(c='*', width=48): + print(c * (width // len(c))) + diff --git a/buildutils/patch.py b/buildutils/patch.py new file mode 100644 index 0000000..925b67b --- /dev/null +++ b/buildutils/patch.py @@ -0,0 +1,61 @@ +"""utils for patching libraries""" + +# Copyright (c) PyZMQ Developers. +# Distributed under the terms of the Modified BSD License. + + +import re +import sys +import os +import logging + +from .misc import get_output_error + +pjoin = os.path.join + +# LIB_PAT from delocate +LIB_PAT = re.compile(r"\s*(.*) \(compatibility version (\d+\.\d+\.\d+), " + r"current version (\d+\.\d+\.\d+)\)") + +def _get_libs(fname): + rc, so, se = get_output_error(['otool', '-L', fname]) + if rc: + logging.error("otool -L %s failed: %r" % (fname, se)) + return + for line in so.splitlines()[1:]: + m = LIB_PAT.match(line) + if m: + yield m.group(1) + +def _find_library(lib, path): + """Find a library""" + for d in path[::-1]: + real_lib = os.path.join(d, lib) + if os.path.exists(real_lib): + return real_lib + +def _install_name_change(fname, lib, real_lib): + rc, so, se = get_output_error(['install_name_tool', '-change', lib, real_lib, fname]) + if rc: + logging.error("Couldn't update load path: %s", se) + +def patch_lib_paths(fname, library_dirs): + """Load any weakly-defined libraries from their real location + + (only on OS X) + + - Find libraries with `otool -L` + - Update with `install_name_tool -change` + """ + if sys.platform != 'darwin': + return + + libs = _get_libs(fname) + for lib in libs: + if not lib.startswith(('@', '/')): + real_lib = _find_library(lib, library_dirs) + if real_lib: + _install_name_change(fname, lib, real_lib) + + +__all__ = ['patch_lib_paths'] \ No newline at end of file diff --git a/buildutils/vers.cpp b/buildutils/vers.cpp new file mode 100644 index 0000000..00631a4 --- /dev/null +++ b/buildutils/vers.cpp @@ -0,0 +1,9 @@ +// check libcapnp version + +#include +#include "capnp/common.h" + +int main(int argc, char **argv){ + fprintf(stdout, "vers: %d.%d.%d\n", CAPNP_VERSION_MAJOR, CAPNP_VERSION_MINOR, CAPNP_VERSION_MICRO); + return 0; +} diff --git a/setup.py b/setup.py index 0320c47..44c981d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import print_function + try: from Cython.Build import cythonize import Cython @@ -15,12 +17,18 @@ if setuptools_version < '0.8': from distutils.core import setup import os +from buildutils import test_build, fetch_libcapnp, build_libcapnp, info +from distutils.errors import CompileError +from distutils.extension import Extension +from Cython.Distutils import build_ext as build_ext_c +_this_dir = os.path.dirname(__file__) MAJOR = 0 MINOR = 4 MICRO = 6 VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO) +# Write version info def write_version_py(filename=None): cnt = """\ version = '%s' @@ -43,12 +51,14 @@ from .lib.capnp import _CAPNP_VERSION as LIBCAPNP_VERSION write_version_py() +# Try to convert README using pandoc try: import pypandoc long_description = pypandoc.convert('README.md', 'rst') except (IOError, ImportError): long_description = '' +# Clean command, invoked with `python setup.py clean` from distutils.command.clean import clean as _clean class clean(_clean): def run(self): @@ -60,6 +70,31 @@ class clean(_clean): except OSError: pass + +class build_libcapnp_ext(build_ext_c): + + def build_extension(self, ext): + build_ext_c.build_extension(self, ext) + + def run(self): + try: + test_build() + except CompileError: + info("*WARNING* no libcapnp detected. Will download and build it from source now. If you have C++ Cap'n Proto installed, it may be out of date or is not being detected. Downloading and building libcapnp may take a while.") + bundle_dir = os.path.join(_this_dir, "bundled") + if not os.path.exists(bundle_dir): + os.mkdir(bundle_dir) + build_dir = os.path.join(_this_dir, "build") + if not os.path.exists(build_dir): + os.mkdir(build_dir) + fetch_libcapnp(bundle_dir) + + build_libcapnp(bundle_dir, build_dir) + + self.include_dirs += [os.path.join(build_dir, 'include')] + self.library_dirs += [os.path.join(build_dir, 'lib')] + + return build_ext_c.run(self) setup( name="pycapnp", packages=["capnp"], @@ -67,7 +102,8 @@ setup( package_data={'capnp': ['*.pxd', '*.h', '*.capnp', 'helpers/*.pxd', 'helpers/*.h', 'includes/*.pxd', 'lib/*.pxd', 'lib/*.py', 'lib/*.pyx']}, ext_modules=cythonize('capnp/lib/*.pyx'), cmdclass = { - 'clean': clean + 'clean': clean, + 'build_ext': build_libcapnp_ext }, install_requires=[ 'cython >= 0.21',