pyzmq/setup.py

1395 lines
47 KiB
Python
Executable File

#!/usr/bin/env python
#
# -----------------------------------------------------------------------------
# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.
#
# The `configure` subcommand is copied and adapted 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>
# -----------------------------------------------------------------------------
import copy
import errno
import os
import platform
import shutil
import subprocess
import sys
import time
from contextlib import contextmanager
from glob import glob
from os.path import basename
from os.path import join as pjoin
from subprocess import PIPE, CalledProcessError, Popen, check_call
from sysconfig import get_config_var
from traceback import print_exc
from packaging.version import Version as V
from setuptools import Command, setup
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command.build_ext import build_ext
from setuptools.command.sdist import sdist
from setuptools.errors import OptionError
from setuptools.extension import Extension
# local script imports:
sys.path.insert(0, os.path.dirname(__file__))
from buildutils import (
bundled_version,
compile_and_forget,
config_from_prefix,
customize_mingw,
detect_zmq,
discover_settings,
fatal,
fetch_libzmq,
fetch_libzmq_dll,
info,
line,
localpath,
locate_vcredist_dir,
merge,
patch_lib_paths,
save_config,
stage_platform_hpp,
v_str,
warn,
)
# -----------------------------------------------------------------------------
# Flags
# -----------------------------------------------------------------------------
# name of the libzmq library - can be changed by --libzmq <name>
libzmq_name = "libzmq"
doing_bdist = any(arg.startswith("bdist") for arg in sys.argv[1:])
pypy = platform.python_implementation() == 'PyPy'
# reference points for zmq compatibility
min_legacy_zmq = (2, 1, 4)
min_good_zmq = (3, 2)
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'
# 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
for idx, arg in enumerate(list(sys.argv)):
if arg.startswith('--libzmq='):
sys.argv.remove(arg)
libzmq_name = arg.split("=", 1)[1]
if arg == '--enable-drafts':
sys.argv.remove(arg)
os.environ['ZMQ_DRAFT_API'] = '1'
if not sys.platform.startswith('win'):
cxx_flags = os.getenv("CXXFLAGS", "")
if "-std" not in cxx_flags:
cxx_flags = "-std=c++11 " + cxx_flags
os.environ["CXXFLAGS"] = cxx_flags
if cxx_flags:
# distutils doesn't support $CXXFLAGS
cxx = os.getenv("CXX", get_config_var("CXX"))
# get_config_var is broken on some old versions of pypy, add a fallback
if cxx is None:
cxx = "c++ -pthread"
os.environ["CXX"] = cxx + " " + cxx_flags
# -----------------------------------------------------------------------------
# Configuration (adapted from h5py: https://www.h5py.org/)
# -----------------------------------------------------------------------------
# --- compiler settings -------------------------------------------------
def bundled_settings(cmd):
"""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:
if sys.version_info < (3, 9, 2):
# bpo-39825: EXT_SUFFIX is wrong from sysconfig prior to 3.9.2 / 3.8.7
import distutils.sysconfig
ext_suffix = distutils.sysconfig.get_config_var("EXT_SUFFIX")
else:
ext_suffix = get_config_var("EXT_SUFFIX")
suffix = os.path.splitext(ext_suffix)[0]
print(f"DEBUG EXT_SUFFIX {suffix!r} {ext_suffix!r}")
settings['libraries'].append(libzmq_name + suffix)
settings['library_dirs'].append(pjoin(cmd.build_temp, 'buildutils'))
return settings
def check_pkgconfig():
"""pull compile / link flags from pkg-config if present."""
pcfg = None
zmq_config = None
try:
pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config')
check_call([pkg_config, '--exists', 'libzmq'])
# this would arguably be better with --variable=libdir /
# --variable=includedir, but would require multiple calls
pcfg = Popen(
[pkg_config, '--libs', '--cflags', 'libzmq'], stdout=subprocess.PIPE
)
except OSError as osexception:
if osexception.errno == errno.ENOENT:
info('pkg-config not found')
else:
warn("Running pkg-config failed - %s." % osexception)
except CalledProcessError:
info("Did not find libzmq via pkg-config.")
if pcfg is not None:
output, _ = pcfg.communicate()
output = output.decode('utf8', 'replace')
bits = output.strip().split()
zmq_config = {'library_dirs': [], 'include_dirs': [], 'libraries': []}
for tok in bits:
if tok.startswith("-L"):
zmq_config['library_dirs'].append(tok[2:])
if tok.startswith("-I"):
zmq_config['include_dirs'].append(tok[2:])
if tok.startswith("-l"):
zmq_config['libraries'].append(tok[2:])
info("Settings obtained from pkg-config: %r" % zmq_config)
return zmq_config
def _add_rpath(settings, path):
"""Add rpath to settings
Implemented here because setuptools runtime_library_dirs doesn't do anything on darwin
"""
if sys.platform == 'darwin':
settings['extra_link_args'].extend(['-Wl,-rpath', '-Wl,%s' % path])
else:
settings['runtime_library_dirs'].append(path)
def settings_from_prefix(prefix=None):
"""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'):
global libzmq_name
if prefix:
# add prefix itself as well, for e.g. libzmq Windows releases
for include_dir in [pjoin(prefix, 'include'), prefix]:
if os.path.exists(pjoin(include_dir, "zmq.h")):
settings['include_dirs'].append(include_dir)
info(f"Found zmq.h in {include_dir}")
break
else:
warn(f"zmq.h not found in {prefix} or {prefix}/include")
for library_dir in [pjoin(prefix, 'lib'), prefix]:
matches = glob(pjoin(library_dir, f"{libzmq_name}*.dll"))
if matches:
libzmq_path = matches[0]
libzmq_lib, libzmq_dll_name = os.path.split(libzmq_path)
libzmq_name, _ = os.path.splitext(libzmq_dll_name)
info(f"Found {libzmq_path} in {libzmq_lib}")
if libzmq_lib not in os.environ["PATH"].split(os.pathsep):
info(f"Adding {libzmq_lib} to $PATH")
os.environ["PATH"] += os.pathsep + libzmq_lib
settings['library_dirs'].append(library_dir)
break
else:
warn(f"{libzmq_name}.dll not found in {prefix} or {prefix}/lib")
settings['libraries'].append(libzmq_name)
else:
# add pthread on freebsd
if sys.platform.startswith('freebsd'):
settings['libraries'].append('pthread')
if sys.platform.startswith('sunos'):
if platform.architecture()[0] == '32bit':
settings['extra_link_args'] += ['-m32']
else:
settings['extra_link_args'] += ['-m64']
if prefix:
settings['libraries'].append('zmq')
settings['include_dirs'] += [pjoin(prefix, 'include')]
if (
sys.platform.startswith('sunos')
and platform.architecture()[0] == '64bit'
):
settings['library_dirs'] += [pjoin(prefix, 'lib/amd64')]
settings['library_dirs'] += [pjoin(prefix, 'lib')]
else:
# If prefix is not explicitly set, pull it from pkg-config by default.
# this is probably applicable across platforms, but i don't have
# sufficient test environments to confirm
pkgcfginfo = check_pkgconfig()
if pkgcfginfo is not None:
# we can get all the zmq-specific values from pkgconfg
for key, value in pkgcfginfo.items():
settings[key].extend(value)
else:
settings['libraries'].append('zmq')
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')]
for path in settings['library_dirs']:
_add_rpath(settings, os.path.abspath(path))
info(settings)
return settings
class LibZMQVersionError(Exception):
pass
# -----------------------------------------------------------------------------
# Extra commands
# -----------------------------------------------------------------------------
class bdist_egg_disabled(bdist_egg):
"""Disabled version of bdist_egg
Prevents setup.py install from performing setuptools' default easy_install,
which it should never ever do.
"""
def run(self):
sys.exit(
"Aborting implicit building of eggs. Use `pip install .` to install from source."
)
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):
super().initialize_options()
self.zmq = os.environ.get("ZMQ_PREFIX") or None
self.build_base = 'build'
def finalize_options(self):
super().finalize_options()
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))
# ensure vcredist is on PATH in MSVC toolchain
if sys.platform.startswith("win") and 'MSC' in sys.version:
locate_vcredist_dir(self.plat_name)
# need a dummy extension for run to set up a compiler
self.extensions = [Extension("fake", ["unused.c"])]
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(self)
else:
settings = settings_from_prefix(cfg['zmq_prefix'])
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_forget(
self.tempdir,
pjoin('buildutils', 'check_sys_un.c'),
compiler=self.compiler,
**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)
settings.setdefault('define_macros', [])
if cfg['have_sys_un_h']:
settings['define_macros'].append(('HAVE_SYS_UN_H', 1))
if cfg['win_ver']:
# set target minimum Windows version
settings['define_macros'].extend(
[
('WINVER', cfg['win_ver']),
('_WIN32_WINNT', cfg['win_ver']),
]
)
if cfg.get('zmq_draft_api'):
settings['define_macros'].append(('ZMQ_BUILD_DRAFT_API', 1))
use_static_zmq = cfg.get('use_static_zmq', 'False').upper()
if use_static_zmq in ('TRUE', '1'):
settings['define_macros'].append(('ZMQ_STATIC', '1'))
if os.environ.get("PYZMQ_CYTHON_COVERAGE", "") not in {"", "0"}:
settings['define_macros'].append(('CYTHON_TRACE', '1'))
# include internal directories
settings.setdefault('include_dirs', [])
settings['include_dirs'] += [pjoin('zmq', sub) for sub in ('utils',)]
settings.setdefault('libraries', [])
# Explicitly link dependencies, not necessary if zmq is dynamic
if sys.platform.startswith('win'):
settings['libraries'].extend(('ws2_32', 'iphlpapi', 'advapi32'))
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)
def erase_tempdir(self):
try:
shutil.rmtree(self.tempdir)
except Exception:
pass
@property
def compiler_type(self):
return self.compiler.compiler_type
@property
def cross_compiling(self):
return self.config['bdist_egg'].get('plat-name', sys.platform) != sys.platform
def check_zmq_version(self):
"""check the zmq version"""
cfg = self.config
# build test program
zmq_prefix = cfg['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 cfg['allow_legacy_libzmq']:
min_zmq = min_legacy_zmq
else:
min_zmq = min_good_zmq
if vers < min_zmq:
msg = [
"Detected ZMQ version: %s, but require ZMQ >= %s"
% (vs, v_str(min_zmq)),
]
if zmq_prefix:
msg.append(" ZMQ_PREFIX=%s" % zmq_prefix)
if vers >= min_legacy_zmq:
msg.append(
" Explicitly allow legacy zmq by specifying `ZMQ_PREFIX=/zmq/prefix`"
)
raise LibZMQVersionError('\n'.join(msg))
if vers < min_good_zmq:
msg = [
"Detected legacy ZMQ version: %s. It is STRONGLY recommended to use ZMQ >= %s"
% (vs, v_str(min_good_zmq)),
]
if zmq_prefix:
msg.append(" ZMQ_PREFIX=%s" % zmq_prefix)
warn('\n'.join(msg))
elif 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_name + '.dll')
if not zmq_prefix and not os.path.exists(local_dll):
fatal(
"ZMQ directory must be specified on Windows via setup.cfg or 'ZMQ_PREFIX=/path/to/libzmq' env"
)
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'))
sources = [pjoin('buildutils', 'initlibzmq.cpp')]
sources.extend(
[
src
for src in glob(pjoin(bundledir, 'zeromq', 'src', '*.cpp'))
# exclude draft ws transport files
if not os.path.basename(src).startswith(("ws_", "wss_"))
]
)
if sys.platform.startswith("win"): # only compile wepoll on windows...
sources.append(pjoin('bundled', 'zeromq', 'external', 'wepoll', 'wepoll.c'))
includes = [pjoin(bundledir, 'zeromq', 'include')]
if bundled_version < (4, 2, 0):
tweetnacl = pjoin(bundledir, 'zeromq', 'tweetnacl')
tweetnacl_sources = glob(pjoin(tweetnacl, 'src', '*.c'))
randombytes = pjoin(tweetnacl, 'contrib', 'randombytes')
if sys.platform.startswith('win'):
tweetnacl_sources.append(pjoin(randombytes, 'winrandom.c'))
else:
tweetnacl_sources.append(pjoin(randombytes, 'devurandom.c'))
sources += tweetnacl_sources
includes.append(pjoin(tweetnacl, 'src'))
includes.append(randombytes)
else:
# >= 4.2
sources += glob(pjoin(bundledir, 'zeromq', 'src', 'tweetnacl.c'))
# construct the Extensions:
libzmq = Extension(
'zmq.libzmq',
sources=sources,
include_dirs=includes,
)
# register the extension:
# doing this here means we must be run
# before finalize_options in build_ext
self.distribution.ext_modules.insert(0, libzmq)
# use tweetnacl to provide CURVE support
libzmq.define_macros.append(('ZMQ_HAVE_CURVE', 1))
libzmq.define_macros.append(('ZMQ_USE_TWEETNACL', 1))
# set draft flag
if self.config["zmq_draft_api"]:
libzmq.define_macros.append(('ZMQ_BUILD_DRAFT_API', 1))
# select polling subsystem based on platform
if sys.platform == "darwin" or "bsd" in sys.platform:
libzmq.define_macros.append(('ZMQ_USE_KQUEUE', 1))
libzmq.define_macros.append(('ZMQ_IOTHREADS_USE_KQUEUE', 1))
libzmq.define_macros.append(('ZMQ_POLL_BASED_ON_POLL', 1))
elif 'linux' in sys.platform:
libzmq.define_macros.append(('ZMQ_USE_EPOLL', 1))
libzmq.define_macros.append(('ZMQ_IOTHREADS_USE_EPOLL', 1))
libzmq.define_macros.append(('ZMQ_POLL_BASED_ON_POLL', 1))
elif sys.platform.startswith('win'):
libzmq.define_macros.append(('ZMQ_USE_SELECT', 1))
libzmq.define_macros.append(('ZMQ_IOTHREADS_USE_SELECT', 1))
libzmq.define_macros.append(('ZMQ_POLL_BASED_ON_SELECT', 1))
else:
# this may not be sufficiently precise
libzmq.define_macros.append(('ZMQ_USE_POLL', 1))
libzmq.define_macros.append(('ZMQ_IOTHREADS_USE_POLL', 1))
libzmq.define_macros.append(('ZMQ_POLL_BASED_ON_POLL', 1))
if sys.platform.startswith('win'):
# include defines from zeromq msvc project:
libzmq.define_macros.append(('FD_SETSIZE', 16384))
libzmq.define_macros.append(('DLL_EXPORT', 1))
libzmq.define_macros.append(('_CRT_SECURE_NO_WARNINGS', 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.compiler_type == 'msvc':
libzmq.extra_compile_args.append('/EHsc')
elif self.compiler.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', 'iphlpapi'])
else:
libzmq.include_dirs.append(bundledir)
# check if we need to link against Realtime Extensions library
if not sys.platform.startswith(('darwin', 'freebsd')):
line()
info("checking for timer_create")
if not self.compiler.has_function('timer_create'):
info("no timer_create, linking librt")
libzmq.libraries.append('rt')
else:
info("ok")
# copy the header files to the source tree.
bundledincludedir = pjoin('zmq', 'include')
if not os.path.exists(bundledincludedir):
os.makedirs(bundledincludedir)
if not os.path.exists(pjoin(self.build_lib, bundledincludedir)):
os.makedirs(pjoin(self.build_lib, bundledincludedir))
for header in glob(pjoin(bundledir, 'zeromq', 'include', '*.h')):
shutil.copyfile(header, pjoin(bundledincludedir, basename(header)))
shutil.copyfile(
header, pjoin(self.build_lib, bundledincludedir, basename(header))
)
# 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(
[
"Couldn't find an acceptable libzmq on the system.",
"",
"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_good_zmq),
" * If ZMQ is not in a default location, specify the env ZMQ_PREFIX=<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_PREFIX=/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_PREFIX=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()
line()
info("Configure: Autodetecting ZMQ settings...")
info(" Custom ZMQ dir: %s" % prefix)
try:
detected = detect_zmq(self.tempdir, compiler=self.compiler, **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 build_extensions(self):
"""Need an empty build_extensions so that .run() gives us a configured compiler"""
def run(self):
# super().run() is what sets up self.compiler
super().run()
self.init_settings_from_config()
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
# first try with given config or defaults
try:
self.check_zmq_version()
except LibZMQVersionError as e:
info("\nBad libzmq version: %s\n" % e)
except Exception as e:
# print the error as setuptools 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 LibZMQVersionError as e:
info("\nBad libzmq version: %s\n" % e)
except Exception as e:
# print the error as setuptools 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 config has explicitly prohibited building the libzmq extension."
)
self.fallback_on_bundled()
self.finish_run()
class FetchCommand(Command):
"""Fetch libzmq, that's it."""
description = "Fetch libzmq sources or dll"
user_options = [
('dll', None, "Fetch binary dll (Windows only)"),
]
def initialize_options(self):
self.dll = False
def finalize_options(self):
pass
def run(self):
# fetch sources for libzmq extension:
if self.dll:
self.fetch_libzmq_dll()
else:
self.fetch_libzmq_src()
def fetch_libzmq_dll(self):
libdir = "libzmq-dll"
if os.path.exists(libdir):
info("Scrubbing directory: %s" % libdir)
shutil.rmtree(libdir)
if not os.path.exists(libdir):
os.makedirs(libdir)
fetch_libzmq_dll(libdir)
for archive in glob(pjoin(libdir, '*.zip')):
os.remove(archive)
def fetch_libzmq_src(self):
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_libzmq(bundledir)
class TestCommand(Command):
"""Custom setuptools command to run the test suite."""
description = "DEPRECATED, use pytest"
user_options = []
def initialize_options(self):
self._dir = os.getcwd()
def finalize_options(self):
pass
def run(self):
"""Run the test suite with pytest"""
warn("Running pyzmq's tests with `setup.py test` is deprecated. Use `pytest`.")
time.sleep(10)
# 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(f"Testing pyzmq-{zmq.pyzmq_version()} with libzmq-{zmq.zmq_version()}")
p = Popen([sys.executable, '-m', 'pytest', '-v', os.path.join('zmq', 'tests')])
p.wait()
sys.exit(p.returncode)
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 OSError:
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 setuptools 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):
_clean_me = []
_clean_trees = []
for d in ('build', 'dist', 'conf'):
if os.path.exists(d):
_clean_trees.append(d)
for root, dirs, files in os.walk('buildutils'):
if any(root.startswith(pre) for pre in _clean_trees):
continue
for f in files:
if os.path.splitext(f)[-1] == '.pyc':
_clean_me.append(pjoin(root, f))
if '__pycache__' in dirs:
_clean_trees.append(pjoin(root, '__pycache__'))
for root, dirs, files in os.walk('zmq'):
if any(root.startswith(pre) for pre in _clean_trees):
continue
for f in files:
if os.path.splitext(f)[-1] in ('.pyc', '.so', '.o', '.pyd', '.json'):
_clean_me.append(pjoin(root, f))
# remove generated cython files
if self.all:
for f in files:
f2 = os.path.splitext(f)
if f2[1] == '.c' and os.path.isfile(
os.path.join(root, f2[0]) + '.pyx'
):
_clean_me.append(pjoin(root, f))
for d in dirs:
if d == '__pycache__':
_clean_trees.append(pjoin(root, d))
bundled = glob(pjoin('zmq', 'libzmq*'))
_clean_me.extend([b for b in bundled if b not in _clean_me])
bundled_headers = glob(pjoin('zmq', 'include', '*.h'))
_clean_me.extend([h for h in bundled_headers if h not in _clean_me])
for clean_me in _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 _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)
@contextmanager
def use_cxx(compiler):
"""use C++ compiler in this context
used in fix_cxx which detects when C++ should be used
"""
compiler_so_save = compiler.compiler_so[:]
compiler_so_cxx = compiler.compiler_cxx + compiler.compiler_so[1:]
# actually use CXX compiler
compiler.compiler_so = compiler_so_cxx
try:
yield
finally:
# restore original state
compiler.compiler_so = compiler_so_save
@contextmanager
def fix_cxx(compiler, extension):
"""Fix C++ compilation to use C++ compiler
See https://bugs.python.org/issue1222585 for Python support for C++,
which apparently doesn't exist and only works by accident.
"""
if compiler.detect_language(extension.sources) != "c++":
# no c++, nothing to do
yield
return
_compile_save = compiler._compile
def _compile_cxx(obj, src, ext, *args, **kwargs):
if compiler.language_map.get(ext) == "c++":
with use_cxx(compiler):
_compile_save(obj, src, ext, *args, **kwargs)
else:
_compile_save(obj, src, ext, *args, **kwargs)
compiler._compile = _compile_cxx
try:
yield
finally:
compiler._compile = _compile_save
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 >= %s is required to compile pyzmq from a development branch.
Please install Cython or download a release package of pyzmq.
"""
% (src, min_cython_version)
)
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):
with fix_cxx(self.compiler, ext):
super().build_extension(ext)
ext_path = self.get_ext_fullpath(ext.name)
patch_lib_paths(ext_path, self.compiler.library_dirs)
def finalize_options(self):
# this is normally done by super().finalize_options() but it
# needs to be done *before* we call configure if cython,
# numpy and pythran are all installed; and we can't call it
# after finalize_options() because that breaks pypy builds
# on Windows; logic copied from distutils/command/build_ext.py
# https://github.com/zeromq/pyzmq/pull/1872
if isinstance(self.parallel, str):
try:
self.parallel = int(self.parallel)
except ValueError:
raise OptionError("parallel should be an integer")
# check version, to prevent confusing undefined constant errors
self.distribution.run_command("configure")
return super().finalize_options()
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()
cmdclass = {
"bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled,
"clean": CleanCommand,
"configure": Configure,
"constants": ConstantsCommand,
"fetch_libzmq": FetchCommand,
"revision": GitRevisionCommand,
"sdist": CheckSDist,
"test": TestCommand,
}
# -----------------------------------------------------------------------------
# Extensions
# -----------------------------------------------------------------------------
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')
doth = lambda *path: makename(path, '.h')
libzmq = pxd('backend', 'cython', 'libzmq')
buffers = pxd('utils', 'buffers')
message = pxd('backend', 'cython', 'message')
context = pxd('backend', 'cython', 'context')
socket = pxd('backend', 'cython', 'socket')
checkrc = pxd('backend', 'cython', 'checkrc')
monqueue = pxd('devices', 'monitoredqueue')
mutex = doth('utils', 'mutex')
submodules = {
'backend.cython': {
'error': [libzmq, checkrc],
'_poll': [libzmq, socket, context, checkrc],
'utils': [libzmq, checkrc],
'context': [context, libzmq, checkrc],
'message': [libzmq, buffers, message, checkrc, mutex],
'socket': [context, message, socket, libzmq, buffers, checkrc],
'_device': [libzmq, socket, context, checkrc],
'_proxy_steerable': [libzmq, socket, checkrc],
'_version': [libzmq],
},
'devices': {
'monitoredqueue': [buffers, libzmq, monqueue, socket, context, checkrc],
},
}
# require cython 0.29
min_cython_version = "0.29"
cython_language_level = "3str"
try:
import Cython
if V(Cython.__version__) < V(min_cython_version):
raise ImportError(
"Cython >= %s required for cython build, found %s"
% (min_cython_version, Cython.__version__)
)
from Cython.Build import cythonize
from Cython.Distutils.build_ext import new_build_ext as build_ext_cython
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(min_cython_version):
warn(
"Cython >= %s is required for compiling Cython sources, "
"found: %s" % (min_cython_version, cv or Cython)
)
cmdclass['cython'] = MissingCython
else:
suffix = '.pyx'
class CythonCommand(build_ext_cython):
"""Custom setuptools 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_cython):
def build_extensions(self):
if self.compiler.compiler_type == 'mingw32':
customize_mingw(self.compiler)
return super().build_extensions()
def build_extension(self, ext):
with fix_cxx(self.compiler, ext):
super().build_extension(ext)
ext_path = self.get_ext_fullpath(ext.name)
patch_lib_paths(ext_path, self.compiler.library_dirs)
def finalize_options(self):
# this is normally done by super().finalize_options() but it
# needs to be done *before* we call configure if cython,
# numpy and pythran are all installed; and we can't call it
# after finalize_options() because that breaks pypy builds
# on Windows; logic copied from distutils/command/build_ext.py
# https://github.com/zeromq/pyzmq/pull/1872
if isinstance(self.parallel, str):
try:
self.parallel = int(self.parallel)
except ValueError:
raise OptionError("parallel should be an integer")
self.distribution.run_command("configure")
return super().finalize_options()
cmdclass["cython"] = CythonCommand
cmdclass["build_ext"] = zbuild_ext
extensions = []
ext_include_dirs = [pjoin('zmq', sub) for sub in ('utils',)]
ext_kwargs = {
'include_dirs': ext_include_dirs,
}
for submod, packages in submodules.items():
for pkg in sorted(packages):
sources = [pjoin("zmq", submod.replace(".", os.path.sep), pkg + suffix)]
ext = Extension(f"zmq.{submod}.{pkg}", sources=sources, **ext_kwargs)
extensions.append(ext)
if cython:
# set binding so that compiled methods can be inspected
# set language-level to 3str, requires Cython 0.29
cython_directives = {"binding": True, "language_level": "3str"}
if os.environ.get("PYZMQ_CYTHON_COVERAGE", "") not in {"", "0"}:
cython_directives["linetrace"] = True
extensions = cythonize(extensions, compiler_directives=cython_directives)
if pypy:
extensions = []
if pypy or os.environ.get("PYZMQ_BACKEND_CFFI"):
cffi_modules = ['buildutils/build_cffi.py:ffi']
else:
cffi_modules = []
package_data = {
'zmq': ['*.pxd', '*.pyi', '*' + lib_ext, 'py.typed'],
'zmq.backend': ['*.pyi'],
'zmq.backend.cython': ['*.pxd', '*.pxi'],
'zmq.backend.cffi': ['*.h', '*.c'],
'zmq.devices': ['*.pxd'],
'zmq.sugar': ['*.pyi'],
'zmq.tests': ['*.pyx'],
'zmq.utils': ['*.pxd', '*.h', '*.json'],
}
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
# -----------------------------------------------------------------------------
with open('README.md', encoding='utf-8') as f:
long_desc = f.read()
setup_args = dict(
name="pyzmq",
version="25.1.1",
packages=find_packages(),
ext_modules=extensions,
cffi_modules=cffi_modules,
package_data=package_data,
author="Brian E. Granger, Min Ragan-Kelley",
author_email="zeromq-dev@lists.zeromq.org",
url="https://pyzmq.readthedocs.org",
project_urls={
'Source': 'https://github.com/zeromq/pyzmq',
},
description="Python bindings for 0MQ",
long_description=long_desc,
long_description_content_type="text/markdown",
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 :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
zip_safe=False,
python_requires=">=3.6",
install_requires=[
"cffi; implementation_name == 'pypy'",
],
)
setup(**setup_args)