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',