#!/usr/bin/env python
#-----------------------------------------------------------------------------
#  Copyright (C) PyZMQ Developers
#  Distributed under the terms of the Modified BSD License.
#
#  The `configure` subcommand is copied and adaped from h5py
#  h5py source used under the New BSD license
#
#  h5py: <http://code.google.com/p/h5py/>
#
#  The code to bundle libzmq as an Extension is from pyzmq-static
#  pyzmq-static source used under the New BSD license
#
#  pyzmq-static: <https://github.com/brandon-rhodes/pyzmq-static>
#-----------------------------------------------------------------------------

from __future__ import with_statement, print_function

import copy
import os
import re
import shutil
import subprocess
import sys
import time
import errno
import platform
from traceback import print_exc


import distutils
from distutils.core import setup, Command
from distutils.ccompiler import get_default_compiler
from distutils.ccompiler import new_compiler
from distutils.extension import Extension
from distutils.errors import CompileError, LinkError
from distutils.command.build import build
from distutils.command.build_ext import build_ext
from distutils.command.sdist import sdist
from distutils.version import LooseVersion as V

from unittest import TextTestRunner, TestLoader
from glob import glob
from os.path import splitext, basename, join as pjoin

from subprocess import Popen, PIPE
import logging

try:
    from configparser import ConfigParser
except:
    from ConfigParser import ConfigParser

try:
    import nose
except ImportError:
    nose = None

# local script imports:
from buildutils import (
    discover_settings, v_str, save_config, load_config, detect_zmq, merge,
    config_from_prefix,
    info, warn, fatal, debug, line, copy_and_patch_libzmq, localpath,
    fetch_libsodium, stage_libsodium_headers, fetch_libzmq, stage_platform_hpp,
    bundled_version, customize_mingw,
    test_compilation, compile_and_run,
    patch_lib_paths,
    )

#-----------------------------------------------------------------------------
# Flags
#-----------------------------------------------------------------------------

pypy = 'PyPy' in sys.version

# reference points for zmq compatibility
min_zmq = (2,1,4)
target_zmq = bundled_version
dev_zmq = (target_zmq[0], target_zmq[1] + 1, 0)

# set dylib ext:
if sys.platform.startswith('win'):
    lib_ext = '.dll'
elif sys.platform == 'darwin':
    lib_ext = '.dylib'
else:
    lib_ext = '.so'

# whether any kind of bdist is happening
doing_bdist = any(arg.startswith('bdist') for arg in sys.argv[1:])

# allow `--zmq=foo` to be passed at any point,
# but always assign it to configure

configure_idx = -1
fetch_idx = -1
for idx, arg in enumerate(list(sys.argv)):
    # track index of configure and fetch_libzmq
    if arg == 'configure':
        configure_idx = idx
    elif arg == 'fetch_libzmq':
        fetch_idx = idx
    
    if arg.startswith('--zmq='):
        sys.argv.pop(idx)
        if configure_idx < 0:
            if fetch_idx < 0:
                configure_idx = 1
            else:
                configure_idx = fetch_idx + 1
            sys.argv.insert(configure_idx, 'configure')
        sys.argv.insert(configure_idx + 1, arg)
        break

#-----------------------------------------------------------------------------
# Configuration (adapted from h5py: http://h5py.googlecode.com)
#-----------------------------------------------------------------------------

# --- compiler settings -------------------------------------------------

def bundled_settings():
    """settings for linking extensions against bundled libzmq"""
    settings = {}
    settings['libraries'] = []
    settings['library_dirs'] = []
    settings['include_dirs'] = [pjoin("bundled", "zeromq", "include")]
    settings['runtime_library_dirs'] = []
    # add pthread on freebsd
    # is this necessary?
    if sys.platform.startswith('freebsd'):
        settings['libraries'].append('pthread')
    elif sys.platform.startswith('win'):
        # link against libzmq in build dir:
        plat = distutils.util.get_platform()
        temp = 'temp.%s-%s' % (plat, sys.version[0:3])
        settings['libraries'].append('libzmq')
        settings['library_dirs'].append(pjoin('build', temp, 'Release', 'buildutils'))
    
    return settings


def settings_from_prefix(prefix=None, bundle_libzmq_dylib=False):
    """load appropriate library/include settings from ZMQ prefix"""
    settings = {}
    settings['libraries'] = []
    settings['include_dirs'] = []
    settings['library_dirs'] = []
    settings['runtime_library_dirs'] = []
    settings['extra_link_args'] = [] 
    
    if sys.platform.startswith('win'):
        settings['libraries'].append('libzmq')
        
        if prefix:
            settings['include_dirs'] += [pjoin(prefix, 'include')]
            settings['library_dirs'] += [pjoin(prefix, 'lib')]
    else:

        # If prefix is not explicitly set, pull it from pkg-config by default.

        if not prefix:
            try:
                p = Popen('pkg-config --variable=prefix --print-errors libzmq'.split(), stdout=PIPE, stderr=PIPE)
            except OSError as e:
                if e.errno == errno.ENOENT:
                    info("pkg-config not found")
                else:
                    warn("Running pkg-config failed - %s." % e)
                p = None
            if p is not None:
                if p.wait():
                    info("Did not find libzmq via pkg-config:")
                    info(p.stderr.read().decode())
                else:
                    prefix = p.stdout.readline().strip().decode()
                    info("Using zmq-prefix %s (found via pkg-config)." % prefix)

        settings['libraries'].append('zmq')
        # add pthread on freebsd
        if sys.platform.startswith('freebsd'):
            settings['libraries'].append('pthread')
    
        if sys.platform == 'sunos5':
          if platform.architecture()[0] == '32bit':
            settings['extra_link_args'] += ['-m32']
          else:
            settings['extra_link_args'] += ['-m64']
        if prefix:
            settings['include_dirs'] += [pjoin(prefix, 'include')]
            if not bundle_libzmq_dylib:
                if sys.platform == 'sunos5' and platform.architecture()[0] == '64bit':
                    settings['library_dirs'] += [pjoin(prefix, 'lib/amd64')]
                settings['library_dirs'] += [pjoin(prefix, 'lib')]
        else:
            if sys.platform == 'darwin' and os.path.isdir('/opt/local/lib'):
                # allow macports default
                settings['include_dirs'] += ['/opt/local/include']
                settings['library_dirs'] += ['/opt/local/lib']
            if os.environ.get('VIRTUAL_ENV', None):
                # find libzmq installed in virtualenv
                env = os.environ['VIRTUAL_ENV']
                settings['include_dirs'] += [pjoin(env, 'include')]
                settings['library_dirs'] += [pjoin(env, 'lib')]
    
        if bundle_libzmq_dylib:
            # bdist should link against bundled libzmq
            settings['library_dirs'].append('zmq')
            if sys.platform == 'darwin':
                pass
                # unused rpath args for OS X:
                # settings['extra_link_args'] = ['-Wl,-rpath','-Wl,$ORIGIN/..']
            else:
                settings['runtime_library_dirs'] += ['$ORIGIN/..']
        elif sys.platform != 'darwin':
            settings['runtime_library_dirs'] += [
                os.path.abspath(x) for x in settings['library_dirs']
            ]
    
    return settings


#-----------------------------------------------------------------------------
# Extra commands
#-----------------------------------------------------------------------------

class Configure(build_ext):
    """Configure command adapted from h5py"""

    description = "Discover ZMQ version and features"
    
    user_options = build_ext.user_options + [
        ('zmq=', None, "libzmq install prefix"),
        ('build-base=', 'b', "base directory for build library"), # build_base from build
        
    ]
    def initialize_options(self):
        build_ext.initialize_options(self)
        self.zmq = None
        self.build_base = 'build'

    # DON'T REMOVE: distutils demands these be here even if they do nothing.
    def finalize_options(self):
        build_ext.finalize_options(self)
        self.tempdir = pjoin(self.build_temp, 'scratch')
        self.has_run = False
        self.config = discover_settings(self.build_base)
        if self.zmq is not None:
            merge(self.config, config_from_prefix(self.zmq))
        self.init_settings_from_config()
    
    def save_config(self, name, cfg):
        """write config to JSON"""
        save_config(name, cfg, self.build_base)
        # write to zmq.utils.[name].json
        save_config(name, cfg, os.path.join('zmq', 'utils'))
        # also write to build_lib, because we might be run after copying to
        # build_lib has already happened.
        build_lib_utils = os.path.join(self.build_lib, 'zmq', 'utils')
        if os.path.exists(build_lib_utils):
            save_config(name, cfg, build_lib_utils)
    
    def init_settings_from_config(self):
        """set up compiler settings, based on config"""
        cfg = self.config
        
        if cfg['libzmq_extension']:
            settings = bundled_settings()
        else:
            settings = settings_from_prefix(cfg['zmq_prefix'], self.bundle_libzmq_dylib)
    
        if 'have_sys_un_h' not in cfg:
            # don't link against anything when checking for sys/un.h
            minus_zmq = copy.deepcopy(settings)
            try:
                minus_zmq['libraries'] = []
            except Exception:
                pass
            try:
                compile_and_run(self.tempdir,
                    pjoin('buildutils', 'check_sys_un.c'),
                    **minus_zmq
                )
            except Exception as e:
                warn("No sys/un.h, IPC_PATH_MAX_LEN will be undefined: %s" % e)
                cfg['have_sys_un_h'] = False
            else:
                cfg['have_sys_un_h'] = True
        
            self.save_config('config', cfg)
    
        if cfg['have_sys_un_h']:
            settings['define_macros'] = [('HAVE_SYS_UN_H', 1)]
    
        settings.setdefault('define_macros', [])
    
        # include internal directories
        settings.setdefault('include_dirs', [])
        settings['include_dirs'] += [pjoin('zmq', sub) for sub in (
            'utils',
            pjoin('backend', 'cython'),
            'devices',
        )]
        
        for ext in self.distribution.ext_modules:
            if ext.name.startswith('zmq.lib'):
                continue
            for attr, value in settings.items():
                setattr(ext, attr, value)
        
        self.compiler_settings = settings
        self.save_config('compiler', settings)

    def create_tempdir(self):
        self.erase_tempdir()
        os.makedirs(self.tempdir)
        if sys.platform.startswith('win'):
            # fetch libzmq.dll into local dir
            local_dll = pjoin(self.tempdir, 'libzmq.dll')
            if not self.config['zmq_prefix'] 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'")
            
            try:
                shutil.copy(pjoin(self.config['zmq_prefix'], '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):
        try:
            shutil.rmtree(self.tempdir)
        except Exception:
            pass

    @property
    def compiler_type(self):
        compiler = self.compiler
        if compiler is None:
            return get_default_compiler()
        elif isinstance(compiler, str):
            return compiler
        else:
            return compiler.compiler_type
    
    @property
    def cross_compiling(self):
        return self.config['bdist_egg'].get('plat-name', sys.platform) != sys.platform

    @property
    def bundle_libzmq_dylib(self):
        """
        bundle_libzmq_dylib flag for whether external libzmq library will be included in pyzmq:
        only relevant when not building libzmq extension
        """
        if 'bundle_libzmq_dylib' in self.config:
            return self.config['bundle_libzmq_dylib']
        elif (sys.platform.startswith('win') or self.cross_compiling) \
                and not self.config['libzmq_extension']:
            # always bundle libzmq on Windows and cross-compilation
            return True
        elif self.config['zmq_prefix'] and not self.config['libzmq_extension']:
            # only bundle for bdists in sane environments
            return doing_bdist
        else:
            return False

    def check_zmq_version(self):
        """check the zmq version"""
        cfg = self.config
        
        # build test program
        zmq_prefix = self.config['zmq_prefix']
        detected = self.test_build(zmq_prefix, self.compiler_settings)
        # now check the libzmq version
        
        vers = tuple(detected['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_prefix or 'unspecified'))
        
        if vers < target_zmq:
            warn("Detected ZMQ version: %s, but pyzmq targets ZMQ %s." % (
                    vs, v_str(target_zmq))
            )
            warn("libzmq features and fixes introduced after %s will be unavailable." % vs)
            line()
        elif vers >= dev_zmq:
            warn("Detected ZMQ version: %s. Some new features in libzmq may not be exposed by pyzmq." % vs)
            line()

        if sys.platform.startswith('win'):
            # fetch libzmq.dll into local dir
            local_dll = localpath('zmq','libzmq.dll')
            if not zmq_prefix 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'")
            try:
                shutil.copy(pjoin(zmq_prefix, '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 bundle_libsodium_extension(self, libzmq):
        bundledir = "bundled"
        ext_modules = self.distribution.ext_modules
        if ext_modules and any(m.name == 'zmq.libsodium' for m in ext_modules):
            # I've already been run
            return
        
        if not os.path.exists(bundledir):
            os.makedirs(bundledir)
        
        line()
        info("Using bundled libsodium")
        
        # fetch sources for libsodium
        fetch_libsodium(bundledir)
        
        # stage headers
        stage_libsodium_headers(pjoin(bundledir, 'libsodium'))
        
        # construct the Extension
        libsodium_src = pjoin(bundledir, 'libsodium', 'src', 'libsodium')
        exclude = pjoin(libsodium_src, 'crypto_stream', 'salsa20', 'amd64_xmm6') # or ref?
        exclude = pjoin(libsodium_src, 'crypto_scalarmult', 'curve25519', 'donna_c64') # or ref?
        
        libsodium_sources = [pjoin('buildutils', 'initlibsodium.c')]
        
        for dir,subdirs,files in os.walk(libsodium_src):
            if dir.startswith(exclude):
                continue
            for f in files:
                if f.endswith('.c'):
                    libsodium_sources.append(pjoin(dir, f))
        
        libsodium = Extension(
            'zmq.libsodium',
            sources = libsodium_sources,
            include_dirs = [
                pjoin(libsodium_src, 'include'),
                pjoin(libsodium_src, 'include', 'sodium'),
            ],
        )
        # register the Extension
        self.distribution.ext_modules.insert(0, libsodium)
        
        if sys.byteorder == 'little':
            libsodium.define_macros.append(("NATIVE_LITTLE_ENDIAN", 1))
        else:
            libsodium.define_macros.append(("NATIVE_BIG_ENDIAN", 1))
        
        # tell libzmq about libsodium
        libzmq.define_macros.append(("HAVE_LIBSODIUM", 1))
        libzmq.include_dirs.extend(libsodium.include_dirs)
        
        
    
    def bundle_libzmq_extension(self):
        bundledir = "bundled"
        ext_modules = self.distribution.ext_modules
        if ext_modules and any(m.name == 'zmq.libzmq' for m in ext_modules):
            # I've already been run
            return
        
        line()
        info("Using bundled libzmq")
        
        # fetch sources for libzmq extension:
        if not os.path.exists(bundledir):
            os.makedirs(bundledir)
        
        fetch_libzmq(bundledir)
        
        stage_platform_hpp(pjoin(bundledir, 'zeromq'))
        
        # construct the Extensions:
        libzmq = Extension(
            'zmq.libzmq',
            sources = [pjoin('buildutils', 'initlibzmq.c')] + 
                        glob(pjoin(bundledir, 'zeromq', 'src', '*.cpp')),
            include_dirs = [
                pjoin(bundledir, 'zeromq', 'include'),
            ],
        )
        
        # register the extension:
        self.distribution.ext_modules.insert(0, libzmq)
        
        if sys.platform.startswith('win'):
            # include defines from zeromq msvc project:
            libzmq.define_macros.append(('FD_SETSIZE', 1024))
            libzmq.define_macros.append(('DLL_EXPORT', 1))
            
            # When compiling the C++ code inside of libzmq itself, we want to
            # avoid "warning C4530: C++ exception handler used, but unwind
            # semantics are not enabled. Specify /EHsc".
            if self.compiler_type == 'msvc':
                libzmq.extra_compile_args.append('/EHsc')
            elif self.compiler_type == 'mingw32':
                libzmq.define_macros.append(('ZMQ_HAVE_MINGW32', 1))

            # And things like sockets come from libraries that must be named.

            libzmq.libraries.extend(['rpcrt4', 'ws2_32', 'advapi32'])
        else:
            libzmq.include_dirs.append(bundledir)
            
            # check if we need to link against Realtime Extensions library
            cc = new_compiler(compiler=self.compiler_type)
            cc.output_dir = self.build_temp
            if not sys.platform.startswith(('darwin', 'freebsd')):
                line()
                info("checking for timer_create")
                if not cc.has_function('timer_create'):
                    info("no timer_create, linking librt")
                    libzmq.libraries.append('rt')
                else:
                    info("ok")
                
                if pypy:
                    # seem to need explicit libstdc++ on linux + pypy
                    # not sure why
                    libzmq.libraries.append("stdc++")
        
            # On non-Windows, also bundle libsodium:
            self.bundle_libsodium_extension(libzmq)
        
        # update other extensions, with bundled settings
        self.config['libzmq_extension'] = True
        self.init_settings_from_config()
        self.save_config('config', self.config)
        
    
    def fallback_on_bundled(self):
        """Couldn't build, fallback after waiting a while"""
        
        line()
        
        warn('\n'.join([
        "Failed to build or run libzmq detection test.",
        "",
        "If you expected pyzmq to link against an installed libzmq, please check to make sure:",
        "",
        "    * You have a C compiler installed",
        "    * A development version of Python is installed (including headers)",
        "    * A development version of ZMQ >= %s is installed (including headers)" % v_str(min_zmq),
        "    * If ZMQ is not in a default location, supply the argument --zmq=<path>",
        "    * If you did recently install ZMQ to a default location,",
        "      try rebuilding the ld cache with `sudo ldconfig`",
        "      or specify zmq's location with `--zmq=/usr/local`",
        "",
        ]))
        
        info('\n'.join([
            "You can skip all this detection/waiting nonsense if you know",
            "you want pyzmq to bundle libzmq as an extension by passing:",
            "",
            "    `--zmq=bundled`",
            "",
            "I will now try to build libzmq as a Python extension",
            "unless you interrupt me (^C) in the next 10 seconds...",
            "",
        ]))
        
        for i in range(10,0,-1):
            sys.stdout.write('\r%2i...' % i)
            sys.stdout.flush()
            time.sleep(1)
        
        info("")
        
        return self.bundle_libzmq_extension()
        
    
    def test_build(self, prefix, settings):
        """do a test build ob libzmq"""
        self.create_tempdir()
        settings = settings.copy()
        if self.bundle_libzmq_dylib and not sys.platform.startswith('win'):
            # rpath slightly differently here, because libzmq not in .. but ../zmq:
            settings['library_dirs'] = ['zmq']
            if sys.platform == 'darwin':
                pass
                # unused rpath args for OS X:
                # settings['extra_link_args'] = ['-Wl,-rpath','-Wl,$ORIGIN/../zmq']
            else:
                settings['runtime_library_dirs'] = [ os.path.abspath(pjoin('.', 'zmq')) ]
        
        line()
        info("Configure: Autodetecting ZMQ settings...")
        info("    Custom ZMQ dir:       %s" % prefix)
        try:
            detected = detect_zmq(self.tempdir, compiler=self.compiler_type, **settings)
        finally:
            self.erase_tempdir()
        
        info("    ZMQ version detected: %s" % v_str(detected['vers']))
        
        return detected
    

    def finish_run(self):
        self.save_config('config', self.config)
        line()
    
    def run(self):
        cfg = self.config
        
        if cfg['libzmq_extension']:
            self.bundle_libzmq_extension()
            self.finish_run()
            return
        
        # When cross-compiling and zmq is given explicitly, we can't testbuild
        # (as we can't testrun the binary), we assume things are alright.
        if cfg['skip_check_zmq'] or self.cross_compiling:
            warn("Skipping zmq version check")
            self.finish_run()
            return
        
        zmq_prefix = cfg['zmq_prefix']
        # There is no available default on Windows, so start with fallback unless
        # zmq was given explicitly, or libzmq extension was explicitly prohibited.
        if sys.platform.startswith("win") and \
                not cfg['no_libzmq_extension'] and \
                not zmq_prefix:
            self.fallback_on_bundled()
            self.finish_run()
            return
        
        if zmq_prefix and self.bundle_libzmq_dylib and not sys.platform.startswith('win'):
            copy_and_patch_libzmq(zmq_prefix, 'libzmq'+lib_ext)
        
        # first try with given config or defaults
        try:
            self.check_zmq_version()
        except Exception as e:
            # print the error as distutils would if we let it raise:
            info("\nerror: %s\n" % e)
        else:
            self.finish_run()
            return
        
        # try fallback on /usr/local on *ix if no prefix is given
        if not zmq_prefix and not sys.platform.startswith('win'):
            info("Failed with default libzmq, trying again with /usr/local")
            time.sleep(1)
            zmq_prefix = cfg['zmq_prefix'] = '/usr/local'
            self.init_settings_from_config()
            try:
                self.check_zmq_version()
            except Exception as e:
                # print the error as distutils would if we let it raise:
                info("\nerror: %s\n" % e)
            else:
                # if we get here the second run succeeded, so we need to update compiler
                # settings for the extensions with /usr/local prefix
                self.finish_run()
                return
        
        # finally, fallback on bundled
        
        if cfg['no_libzmq_extension']:
            fatal("Falling back on bundled libzmq,"
            " but setup.cfg has explicitly prohibited building the libzmq extension."
            )
        
        self.fallback_on_bundled()
        
        self.finish_run()


class FetchCommand(Command):
    """Fetch libzmq sources, that's it."""
    
    description = "Fetch libzmq sources into bundled/zeromq"
    
    user_options = [ ]
    
    def initialize_options(self):
        pass
    
    def finalize_options(self):
        pass
    
    def run(self):
        # fetch sources for libzmq extension:
        bundledir = "bundled"
        if os.path.exists(bundledir):
            info("Scrubbing directory: %s" % bundledir)
            shutil.rmtree(bundledir)
        if not os.path.exists(bundledir):
            os.makedirs(bundledir)
        fetch_libsodium(bundledir)
        fetch_libzmq(bundledir)
        for tarball in glob(pjoin(bundledir, '*.tar.gz')):
            os.remove(tarball)
        


class TestCommand(Command):
    """Custom distutils command to run the test suite."""

    description = "Test PyZMQ (must have been built inplace: `setup.py build_ext --inplace`)"

    user_options = [ ]

    def initialize_options(self):
        self._dir = os.getcwd()

    def finalize_options(self):
        pass
    
    def run_nose(self):
        """Run the test suite with nose."""
        nose = 'nose.__main__' if sys.version_info < (2,7) else 'nose'
        if subprocess.call([sys.executable, '-m', nose, '-vvx', pjoin(self._dir, 'zmq', 'tests')]):
            sys.exit(1)
    
    def run_unittest(self):
        """Finds all the tests modules in zmq/tests/ and runs them."""
        testfiles = [ ]
        for t in glob(pjoin(self._dir, 'zmq', 'tests', '*.py')):
            name = splitext(basename(t))[0]
            if name.startswith('test_'):
                testfiles.append('.'.join(
                    ['zmq.tests', name])
                )
        tests = TestLoader().loadTestsFromNames(testfiles)
        t = TextTestRunner(verbosity = 2)
        t.run(tests)
    
    def run(self):
        """Run the test suite, with nose, or unittest if nose is unavailable"""
        # crude check for inplace build:
        try:
            import zmq
        except ImportError:
            print_exc()
            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)
        
        info("Testing pyzmq-%s with libzmq-%s" % (zmq.pyzmq_version(), zmq.zmq_version()))
        
        if nose is None:
            warn("nose unavailable, falling back on unittest. Skipped tests will appear as ERRORs.")
            return self.run_unittest()
        else:
            return self.run_nose()

class GitRevisionCommand(Command):
    """find the current git revision and add it to zmq.sugar.version.__revision__"""
    
    description = "Store current git revision in version.py"
    
    user_options = [ ]
    
    def initialize_options(self):
        self.version_py = pjoin('zmq','sugar','version.py')
    
    def run(self):
        try:
            p = Popen('git log -1'.split(), stdin=PIPE, stdout=PIPE, stderr=PIPE)
        except IOError:
            warn("No git found, skipping git revision")
            return
        
        if p.wait():
            warn("checking git branch failed")
            info(p.stderr.read())
            return
        
        line = p.stdout.readline().decode().strip()
        if not line.startswith('commit'):
            warn("bad commit line: %r" % line)
            return
        
        rev = line.split()[-1]
        
        # now that we have the git revision, we can apply it to version.py
        with open(self.version_py) as f:
            lines = f.readlines()
        
        for i,line in enumerate(lines):
            if line.startswith('__revision__'):
                lines[i] = "__revision__ = '%s'\n"%rev
                break
        with open(self.version_py, 'w') as f:
            f.writelines(lines)
    
    def finalize_options(self):
        pass

class CleanCommand(Command):
    """Custom distutils command to clean the .so and .pyc files."""
    user_options = [('all', 'a',
         "remove all build output, not just temporary by-products")
    ]

    boolean_options = ['all']

    def initialize_options(self):
        self.all = None

    def finalize_options(self):
        pass

    def run(self):
        self._clean_me = []
        self._clean_trees = []
        
        for d in ('build', 'dist', 'conf'):
            if os.path.exists(d):
                self._clean_trees.append(d)
        
        for root, dirs, files in os.walk('buildutils'):
            if any(root.startswith(pre) for pre in self._clean_trees):
                continue
            for f in files:
                if os.path.splitext(f)[-1] == '.pyc':
                    self._clean_me.append(pjoin(root, f))
            
            if '__pycache__' in dirs:
                self._clean_trees.append(pjoin(root, '__pycache__'))

        for root, dirs, files in os.walk('zmq'):
            if any(root.startswith(pre) for pre in self._clean_trees):
                continue
            
            for f in files:
                if os.path.splitext(f)[-1] in ('.pyc', '.so', '.o', '.pyd', '.json'):
                    self._clean_me.append(pjoin(root, f))

            for d in dirs:
                if d == '__pycache__':
                    self._clean_trees.append(pjoin(root, d))
        
        # remove generated cython files
        if self.all:
            for root, dirs, files in os.walk(pjoin('zmq', 'backend', 'cython')):
                 if os.path.splitext(f)[-1] == '.c':
                     self._clean_me.append(pjoin(root, f))
        
        bundled = glob(pjoin('zmq', 'libzmq*'))
        self._clean_me.extend([ b for b in bundled if b not in _clean_me ])
        
        for clean_me in self._clean_me:
            print("removing %s" % clean_me)
            try:
                os.unlink(clean_me)
            except Exception as e:
                print(e, file=sys.stderr)
        for clean_tree in self._clean_trees:
            print("removing %s/" % clean_tree)
            try:
                shutil.rmtree(clean_tree)
            except Exception as e:
                print(e, file=sys.stderr)


class CheckSDist(sdist):
    """Custom sdist that ensures Cython has compiled all pyx files to c."""

    def initialize_options(self):
        sdist.initialize_options(self)
        self._pyxfiles = []
        for root, dirs, files in os.walk('zmq'):
            for f in files:
                if f.endswith('.pyx'):
                    self._pyxfiles.append(pjoin(root, f))
    def run(self):
        self.run_command('fetch_libzmq')
        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):
    """Subclass build_ext to get clearer report if Cython is necessary."""
    
    def check_cython_extensions(self, extensions):
        for ext in extensions:
          for src in ext.sources:
            if not os.path.exists(src):
                fatal("""Cython-generated file '%s' not found.
                Cython >= 0.16 is required to compile pyzmq from a development branch.
                Please install Cython or download a release package of pyzmq.
                """%src)
    
    def build_extensions(self):
        self.check_cython_extensions(self.extensions)
        self.check_extensions_list(self.extensions)
        
        if self.compiler.compiler_type == 'mingw32':
            customize_mingw(self.compiler)
        
        for ext in self.extensions:
            self.build_extension(ext)
    
    def build_extension(self, ext):
        build_ext.build_extension(self, ext)
        ext_path = self.get_ext_fullpath(ext.name)
        patch_lib_paths(ext_path, self.compiler.library_dirs)
    
    def run(self):
        # check version, to prevent confusing undefined constant errors
        self.distribution.run_command('configure')
        build_ext.run(self)


class ConstantsCommand(Command):
    """Rebuild templated files for constants
    
    To be run after adding new constants to `utils/constant_names`.
    """
    user_options = []
    def initialize_options(self):
        return 
    
    def finalize_options(self):
        pass
    
    def run(self):
        from buildutils.constants import render_constants
        render_constants()

#-----------------------------------------------------------------------------
# Extensions
#-----------------------------------------------------------------------------

cmdclass = {'test':TestCommand, 'clean':CleanCommand, 'revision':GitRevisionCommand,
            'configure': Configure, 'fetch_libzmq': FetchCommand,
            'sdist': CheckSDist, 'constants': ConstantsCommand,
        }

if 'bdist_wheel' in sys.argv and sys.platform == 'darwin':
    from wheel.bdist_wheel import bdist_wheel
    
    class bdist_wheel_mac_tag(bdist_wheel):
        """add 'current' platform tags to wheels
        
        A 10.6-intel wheel works on all 10.X >= 10.6 and arch in 32,64,intel.
        
        Since that would produce a ludicrous filename, just add the two most common:
        
        - current-intel
        - current-x86_64
        
        partial workaround for pypa/pip#1465
        """
        def get_tag(self):
            import platform
            impl, abi, plat = bdist_wheel.get_tag(self)
            plat_tag_re = re.compile(r'macosx_(\d+)_(\d+)_(.+)')
            m = plat_tag_re.match(plat)
            if m:
                plat_tags = [plat]
                major, minor, arch = m.groups()
                arches = [arch]
                if arch == 'intel':
                    arches.append('x86_64')
                host_list = re.findall('\d+', platform.mac_ver()[0])
                host = (int(host_list[0]), int(host_list[1]))
                host_s = '%s_%s' % tuple(host_list[:2])
                target = (int(major), int(minor))
                if host > target or len(arches) > 1:
                    for arch in arches:
                        plat_tags.append('macosx_%s_%s' % (host_s, arch))
                
                plat = '.'.join(sorted(set(plat_tags)))
            return (impl, abi, plat)
    
    cmdclass['bdist_wheel'] = bdist_wheel_mac_tag
    
    
def makename(path, ext):
    return os.path.abspath(pjoin('zmq', *path)) + ext

pxd = lambda *path: makename(path, '.pxd')
pxi = lambda *path: makename(path, '.pxi')
pyx = lambda *path: makename(path, '.pyx')
dotc = lambda *path: makename(path, '.c')

libzmq = pxd('backend', 'cython', 'libzmq')
buffers = pxd('utils', 'buffers')
message = pxd('backend', 'cython', 'message')
context = pxd('backend', 'cython', 'context')
socket = pxd('backend', 'cython', 'socket')
utils = pxd('backend', 'cython', 'utils')
checkrc = pxd('backend', 'cython', 'checkrc')
monqueue = pxd('devices', 'monitoredqueue')

submodules = {
    'backend.cython' : {'constants': [libzmq, pxi('backend', 'cython', 'constants')],
            'error':[libzmq, checkrc],
            '_poll':[libzmq, socket, context, checkrc],
            'utils':[libzmq, utils, checkrc],
            'context':[context, libzmq, checkrc],
            'message':[libzmq, buffers, message, checkrc],
            'socket':[context, message, socket, libzmq, buffers, checkrc],
            '_device':[libzmq, socket, context, checkrc],
            '_version':[libzmq],
    },
    'devices' : {
            'monitoredqueue':[buffers, libzmq, monqueue, socket, context, checkrc],
    },
}

try:
    import Cython
    if V(Cython.__version__) < V('0.16'):
        raise ImportError("Cython >= 0.16 required, found %s" % Cython.__version__)
    from Cython.Distutils import build_ext as build_ext_c
    cython=True
except Exception:
    cython=False
    suffix = '.c'
    cmdclass['build_ext'] = CheckingBuildExt
    
    class MissingCython(Command):
        
        user_options = []
        
        def initialize_options(self):
            pass
        
        def finalize_options(self):
            pass
        
        def run(self):
            try:
                import Cython
            except ImportError:
                warn("Cython is missing")
            else:
                cv = getattr(Cython, "__version__", None)
                if cv is None or V(cv) < V('0.16'):
                    warn(
                        "Cython >= 0.16 is required for compiling Cython sources, "
                        "found: %s" % (cv or "super old")
                    )
    cmdclass['cython'] = MissingCython
else:
    
    suffix = '.pyx'
    
    class CythonCommand(build_ext_c):
        """Custom distutils command subclassed from Cython.Distutils.build_ext
        to compile pyx->c, and stop there. All this does is override the 
        C-compile method build_extension() with a no-op."""
        
        description = "Compile Cython sources to C"
        
        def build_extension(self, ext):
            pass
    
    class zbuild_ext(build_ext_c):
        
        def build_extensions(self):
            if self.compiler.compiler_type == 'mingw32':
                customize_mingw(self.compiler)
            return build_ext_c.build_extensions(self)
        
        def build_extension(self, ext):
            build_ext_c.build_extension(self, ext)
            ext_path = self.get_ext_fullpath(ext.name)
            patch_lib_paths(ext_path, self.compiler.library_dirs)
        
        def run(self):
            self.distribution.run_command('configure')
            
            return build_ext_c.run(self)
    
    cmdclass['cython'] = CythonCommand
    cmdclass['build_ext'] =  zbuild_ext

extensions = []
for submod, packages in submodules.items():
    for pkg in sorted(packages):
        sources = [pjoin('zmq', submod.replace('.', os.path.sep), pkg+suffix)]
        if suffix == '.pyx':
            sources.extend(packages[pkg])
        ext = Extension(
            'zmq.%s.%s'%(submod, pkg),
            sources = sources,
            include_dirs=[pjoin('zmq', sub) for sub in ('utils',pjoin('backend', 'cython'),'devices')],
        )
        if suffix == '.pyx' and ext.sources[0].endswith('.c'):
            # undo setuptools stupidly clobbering cython sources:
            ext.sources = sources
        extensions.append(ext)

if pypy:
    # add dummy extension, to ensure build_ext runs
    dummy_ext = Extension('dummy', sources=[])
    extensions = [dummy_ext]
    
    bld_ext = cmdclass['build_ext']
    class pypy_build_ext(bld_ext):
        """hack to build pypy extension only after building bundled libzmq
        
        otherwise it will fail when libzmq is bundled.
        """
        def build_extensions(self):
            self.extensions.remove(dummy_ext)
            bld_ext.build_extensions(self)
            # build ffi extension after bundled libzmq,
            # because it may depend on linking it
            here = os.getcwd()
            if self.inplace:
                sys.path.insert(0, '')
            else:
                sys.path.insert(0, self.build_lib)
            try:
                from zmq.backend.cffi import ffi
            except ImportError as e:
                warn("Couldn't get CFFI extension: %s" % e)
            else:
                ext = ffi.verifier.get_extension()
                if not ext.name.startswith('zmq.'):
                    ext.name = 'zmq.backend.cffi.' + ext.name
                self.extensions.append(ext)
                self.build_extension(ext)
            finally:
                sys.path.pop(0)

    
    # How many build_ext subclasses is this? 5? Gross.
    cmdclass['build_ext'] = pypy_build_ext


package_data = {'zmq': ['*.pxd'],
                'zmq.backend.cython': ['*.pxd'],
                'zmq.backend.cffi': ['*.h', '*.c'],
                'zmq.devices': ['*.pxd'],
                'zmq.utils': ['*.pxd', '*.h', '*.json'],
}

package_data['zmq'].append('libzmq'+lib_ext)

def extract_version():
    """extract pyzmq version from sugar/version.py, so it's not multiply defined"""
    with open(pjoin('zmq', 'sugar', 'version.py')) as f:
        while True:
            line = f.readline()
            if line.startswith('VERSION'):
                lines = []
                while line and not line.startswith('def'):
                    lines.append(line)
                    line = f.readline()
                break
    exec(''.join(lines), globals())
    return __version__

def find_packages():
    """adapted from IPython's setupbase.find_packages()"""
    packages = []
    for dir,subdirs,files in os.walk('zmq'):
        package = dir.replace(os.path.sep, '.')
        if '__init__.py' not in files:
            # not a package
            continue
        packages.append(package)
    return packages

#-----------------------------------------------------------------------------
# Main setup
#-----------------------------------------------------------------------------

long_desc = \
"""
PyZMQ is the official Python binding for the ZeroMQ Messaging Library (http://www.zeromq.org).
"""

setup_args = dict(
    name = "pyzmq",
    version = extract_version(),
    packages = find_packages(),
    ext_modules = extensions,
    package_data = package_data,
    author = "Brian E. Granger, Min Ragan-Kelley",
    author_email = "zeromq-dev@lists.zeromq.org",
    url = 'http://github.com/zeromq/pyzmq',
    download_url = 'http://github.com/zeromq/pyzmq/releases',
    description = "Python bindings for 0MQ",
    long_description = long_desc, 
    license = "LGPL+BSD",
    cmdclass = cmdclass,
    classifiers = [
        'Development Status :: 5 - Production/Stable',
        'Intended Audience :: Developers',
        'Intended Audience :: Science/Research',
        'Intended Audience :: System Administrators',
        'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
        'License :: OSI Approved :: BSD License',
        'Operating System :: MacOS :: MacOS X',
        'Operating System :: Microsoft :: Windows',
        'Operating System :: POSIX',
        'Topic :: System :: Networking',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.6',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.2',
        'Programming Language :: Python :: 3.3',
    ],
)
if 'setuptools' in sys.modules and pypy:
    setup_args['install_requires'] = [
        'py',
        'cffi',
    ]

setup(**setup_args)

