From 61088678c8d3694c076fa38f6081df528262043d Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 18 Jan 2011 17:23:31 -0800 Subject: [PATCH 1/6] configure subcommand initial commit adapted and simplified h5py's setup.py configure command for zeromq --- detect.py | 90 +++++++++++++++ setup.py | 325 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 384 insertions(+), 31 deletions(-) create mode 100644 detect.py diff --git a/detect.py b/detect.py new file mode 100644 index 000000000..591a1bfae --- /dev/null +++ b/detect.py @@ -0,0 +1,90 @@ +"""Detect zmq version""" +# +# Copyright (c) 2011 Min Ragan-Kelley +# +# This file is part of pyzmq, copied and adapted from h5py. +# h5py source used under the New BSD license +# +# h5py: +# BSD license: +# +# pyzmq is free software; you can redistribute it and/or modify it under +# the terms of the Lesser GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# pyzmq is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Lesser GNU General Public License for more details. +# +# You should have received a copy of the Lesser GNU General Public License +# along with this program. If not, see . + +import sys +import os.path + +pjoin = os.path.join + +def detect_zmq(basedir, **compiler_attrs): + """ Compile, link & execute a test program, in empty directory basedir. + The C compiler will be updated with any keywords given via setattr. + + Returns a dictionary containing information about the zmq installation. + """ + + from distutils import ccompiler + import subprocess + + cc = ccompiler.new_compiler() + for name, val in compiler_attrs.items(): + setattr(cc, name, val) + + cfile = pjoin(basedir, 'vers.c') + efile = pjoin(basedir, 'vers') + + f = open(cfile, 'w') + try: + f.write( +r""" +#include +#include "zmq.h" + +int main(){ + unsigned int major, minor, patch; + zmq_version(&major, &minor, &patch); + fprintf(stdout, "vers: %d.%d.%d\n", major, minor, patch); + return 0; +} +""") + finally: + f.close() + + if sys.platform == 'darwin': + # allow for missing UB arch, since it will still work: + preargs = ['-undefined', 'dynamic_lookup'] + else: + preargs = None + + objs = cc.compile([cfile]) + cc.link_executable(objs, efile, extra_preargs=preargs) + + result = subprocess.Popen(efile, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + so, se = result.communicate() + # for py3k: + so = so.decode() + se = se.decode() + if result.returncode: + raise IOError("Error running version detection script:\n%s\n%s" % (so,se)) + + 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) + + props['options'] = compiler_attrs + + return props diff --git a/setup.py b/setup.py index 1ab493cc0..56b1f47d2 100644 --- a/setup.py +++ b/setup.py @@ -18,13 +18,19 @@ # You should have received a copy of the Lesser GNU General Public License # along with this program. If not, see . # +# The `configure` subcommand is copied and adaped from h5py +# h5py source used under the New BSD license +# +# h5py: +# BSD license: +# #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- from __future__ import with_statement -import os, sys +import os, sys, shutil from traceback import print_exc from distutils.core import setup, Command @@ -38,18 +44,20 @@ from os.path import splitext, basename, join as pjoin from subprocess import Popen, PIPE +import logging -from zmqversion import check_zmq_version +try: + from configparser import ConfigParser +except: + from ConfigParser import ConfigParser try: import nose except ImportError: nose = None -try: - from os.path import walk -except: - from os import walk +# local script imports: +import detect #----------------------------------------------------------------------------- # Flags @@ -66,11 +74,249 @@ # the minimum zeromq version this will work against: min_zmq = (2,1,0) +#----------------------------------------------------------------------------- +# Configuration (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- +logger = logging.getLogger() +logger.addHandler(logging.StreamHandler(sys.stderr)) +# --- Convenience functions -------------------------------------------------- + +def debug(what): + pass + +def fatal(instring, code=1): + logger.error("Fatal: "+instring) + exit(code) + +def warn(instring): + logger.error("Warning: "+instring) + +def localpath(*args): + return os.path.abspath(reduce(pjoin, (os.path.dirname(__file__),)+args)) + +def loadpickle(name): + """ Load object from pickle file, or None if it can't be opened """ + import pickle + name = pjoin('conf', name) + try: + f = open(name,'rb') + except IOError: + # raise + return None + try: + return pickle.load(f) + except Exception: + # raise + return None + finally: + f.close() + +def savepickle(name, data): + """ Save to pickle file, exiting if it can't be written """ + import pickle + if not os.path.exists('conf'): + os.mkdir('conf') + name = pjoin('conf', name) + try: + f = open(name, 'wb') + except IOError: + fatal("Can't open pickle file \"%s\" for writing" % name) + try: + pickle.dump(data, f, 0) + finally: + f.close() + +def v_str(v_tuple): + """turn (2,0,1) into '2.0.1'.""" + return ".".join(str(x) for x in v_tuple) + +# --- Try to discover path --- + +def discover_settings(): + """ Discover custom settings for ZMQ path""" + + def get_eargs(): + """ Look for options in environment vars """ + + settings = {} + + zmq = os.environ.get("ZMQ_DIR", '') + if zmq != '': + debug("Found environ var ZMQ_DIR=%s" % zmq) + settings['zmq'] = zmq + + return settings + + def get_cfg_args(): + """ Look for options in setup.cfg """ + + settings = {} + zmq = '' + if not os.path.exists('setup.cfg'): + return settings + cfg = ConfigParser() + cfg.read('setup.cfg') + if 'build_ext' in cfg.sections() and \ + cfg.has_option('build_ext', 'include_dirs'): + includes = cfg.get('build_ext', 'include_dirs') + include = includes.split(os.pathsep)[0] + if include.endswith('include') and os.path.isdir(include): + zmq = include[:-8] + if zmq != '': + debug("Found ZMQ=%s in setup.cfg" % zmq) + settings['zmq'] = zmq + + return settings + + def get_cargs(): + """ Look for global options in the command line """ + settings = loadpickle('buildconf.pickle') + if settings is None: settings = {} + for arg in sys.argv[:]: + if arg.find('--zmq=') == 0: + zmq = arg.split('=')[-1] + if zmq.lower() == 'default': + settings.pop('zmq', None) + else: + settings['zmq'] = zmq + sys.argv.remove(arg) + savepickle('buildconf.pickle', settings) + return settings + + settings = get_cfg_args() # lowest priority + settings.update(get_eargs()) + settings.update(get_cargs()) # highest priority + return settings.get('zmq') + +ZMQ = None +for cmd in ['install', 'build', 'build_ext', 'configure']: + if cmd in sys.argv: + ZMQ = discover_settings() + break + +if ZMQ is not None and not os.path.exists(ZMQ): + warn("ZMQ directory \"%s\" does not appear to exist" % ZMQ) + +# --- compiler settings ------------------------------------------------- + +if sys.platform.startswith('win'): + COMPILER_SETTINGS = { + 'libraries' : ['libzmq'], + 'include_dirs' : [], + 'library_dirs' : [], + } + if ZMQ is not None: + COMPILER_SETTINGS['include_dirs'] += [pjoin(ZMQ, 'include')] + COMPILER_SETTINGS['library_dirs'] += [pjoin(ZMQ, 'lib')] +else: + COMPILER_SETTINGS = { + 'libraries' : ['zmq'], + 'include_dirs' : [], + 'library_dirs' : [], + } + if ZMQ is not None: + COMPILER_SETTINGS['include_dirs'] += [pjoin(ZMQ, 'include')] + COMPILER_SETTINGS['library_dirs'] += [pjoin(ZMQ, 'lib')] + elif sys.platform == 'darwin' and os.path.isdir('/opt/local/lib'): + # allow macports default + COMPILER_SETTINGS['include_dirs'] += ['/opt/local/include'] + COMPILER_SETTINGS['library_dirs'] += ['/opt/local/lib'] + COMPILER_SETTINGS['runtime_library_dirs'] = [os.path.abspath(x) for x in COMPILER_SETTINGS['library_dirs']] + #----------------------------------------------------------------------------- # Extra commands #----------------------------------------------------------------------------- +class configure(Command): + """Configure command adapted from h5py""" + + description = "Discover ZMQ version and features" + + # DON'T REMOVE: distutils demands these be here even if they do nothing. + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + + tempdir = 'detect' + + def create_tempdir(self): + self.erase_tempdir() + os.mkdir(self.tempdir) + if sys.platform.startswith('win'): + # fetch libzmq.dll into local dir + if ZMQ is None: + fatal("ZMQ directory must be specified on Windows via setup.cfg or 'python setup.py configure --zmq=/path/to/zeromq2'") + shutil.copy(pjoin(ZMQ, 'lib', 'libzmq.dll'), pjoin(self.tempdir, 'libzmq.dll')) + + def erase_tempdir(self): + import shutil + try: + shutil.rmtree(self.tempdir) + except Exception: + pass + + def getcached(self): + return loadpickle('configure.pickle') + + def check_zmq_version(self): + zmq = ZMQ + if zmq is not None and not os.path.isdir(zmq): + fatal("Custom zmq directory \"%s\" does not exist" % zmq) + + config = self.getcached() + if config is None or config['options'] != COMPILER_SETTINGS: + self.run() + config = self.config + + vers = config['vers'] + vs = v_str(vers) + if vers < min_zmq: + fatal("Detected ZMQ version: %s, but depend on zmq >= %s"%( + vs, v_str(min_zmq)) + +'\n Using ZMQ=%s'%(zmq or 'unspecified')) + fatal() + pyzmq_version = extract_version().strip('abcdefghijklmnopqrstuvwxyz') + + if vs < pyzmq_version: + warn("Detected ZMQ version: %s, but pyzmq is based on zmq %s."%( + vs, pyzmq_version)) + warn("Some features may be missing or broken.") + print('*'*42) + + if sys.platform.startswith('win'): + # fetch libzmq.dll into local dir + if zmq is None: + fatal("ZMQ directory must be specified on Windows") + shutil.copy(pjoin(zmq, 'lib', 'libzmq.dll'), localpath('zmq','libzmq.dll')) + + def run(self): + self.create_tempdir() + try: + print ("*"*42) + print ("Configure: Autodetecting ZMQ settings...") + print (" Custom ZMQ dir: %s" % (ZMQ,)) + config = detect.detect_zmq(self.tempdir, **COMPILER_SETTINGS) + savepickle('configure.pickle', config) + except Exception: + logger.error(""" + Failed to compile ZMQ test program. Please check to make sure: + + * You have a C compiler installed + * A development version of Python is installed (including header files) + * A development version of ZeroMQ >= 2.1.0 is installed (including header files) + * If ZMQ is not in a default location, supply the argument --zmq=""") + raise + else: + print (" ZMQ version detected: %s" % v_str(config['vers'])) + finally: + print ("*"*42) + self.erase_tempdir() + self.config = config + class TestCommand(Command): """Custom distutils command to run the test suite.""" @@ -106,9 +352,9 @@ def run(self): import zmq except ImportError: print_exc() - print ("Could not import zmq!") - print ("You must build pyzmq with 'python setup.py build_ext --inplace' for 'python setup.py test' to work.") - print ("If you did build pyzmq in-place, then this is a real error.") + fatal('\n '.join(["Could not import zmq!", + "You must build pyzmq with 'python setup.py build_ext --inplace' for 'python setup.py test' to work.", + "If you did build pyzmq in-place, then this is a real error."])) sys.exit(1) if nose is None: @@ -165,10 +411,15 @@ class CleanCommand(Command): def initialize_options(self): self._clean_me = [] - for root, dirs, files in os.walk('.'): + self._clean_trees = [] + for root, dirs, files in os.walk('zmq'): for f in files: - if f.endswith('.pyc') or f.endswith('.so'): + if os.path.splitext(f)[-1] in ('.pyc', '.so', '.o', '.pyd'): self._clean_me.append(pjoin(root, f)) + for d in [ 'build' ]: + if os.path.isdir(d): + self._clean_trees.append(d) + def finalize_options(self): pass @@ -179,6 +430,11 @@ def run(self): os.unlink(clean_me) except: pass + for clean_tree in self._clean_trees: + try: + shutil.rmtree(clean_tree) + except: + pass class CheckSDist(sdist): @@ -192,11 +448,14 @@ def initialize_options(self): if f.endswith('.pyx'): self._pyxfiles.append(pjoin(root, f)) def run(self): - for pyxfile in self._pyxfiles: - cfile = pyxfile[:-3]+'c' - msg = "C-source file '%s' not found."%(cfile)+\ - " Run 'setup.py cython' before sdist." - assert os.path.isfile(cfile), msg + if 'cython' in cmdclass: + self.run_command('cython') + else: + for pyxfile in self._pyxfiles: + cfile = pyxfile[:-3]+'c' + msg = "C-source file '%s' not found."%(cfile)+\ + " Run 'setup.py cython' before sdist." + assert os.path.isfile(cfile), msg sdist.run(self) class CheckingBuildExt(build_ext): @@ -221,7 +480,9 @@ def build_extensions(self): def run(self): # check version, to prevent confusing undefined constant errors - check_zmq_version(min_zmq) + # check_zmq_version(min_zmq) + configure = self.distribution.get_command_obj('configure') + configure.check_zmq_version() build_ext.run(self) @@ -234,13 +495,16 @@ def run(self): for warning in ('unused-function', 'strict-aliasing'): extra_flags.append('-Wno-'+warning) +COMPILER_SETTINGS['extra_compile_args'] = extra_flags + #----------------------------------------------------------------------------- # Extensions #----------------------------------------------------------------------------- -cmdclass = {'test':TestCommand, 'clean':CleanCommand, 'revision':GitRevisionCommand} +cmdclass = {'test':TestCommand, 'clean':CleanCommand, 'revision':GitRevisionCommand, + 'configure': configure} -includes = [pjoin('zmq', sub) for sub in ('utils','core','devices')] +COMPILER_SETTINGS['include_dirs'] += [pjoin('zmq', sub) for sub in ('utils','core','devices')] def pxd(subdir, name): return os.path.abspath(pjoin('zmq', subdir, name+'.pxd')) @@ -278,7 +542,9 @@ def dotc(subdir, name): try: from Cython.Distutils import build_ext + cython=True except ImportError: + cython=False suffix = '.c' cmdclass['build_ext'] = CheckingBuildExt else: @@ -292,20 +558,16 @@ class CythonCommand(build_ext): def build_extension(self, ext): pass - class CheckZMQBuildExt(build_ext): + class zbuild_ext(build_ext): def run(self): - check_zmq_version(min_zmq) + configure = self.distribution.get_command_obj('configure') + configure.check_zmq_version() return build_ext.run(self) cmdclass['cython'] = CythonCommand - cmdclass['build_ext'] = CheckZMQBuildExt + cmdclass['build_ext'] = zbuild_ext cmdclass['sdist'] = CheckSDist -if sys.platform == 'win32': - libzmq = 'libzmq' -else: - libzmq = 'zmq' - extensions = [] for submod, packages in submodules.items(): for pkg in sorted(packages): @@ -315,9 +577,7 @@ def run(self): ext = Extension( 'zmq.%s.%s'%(submod, pkg), sources = sources, - libraries = [libzmq], - include_dirs = includes, - extra_compile_args = extra_flags + **COMPILER_SETTINGS ) extensions.append(ext) @@ -329,9 +589,12 @@ def run(self): } if release: - for pkg,data in package_data.iteritems(): + for pkg,data in package_data.items(): data.append('*.c') +if sys.platform.startswith('win'): + package_data['zmq'].append('libzmq.dll') + def extract_version(): """extract pyzmq version from core/version.pyx, so it's not multiply defined""" with open(pjoin('zmq', 'core', 'version.pyx')) as f: From 9bef246e4ecd45f1e67a0faf72adcea303d10a26 Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 18 Jan 2011 18:00:25 -0800 Subject: [PATCH 2/6] explicitly load libzmq.dll on Windows --- zmq/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zmq/__init__.py b/zmq/__init__.py index ce2566f49..ffe0c2f89 100644 --- a/zmq/__init__.py +++ b/zmq/__init__.py @@ -22,6 +22,12 @@ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- +import sys + +if sys.platform.startswith('win'): + import os, win32api + here = os.path.dirname(__file__) + win32api.LoadLibrary(os.path.join(here, 'libzmq.dll')) from zmq.utils import initthreads # initialize threads initthreads.init_threads() From 2324538abd789645f99bd567a70889b05e73a98a Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 18 Jan 2011 20:47:23 -0800 Subject: [PATCH 3/6] die meaningfully when zmq unspecified on Windows --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 56b1f47d2..e17bbe5a0 100644 --- a/setup.py +++ b/setup.py @@ -290,7 +290,7 @@ def check_zmq_version(self): if sys.platform.startswith('win'): # fetch libzmq.dll into local dir if zmq is None: - fatal("ZMQ directory must be specified on Windows") + fatal("ZMQ directory must be specified on Windows via setup.cfg or 'python setup.py configure --zmq=/path/to/zeromq2'") shutil.copy(pjoin(zmq, 'lib', 'libzmq.dll'), localpath('zmq','libzmq.dll')) def run(self): From 0cd90776155f816b2d70e02538f73260db0d4935 Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 18 Jan 2011 20:55:24 -0800 Subject: [PATCH 4/6] always discover_settings --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index e17bbe5a0..95f70bab0 100644 --- a/setup.py +++ b/setup.py @@ -188,11 +188,7 @@ def get_cargs(): settings.update(get_cargs()) # highest priority return settings.get('zmq') -ZMQ = None -for cmd in ['install', 'build', 'build_ext', 'configure']: - if cmd in sys.argv: - ZMQ = discover_settings() - break +ZMQ = discover_settings() if ZMQ is not None and not os.path.exists(ZMQ): warn("ZMQ directory \"%s\" does not appear to exist" % ZMQ) From c8bef8a31fa3fe147f80da086694a7d20fb967a1 Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 8 Mar 2011 16:54:48 -0800 Subject: [PATCH 5/6] configure edits per review --- MANIFEST.in | 2 +- README.rst | 32 +++++-- buildutils.py | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ detect.py | 90 -------------------- setup.py | 170 +++++++++++++------------------------ 5 files changed, 314 insertions(+), 211 deletions(-) create mode 100644 buildutils.py delete mode 100644 detect.py diff --git a/MANIFEST.in b/MANIFEST.in index 78f57fb57..413a7ab95 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,7 +13,7 @@ graft zmq graft perf exclude setup.cfg - +exclude zmq/libzmq* # exclude docs/_static # exclude docs/_templates diff --git a/README.rst b/README.rst index ca38fc8b7..e55a5b433 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ This package contains Python bindings for `0MQ `_. Versioning ========== -Current release of pyzmq is 2.1.1, and targets libzmq-2.1.1rc1. For zeromq +Current release of pyzmq is 2.1.2, and targets libzmq-2.1.2rc2. For zeromq 2.0.10 or `maint` branch, use pyzmq release 2.0.10 or the 2.0.x development branch. PyZMQ versioning follows 0MQ versioning. In general, your pyzmq version should be the same @@ -44,10 +44,16 @@ To build and install this Python package, you will first need to build and install the latest development version of 0MQ itself. After you have done this, follow these steps: -First, copy the ``setup.cfg.template`` file in this directory to ``setup.cfg`` -and edit the `include_dirs` and `library_dirs` fields of the ``setup.cfg`` -file to point to the directories that contain the library and header file for -your 0MQ installation. +Tell pyzmq where zeromq is via the configure subcommand: + + $ python setup.py configure --zmq=/path/to/zeromq2 + +or the zmq install directory on OSX/Linux: + + $ python setup.py configure --zmq=/usr/local + +The argument should be a directory containing a ``lib`` and a ``include`` directory, containing +``libzmq`` and ``zmq.h`` respectively. Second, run this command:: @@ -60,13 +66,21 @@ on GitHub. Windows ------- -Generally you'll need to add the location of ``libzmq.dll`` to your ``$PATH``. -Here's Microsoft's docs: -http://msdn.microsoft.com/en-us/library/7d83bc18(VS.80).aspx on this topic. +On Windows, libzmq.dll will be copied into the zmq directory, and installed along with pyzmq, +so you shouldn't need to edit your PATH. It is best to compile both ØMQ and PyØMQ with Microsoft Visual Studio 2008 or above. You should not need to use mingw. +Current testing indicates that running + + $ python setup.py bdist_msi + +successfully builds an MSI installer. Note that if you are on a development version of pyzmq, +you will need to edit the ``__version__`` in zmq/core/version.pyx and remove the 'dev', because +the msi builder rejects that as an invalid version for some reason. + + Linux ----- @@ -74,7 +88,7 @@ If you install libzmq to a location other than the default (``/usr/local``) on L you will need to do one of the following: * Set ``LD_LIBRARY_PATH`` to point to the ``lib`` directory of 0MQ. -* Build the extension using the ``-rpath`` flag:: +* Build the extension using the ``--rpath`` flag:: $ python setup.py build_ext --rpath=/opt/zeromq-dev/lib --inplace diff --git a/buildutils.py b/buildutils.py new file mode 100644 index 000000000..971c26357 --- /dev/null +++ b/buildutils.py @@ -0,0 +1,231 @@ +"""Detect zmq version""" +# +# Copyright (c) 2011 Min Ragan-Kelley +# +# This file is part of pyzmq, copied and adapted from h5py. +# h5py source used under the New BSD license +# +# h5py: +# BSD license: +# +# pyzmq is free software; you can redistribute it and/or modify it under +# the terms of the Lesser GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# pyzmq is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Lesser GNU General Public License for more details. +# +# You should have received a copy of the Lesser GNU General Public License +# along with this program. If not, see . + +import sys +import os +import logging + +try: + from configparser import ConfigParser +except: + from ConfigParser import ConfigParser + +pjoin = os.path.join + +#----------------------------------------------------------------------------- +# Logging (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- +logger = logging.getLogger() + +def debug(what): + pass + +def fatal(instring, code=1): + logger.error("Fatal: "+instring) + exit(code) + +def warn(instring): + logger.error("Warning: "+instring) + + +#----------------------------------------------------------------------------- +# Utility functions (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- + +def detect_zmq(basedir, **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_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) + options : dict + The compiler options used to compile the test function, e.g. `include_dirs`, + `library_dirs`, `libs`, etc. + """ + + from distutils import ccompiler + import subprocess + + cc = ccompiler.new_compiler() + for name, val in compiler_attrs.items(): + setattr(cc, name, val) + + cfile = pjoin(basedir, 'vers.c') + efile = pjoin(basedir, 'vers') + + f = open(cfile, 'w') + try: + f.write( +r""" +#include +#include "zmq.h" + +int main(){ + unsigned int major, minor, patch; + zmq_version(&major, &minor, &patch); + fprintf(stdout, "vers: %d.%d.%d\n", major, minor, patch); + return 0; +} +""") + finally: + f.close() + + if sys.platform == 'darwin': + # allow for missing UB arch, since it will still work: + preargs = ['-undefined', 'dynamic_lookup'] + else: + preargs = None + + objs = cc.compile([cfile]) + cc.link_executable(objs, efile, extra_preargs=preargs) + + result = subprocess.Popen(efile, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + so, se = result.communicate() + # for py3k: + so = so.decode() + se = se.decode() + if result.returncode: + 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) + + props['options'] = compiler_attrs + return props + +def localpath(*args): + return os.path.abspath(reduce(pjoin, (os.path.dirname(__file__),)+args)) + +def loadpickle(name): + """ Load object from pickle file, or None if it can't be opened """ + import pickle + name = pjoin('conf', name) + try: + f = open(name,'rb') + except IOError: + # raise + return None + try: + return pickle.load(f) + except Exception: + # raise + return None + finally: + f.close() + +def savepickle(name, data): + """ Save to pickle file, exiting if it can't be written """ + import pickle + if not os.path.exists('conf'): + os.mkdir('conf') + name = pjoin('conf', name) + try: + f = open(name, 'wb') + except IOError: + fatal("Can't open pickle file \"%s\" for writing" % name) + try: + pickle.dump(data, f, 0) + finally: + f.close() + +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_DIR", '') + if zmq != '': + debug("Found environ var ZMQ_DIR=%s" % zmq) + settings['zmq'] = zmq + + return settings + +# +def get_cfg_args(): + """ Look for options in setup.cfg """ + + settings = {} + zmq = '' + if not os.path.exists('setup.cfg'): + return settings + cfg = ConfigParser() + cfg.read('setup.cfg') + if 'build_ext' in cfg.sections() and \ + cfg.has_option('build_ext', 'include_dirs'): + includes = cfg.get('build_ext', 'include_dirs') + include = includes.split(os.pathsep)[0] + if include.endswith('include') and os.path.isdir(include): + zmq = include[:-8] + if zmq != '': + debug("Found ZMQ=%s in setup.cfg" % zmq) + settings['zmq'] = zmq + + return settings + +# +def get_cargs(): + """ Look for global options in the command line """ + settings = loadpickle('buildconf.pickle') + if settings is None: settings = {} + for arg in sys.argv[:]: + if arg.find('--zmq=') == 0: + zmq = arg.split('=')[-1] + if zmq.lower() == 'default': + settings.pop('zmq', None) + else: + settings['zmq'] = zmq + sys.argv.remove(arg) + savepickle('buildconf.pickle', settings) + return settings + +def discover_settings(): + """ Discover custom settings for ZMQ path""" + settings = get_cfg_args() # lowest priority + settings.update(get_eargs()) + settings.update(get_cargs()) # highest priority + return settings.get('zmq') diff --git a/detect.py b/detect.py deleted file mode 100644 index 591a1bfae..000000000 --- a/detect.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Detect zmq version""" -# -# Copyright (c) 2011 Min Ragan-Kelley -# -# This file is part of pyzmq, copied and adapted from h5py. -# h5py source used under the New BSD license -# -# h5py: -# BSD license: -# -# pyzmq is free software; you can redistribute it and/or modify it under -# the terms of the Lesser GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# pyzmq is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# Lesser GNU General Public License for more details. -# -# You should have received a copy of the Lesser GNU General Public License -# along with this program. If not, see . - -import sys -import os.path - -pjoin = os.path.join - -def detect_zmq(basedir, **compiler_attrs): - """ Compile, link & execute a test program, in empty directory basedir. - The C compiler will be updated with any keywords given via setattr. - - Returns a dictionary containing information about the zmq installation. - """ - - from distutils import ccompiler - import subprocess - - cc = ccompiler.new_compiler() - for name, val in compiler_attrs.items(): - setattr(cc, name, val) - - cfile = pjoin(basedir, 'vers.c') - efile = pjoin(basedir, 'vers') - - f = open(cfile, 'w') - try: - f.write( -r""" -#include -#include "zmq.h" - -int main(){ - unsigned int major, minor, patch; - zmq_version(&major, &minor, &patch); - fprintf(stdout, "vers: %d.%d.%d\n", major, minor, patch); - return 0; -} -""") - finally: - f.close() - - if sys.platform == 'darwin': - # allow for missing UB arch, since it will still work: - preargs = ['-undefined', 'dynamic_lookup'] - else: - preargs = None - - objs = cc.compile([cfile]) - cc.link_executable(objs, efile, extra_preargs=preargs) - - result = subprocess.Popen(efile, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - so, se = result.communicate() - # for py3k: - so = so.decode() - se = se.decode() - if result.returncode: - raise IOError("Error running version detection script:\n%s\n%s" % (so,se)) - - 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) - - props['options'] = compiler_attrs - - return props diff --git a/setup.py b/setup.py index 95f70bab0..3387ab76f 100644 --- a/setup.py +++ b/setup.py @@ -36,8 +36,9 @@ from distutils.core import setup, Command from distutils.ccompiler import get_default_compiler from distutils.extension import Extension -from distutils.command.sdist import sdist +from distutils.command.bdist import bdist from distutils.command.build_ext import build_ext +from distutils.command.sdist import sdist from unittest import TextTestRunner, TestLoader from glob import glob @@ -57,7 +58,7 @@ nose = None # local script imports: -import detect +from buildutils import discover_settings, v_str, localpath, savepickle, loadpickle, detect_zmq #----------------------------------------------------------------------------- # Flags @@ -74,12 +75,21 @@ # the minimum zeromq version this will work against: min_zmq = (2,1,0) + +# set dylib ext: +if sys.platform.startswith('win'): + lib_ext = '.dll' +elif sys.platform == 'darwin': + lib_ext = '.dylib' +else: + lib_ext = '.so' + + #----------------------------------------------------------------------------- -# Configuration (adapted from h5py: http://h5py.googlecode.com) +# Logging (adapted from h5py: http://h5py.googlecode.com) #----------------------------------------------------------------------------- logger = logging.getLogger() logger.addHandler(logging.StreamHandler(sys.stderr)) -# --- Convenience functions -------------------------------------------------- def debug(what): pass @@ -91,102 +101,10 @@ def fatal(instring, code=1): def warn(instring): logger.error("Warning: "+instring) -def localpath(*args): - return os.path.abspath(reduce(pjoin, (os.path.dirname(__file__),)+args)) - -def loadpickle(name): - """ Load object from pickle file, or None if it can't be opened """ - import pickle - name = pjoin('conf', name) - try: - f = open(name,'rb') - except IOError: - # raise - return None - try: - return pickle.load(f) - except Exception: - # raise - return None - finally: - f.close() - -def savepickle(name, data): - """ Save to pickle file, exiting if it can't be written """ - import pickle - if not os.path.exists('conf'): - os.mkdir('conf') - name = pjoin('conf', name) - try: - f = open(name, 'wb') - except IOError: - fatal("Can't open pickle file \"%s\" for writing" % name) - try: - pickle.dump(data, f, 0) - finally: - f.close() - -def v_str(v_tuple): - """turn (2,0,1) into '2.0.1'.""" - return ".".join(str(x) for x in v_tuple) - -# --- Try to discover path --- - -def discover_settings(): - """ Discover custom settings for ZMQ path""" - - def get_eargs(): - """ Look for options in environment vars """ - - settings = {} - - zmq = os.environ.get("ZMQ_DIR", '') - if zmq != '': - debug("Found environ var ZMQ_DIR=%s" % zmq) - settings['zmq'] = zmq - - return settings - - def get_cfg_args(): - """ Look for options in setup.cfg """ - - settings = {} - zmq = '' - if not os.path.exists('setup.cfg'): - return settings - cfg = ConfigParser() - cfg.read('setup.cfg') - if 'build_ext' in cfg.sections() and \ - cfg.has_option('build_ext', 'include_dirs'): - includes = cfg.get('build_ext', 'include_dirs') - include = includes.split(os.pathsep)[0] - if include.endswith('include') and os.path.isdir(include): - zmq = include[:-8] - if zmq != '': - debug("Found ZMQ=%s in setup.cfg" % zmq) - settings['zmq'] = zmq - - return settings - - def get_cargs(): - """ Look for global options in the command line """ - settings = loadpickle('buildconf.pickle') - if settings is None: settings = {} - for arg in sys.argv[:]: - if arg.find('--zmq=') == 0: - zmq = arg.split('=')[-1] - if zmq.lower() == 'default': - settings.pop('zmq', None) - else: - settings['zmq'] = zmq - sys.argv.remove(arg) - savepickle('buildconf.pickle', settings) - return settings - - settings = get_cfg_args() # lowest priority - settings.update(get_eargs()) - settings.update(get_cargs()) # highest priority - return settings.get('zmq') +#----------------------------------------------------------------------------- +# Configuration (adapted from h5py: http://h5py.googlecode.com) +#----------------------------------------------------------------------------- + ZMQ = discover_settings() @@ -224,7 +142,7 @@ def get_cargs(): # Extra commands #----------------------------------------------------------------------------- -class configure(Command): +class Configure(Command): """Configure command adapted from h5py""" description = "Discover ZMQ version and features" @@ -244,9 +162,18 @@ def create_tempdir(self): os.mkdir(self.tempdir) if sys.platform.startswith('win'): # fetch libzmq.dll into local dir - if ZMQ is None: + local_dll = pjoin(self.tempdir, 'libzmq.dll') + if ZMQ is None and not os.path.exists(local_dll): fatal("ZMQ directory must be specified on Windows via setup.cfg or 'python setup.py configure --zmq=/path/to/zeromq2'") - shutil.copy(pjoin(ZMQ, 'lib', 'libzmq.dll'), pjoin(self.tempdir, 'libzmq.dll')) + + try: + shutil.copy(pjoin(ZMQ, 'lib', 'libzmq.dll'), local_dll) + except Exception: + if not os.path.exists(local_dll): + warn("Could not copy libzmq into zmq/, which is usually necessary on Windows." + "Please specify zmq prefix via configure --zmq=/path/to/zmq or copy " + "libzmq into zmq/ manually.") + def erase_tempdir(self): import shutil @@ -274,7 +201,6 @@ def check_zmq_version(self): fatal("Detected ZMQ version: %s, but depend on zmq >= %s"%( vs, v_str(min_zmq)) +'\n Using ZMQ=%s'%(zmq or 'unspecified')) - fatal() pyzmq_version = extract_version().strip('abcdefghijklmnopqrstuvwxyz') if vs < pyzmq_version: @@ -285,9 +211,16 @@ def check_zmq_version(self): if sys.platform.startswith('win'): # fetch libzmq.dll into local dir - if zmq is None: + local_dll = localpath('zmq','libzmq.dll') + if zmq is None and not os.path.exists(local_dll): fatal("ZMQ directory must be specified on Windows via setup.cfg or 'python setup.py configure --zmq=/path/to/zeromq2'") - shutil.copy(pjoin(zmq, 'lib', 'libzmq.dll'), localpath('zmq','libzmq.dll')) + try: + shutil.copy(pjoin(zmq, 'lib', 'libzmq.dll'), local_dll) + except Exception: + if not os.path.exists(local_dll): + warn("Could not copy libzmq into zmq/, which is usually necessary on Windows." + "Please specify zmq prefix via configure --zmq=/path/to/zmq or copy " + "libzmq into zmq/ manually.") def run(self): self.create_tempdir() @@ -295,8 +228,7 @@ def run(self): print ("*"*42) print ("Configure: Autodetecting ZMQ settings...") print (" Custom ZMQ dir: %s" % (ZMQ,)) - config = detect.detect_zmq(self.tempdir, **COMPILER_SETTINGS) - savepickle('configure.pickle', config) + config = detect_zmq(self.tempdir, **COMPILER_SETTINGS) except Exception: logger.error(""" Failed to compile ZMQ test program. Please check to make sure: @@ -307,6 +239,7 @@ def run(self): * If ZMQ is not in a default location, supply the argument --zmq=""") raise else: + savepickle('configure.pickle', config) print (" ZMQ version detected: %s" % v_str(config['vers'])) finally: print ("*"*42) @@ -454,6 +387,21 @@ def run(self): assert os.path.isfile(cfile), msg sdist.run(self) +class CopyingBDist(bdist): + """copy libzmq for bdist""" + def run(self): + libzmq = 'libzmq'+lib_ext + # copy libzmq into zmq for bdist + try: + shutil.copy(pjoin(ZMQ, 'lib', libzmq), localpath('zmq',libzmq)) + except Exception: + if not os.path.exists(localpath('zmq',libzmq)): + raise IOError("Could not copy libzmq into zmq/, which is necessary for bdist." + "Please specify zmq prefix via configure --zmq=/path/to/zmq or copy " + "libzmq into zmq/ manually.") + + bdist.run(self) + class CheckingBuildExt(build_ext): """Subclass build_ext to get clearer report if Cython is neccessary.""" @@ -498,7 +446,7 @@ def run(self): #----------------------------------------------------------------------------- cmdclass = {'test':TestCommand, 'clean':CleanCommand, 'revision':GitRevisionCommand, - 'configure': configure} + 'configure': Configure, 'bdist': CopyingBDist} COMPILER_SETTINGS['include_dirs'] += [pjoin('zmq', sub) for sub in ('utils','core','devices')] @@ -588,8 +536,8 @@ def run(self): for pkg,data in package_data.items(): data.append('*.c') -if sys.platform.startswith('win'): - package_data['zmq'].append('libzmq.dll') +if sys.platform.startswith('win') or 'bdist' in [ s.lower() for s in sys.argv ]: + package_data['zmq'].append('libzmq'+lib_ext) def extract_version(): """extract pyzmq version from core/version.pyx, so it's not multiply defined""" From b364d62b5990972dda7833921ae996b65a5ea4ba Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 8 Mar 2011 21:06:40 -0800 Subject: [PATCH 6/6] more configure edits per review --- buildutils.py | 3 +-- setup.py | 21 +++------------------ 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/buildutils.py b/buildutils.py index 971c26357..26d74ffd3 100644 --- a/buildutils.py +++ b/buildutils.py @@ -36,6 +36,7 @@ # Logging (adapted from h5py: http://h5py.googlecode.com) #----------------------------------------------------------------------------- logger = logging.getLogger() +logger.addHandler(logging.StreamHandler(sys.stderr)) def debug(what): pass @@ -185,7 +186,6 @@ def get_eargs(): return settings -# def get_cfg_args(): """ Look for options in setup.cfg """ @@ -207,7 +207,6 @@ def get_cfg_args(): return settings -# def get_cargs(): """ Look for global options in the command line """ settings = loadpickle('buildconf.pickle') diff --git a/setup.py b/setup.py index 3387ab76f..5a5c2bce3 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,8 @@ nose = None # local script imports: -from buildutils import discover_settings, v_str, localpath, savepickle, loadpickle, detect_zmq +from buildutils import (discover_settings, v_str, localpath, savepickle, loadpickle, detect_zmq, + warn, fatal) #----------------------------------------------------------------------------- # Flags @@ -85,22 +86,6 @@ lib_ext = '.so' -#----------------------------------------------------------------------------- -# Logging (adapted from h5py: http://h5py.googlecode.com) -#----------------------------------------------------------------------------- -logger = logging.getLogger() -logger.addHandler(logging.StreamHandler(sys.stderr)) - -def debug(what): - pass - -def fatal(instring, code=1): - logger.error("Fatal: "+instring) - exit(code) - -def warn(instring): - logger.error("Warning: "+instring) - #----------------------------------------------------------------------------- # Configuration (adapted from h5py: http://h5py.googlecode.com) #----------------------------------------------------------------------------- @@ -230,7 +215,7 @@ def run(self): print (" Custom ZMQ dir: %s" % (ZMQ,)) config = detect_zmq(self.tempdir, **COMPILER_SETTINGS) except Exception: - logger.error(""" + logging.error(""" Failed to compile ZMQ test program. Please check to make sure: * You have a C compiler installed