diff --git a/.travis.yml b/.travis.yml index a38e2362..a4cb2b79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,15 @@ matrix: # macOS Python 3 - os: osx env: PYTHON=python3 + + - os: windows + language: shell before_install: - - brew update - - brew outdated python || brew upgrade python + - choco install python3 --version 3.6.8 --no-progress -y + install: + - C:\\Python36\\python -m pip install -r requirements-dev.txt + script: + - C:\\Python36\\python ./bin/run_tests.py install: $PYTHON -m pip install -r requirements-dev.txt diff --git a/README.md b/README.md index b71fdfac..ea6b1128 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,15 @@ What does it do? | | macOS 10.6+ | manylinux i686 | manylinux x86_64 | Windows 32bit | Windows 64bit | |---|---|---|---|---|---| -| Python 2.7 | ✅ | ✅ | ✅ | ✅ | ✅ | -| Python 3.4 | ✅ | ✅ | ✅ | ✅* | ✅* | +| Python 2.7 | ✅ | ✅ | ✅ | ✅² | ✅² | +| Python 3.4 | ✅ | ✅ | ✅ | ✅¹²| ✅¹²| | Python 3.5 | ✅ | ✅ | ✅ | ✅ | ✅ | | Python 3.6 | ✅ | ✅ | ✅ | ✅ | ✅ | | Python 3.7 | ✅ | ✅ | ✅ | ✅ | ✅ | -> \* Not supported on Azure Pipelines +> ¹ Not supported on Azure Pipelines +> +> ² Not supported on Travis - Builds manylinux, macOS and Windows (32 and 64bit) wheels using Azure Pipelines, Travis CI, AppVeyor, and CircleCI - Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate) @@ -32,7 +34,14 @@ What does it do? Usage ----- -`cibuildwheel` currently works **Travis CI** and **CircleCI** to build Linux and Mac wheels, and **AppVeyor** to build Windows wheels. **Azure Pipelines** supports all three. +`cibuildwheel` currently works on **Travis CI** and **Azure Pipelines** to build wheels for all three supported platforms (Linux, macOS, Windows). On **CircleCI** Linux and macOS wheels can be built, and on **AppVeyor** Windows is supported. + +| | Linux | macOS | Windows | +|-----------------|-------|-------|---------| +| Azure Pipelines | ✅ | ✅ | ✅ | +| Travis CI | ✅ | ✅ | ✅ | +| AppVeyor | | | ✅ | +| CircleCI | ✅ | ✅ | | `cibuildwheel` is not intended to run on your development machine. Because it uses system Python from Python.org it will try to install packages globally - not what you expect from a build tool! Instead, isolated CI services like Travis CI, CircleCI, Azure Pipelines and AppVeyor are ideal. diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 516f0d69..67d90827 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -54,6 +54,8 @@ def main(): args = parser.parse_args() + detect_obsolete_options() + if args.platform != 'auto': platform = args.platform else: @@ -63,6 +65,8 @@ def main(): platform = 'linux' elif os.environ.get('TRAVIS_OS_NAME') == 'osx': platform = 'macos' + elif os.environ.get('TRAVIS_OS_NAME') == 'windows': + platform = 'windows' elif 'APPVEYOR' in os.environ: platform = 'windows' elif 'BITRISE_BUILD_NUMBER' in os.environ: @@ -141,11 +145,17 @@ def main(): ) if platform == 'linux': - manylinux1_x86_64_image = os.environ.get('CIBW_MANYLINUX1_X86_64_IMAGE', None) - manylinux1_i686_image = os.environ.get('CIBW_MANYLINUX1_I686_IMAGE', None) + manylinux_x86_64_image = os.environ.get('CIBW_MANYLINUX_X86_64_IMAGE', 'manylinux2010') + manylinux_i686_image = os.environ.get('CIBW_MANYLINUX_I686_IMAGE', 'manylinux2010') + + default_manylinux_images_x86_64 = {'manylinux1': 'quay.io/pypa/manylinux1_x86_64', + 'manylinux2010': 'quay.io/pypa/manylinux2010_x86_64'} + default_manylinux_images_i686 = {'manylinux1': 'quay.io/pypa/manylinux1_i686', + 'manylinux2010': 'quay.io/pypa/manylinux2010_i686'} build_options.update( - manylinux1_images={'x86_64': manylinux1_x86_64_image, 'i686': manylinux1_i686_image}, + manylinux_images={'x86_64': default_manylinux_images_x86_64.get(manylinux_x86_64_image) or manylinux_x86_64_image, + 'i686': default_manylinux_images_i686.get(manylinux_i686_image) or manylinux_i686_image}, ) elif platform == 'macos': pass @@ -170,6 +180,26 @@ def main(): raise Exception('Unsupported platform') +def detect_obsolete_options(): + # Check the old 'MANYLINUX1_*_IMAGE' options + for (deprecated, alternative) in [('CIBW_MANYLINUX1_X86_64_IMAGE', 'CIBW_MANYLINUX_X86_64_IMAGE'), + ('CIBW_MANYLINUX1_I686_IMAGE', 'CIBW_MANYLINUX_I686_IMAGE')]: + if deprecated in os.environ: + print("'{}' has been deprecated, and will be removed in a future release. Use the option '{}' instead.".format(deprecated, alternative)) + if alternative not in os.environ: + print("Using value of option '{}' as replacement for '{}'".format(deprecated, alternative)) + os.environ[alternative] = os.environ[deprecated] + else: + print("Option '{}' is not empty. Please unset '{}'".format(alternative, deprecated)) + exit(2) + + # Check for 'manylinux1' in the 'CIBW_BUILD' and 'CIBW_SKIP' options + for deprecated in ['CIBW_BUILD', 'CIBW_SKIP']: + if deprecated in os.environ and 'manylinux1' in os.environ[deprecated]: + print("Build identifiers with 'manylinux1' been deprecated. Replacing all occurences of 'manylinux1' by 'manylinux' in the option '{}'".format(deprecated)) + os.environ[deprecated] = os.environ[deprecated].replace('manylinux1', 'manylinux') + + def print_preamble(platform, build_options): print(textwrap.dedent(''' _ _ _ _ _ _ _ diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 473838b0..233f1f02 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -12,25 +12,25 @@ except ImportError: def get_python_configurations(build_selector): PythonConfiguration = namedtuple('PythonConfiguration', ['identifier', 'path']) python_configurations = [ - PythonConfiguration(identifier='cp27-manylinux1_x86_64', path='/opt/python/cp27-cp27m'), - PythonConfiguration(identifier='cp27-manylinux1_x86_64', path='/opt/python/cp27-cp27mu'), - PythonConfiguration(identifier='cp34-manylinux1_x86_64', path='/opt/python/cp34-cp34m'), - PythonConfiguration(identifier='cp35-manylinux1_x86_64', path='/opt/python/cp35-cp35m'), - PythonConfiguration(identifier='cp36-manylinux1_x86_64', path='/opt/python/cp36-cp36m'), - PythonConfiguration(identifier='cp37-manylinux1_x86_64', path='/opt/python/cp37-cp37m'), - PythonConfiguration(identifier='cp27-manylinux1_i686', path='/opt/python/cp27-cp27m'), - PythonConfiguration(identifier='cp27-manylinux1_i686', path='/opt/python/cp27-cp27mu'), - PythonConfiguration(identifier='cp34-manylinux1_i686', path='/opt/python/cp34-cp34m'), - PythonConfiguration(identifier='cp35-manylinux1_i686', path='/opt/python/cp35-cp35m'), - PythonConfiguration(identifier='cp36-manylinux1_i686', path='/opt/python/cp36-cp36m'), - PythonConfiguration(identifier='cp37-manylinux1_i686', path='/opt/python/cp37-cp37m'), + PythonConfiguration(identifier='cp27-manylinux_x86_64', path='/opt/python/cp27-cp27m'), + PythonConfiguration(identifier='cp27-manylinux_x86_64', path='/opt/python/cp27-cp27mu'), + PythonConfiguration(identifier='cp34-manylinux_x86_64', path='/opt/python/cp34-cp34m'), + PythonConfiguration(identifier='cp35-manylinux_x86_64', path='/opt/python/cp35-cp35m'), + PythonConfiguration(identifier='cp36-manylinux_x86_64', path='/opt/python/cp36-cp36m'), + PythonConfiguration(identifier='cp37-manylinux_x86_64', path='/opt/python/cp37-cp37m'), + PythonConfiguration(identifier='cp27-manylinux_i686', path='/opt/python/cp27-cp27m'), + PythonConfiguration(identifier='cp27-manylinux_i686', path='/opt/python/cp27-cp27mu'), + PythonConfiguration(identifier='cp34-manylinux_i686', path='/opt/python/cp34-cp34m'), + PythonConfiguration(identifier='cp35-manylinux_i686', path='/opt/python/cp35-cp35m'), + PythonConfiguration(identifier='cp36-manylinux_i686', path='/opt/python/cp36-cp36m'), + PythonConfiguration(identifier='cp37-manylinux_i686', path='/opt/python/cp37-cp37m'), ] # skip builds as required return [c for c in python_configurations if build_selector(c.identifier)] -def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, environment, manylinux1_images): +def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, environment, manylinux_images): try: subprocess.check_call(['docker', '--version']) except: @@ -42,8 +42,8 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef python_configurations = get_python_configurations(build_selector) platforms = [ - ('manylinux1_x86_64', manylinux1_images.get('x86_64') or 'quay.io/pypa/manylinux1_x86_64'), - ('manylinux1_i686', manylinux1_images.get('i686') or 'quay.io/pypa/manylinux1_i686'), + ('manylinux_x86_64', manylinux_images['x86_64']), + ('manylinux_i686', manylinux_images['i686']), ] for platform_tag, docker_image in platforms: @@ -62,9 +62,9 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef for PYBIN in {pybin_paths}; do # Setup rm -rf /tmp/built_wheel - rm -rf /tmp/delocated_wheel + rm -rf /tmp/delocated_wheels mkdir /tmp/built_wheel - mkdir /tmp/delocated_wheel + mkdir /tmp/delocated_wheels if [ ! -z {before_build} ]; then PATH="$PYBIN:$PATH" sh -c {before_build} @@ -79,30 +79,52 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef # the first element if [[ "$built_wheel" == *none-any.whl ]]; then # pure python wheel - just copy - mv "$built_wheel" /tmp/delocated_wheel + mv "$built_wheel" /tmp/delocated_wheels else - auditwheel repair "$built_wheel" -w /tmp/delocated_wheel + auditwheel repair "$built_wheel" -w /tmp/delocated_wheels fi - delocated_wheel=(/tmp/delocated_wheel/*.whl) + delocated_wheels=(/tmp/delocated_wheels/*.whl) - # Install the wheel we just built - "$PYBIN/pip" install "$delocated_wheel"{test_extras} - - # Install any requirements to run the tests - if [ ! -z "{test_requires}" ]; then - "$PYBIN/pip" install {test_requires} - fi - - # Run the tests from a different directory if [ ! -z {test_command} ]; then - pushd $HOME - PATH="$PYBIN:$PATH" sh -c {test_command} - popd + # Set up a virtual environment to install and test from, to make sure + # there are no dependencies that were pulled in at build time. + "$PYBIN/pip" install virtualenv + venv_dir=`mktemp -d`/venv + "$PYBIN/python" -m virtualenv "$venv_dir" + + # run the tests in a subshell to keep that `activate` + # script from polluting the env + ( + source "$venv_dir/bin/activate" + + echo "Running tests using `which python`" + + # Install the wheel we just built + # Note: If auditwheel produced two wheels, it's because the earlier produced wheel + # conforms to multiple manylinux standards. These multiple versions of the wheel are + # functionally the same, differing only in name, wheel metadata, and possibly include + # different external shared libraries. so it doesn't matter which one we run the tests on. + # Let's just pick the first one. + pip install "${{delocated_wheels[0]}}"{test_extras} + + # Install any requirements to run the tests + if [ ! -z "{test_requires}" ]; then + pip install {test_requires} + fi + + # Run the tests from a different directory + pushd $HOME + sh -c {test_command} + popd + ) + + # clean up + rm -rf "$venv_dir" fi # we're all done here; move it to output - mv "$delocated_wheel" /output - chown {uid}:{gid} "/output/$(basename "$delocated_wheel")" + mv "${{delocated_wheels[@]}}" /output + for delocated_wheel in "${{delocated_wheels[@]}}"; do chown {uid}:{gid} "/output/$(basename "$delocated_wheel")"; done done '''.format( pybin_paths=' '.join(c.path+'/bin' for c in platform_configs), diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 61a432e0..7378f142 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -1,4 +1,5 @@ from __future__ import print_function +import tempfile import os, subprocess, shlex, sys, shutil from collections import namedtuple from glob import glob @@ -13,11 +14,11 @@ from .util import prepare_command, get_build_verbosity_extra_flags def get_python_configurations(build_selector): PythonConfiguration = namedtuple('PythonConfiguration', ['version', 'identifier', 'url']) python_configurations = [ - PythonConfiguration(version='2.7', identifier='cp27-macosx_10_6_intel', url='https://www.python.org/ftp/python/2.7.16/python-2.7.16-macosx10.6.pkg'), + PythonConfiguration(version='2.7', identifier='cp27-macosx_10_6_intel', url='https://www.python.org/ftp/python/2.7.17/python-2.7.17-macosx10.6.pkg'), PythonConfiguration(version='3.4', identifier='cp34-macosx_10_6_intel', url='https://www.python.org/ftp/python/3.4.4/python-3.4.4-macosx10.6.pkg'), PythonConfiguration(version='3.5', identifier='cp35-macosx_10_6_intel', url='https://www.python.org/ftp/python/3.5.4/python-3.5.4-macosx10.6.pkg'), PythonConfiguration(version='3.6', identifier='cp36-macosx_10_6_intel', url='https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.6.pkg'), - PythonConfiguration(version='3.7', identifier='cp37-macosx_10_6_intel', url='https://www.python.org/ftp/python/3.7.4/python-3.7.4-macosx10.6.pkg'), + PythonConfiguration(version='3.7', identifier='cp37-macosx_10_6_intel', url='https://www.python.org/ftp/python/3.7.5/python-3.7.5-macosx10.6.pkg'), ] # skip builds as required @@ -120,18 +121,40 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef call(['delocate-wheel', '-w', '/tmp/delocated_wheel', built_wheel], env=env) delocated_wheel = glob('/tmp/delocated_wheel/*.whl')[0] - # install the wheel - call(['pip', 'install', delocated_wheel + test_extras], env=env) - - # test the wheel - if test_requires: - call(['pip', 'install'] + test_requires, env=env) if test_command: + # set up a virtual environment to install and test from, to make sure + # there are no dependencies that were pulled in at build time. + call(['pip', 'install', 'virtualenv'], env=env) + venv_dir = tempfile.mkdtemp() + call(['python', '-m', 'virtualenv', venv_dir], env=env) + + virtualenv_env = env.copy() + virtualenv_env['PATH'] = os.pathsep.join([ + os.path.join(venv_dir, 'bin'), + virtualenv_env['PATH'], + ]) + # Fix some weird issue with the shebang of installed scripts + # See https://github.com/theacodes/nox/issues/44 and https://github.com/pypa/virtualenv/issues/620 + virtualenv_env.pop('__PYVENV_LAUNCHER__', None) + + # check that we are using the Python from the virtual environment + call(['which', 'python'], env=virtualenv_env) + + # install the wheel + call(['pip', 'install', delocated_wheel + test_extras], env=virtualenv_env) + + # test the wheel + if test_requires: + call(['pip', 'install'] + test_requires, env=virtualenv_env) + # run the tests from $HOME, with an absolute path in the command # (this ensures that Python runs the tests against the installed wheel # and not the repo code) test_command_prepared = prepare_command(test_command, project=abs_project_dir) - call(test_command_prepared, cwd=os.environ['HOME'], env=env, shell=True) + call(test_command_prepared, cwd=os.environ['HOME'], env=virtualenv_env, shell=True) + + # clean up + shutil.rmtree(venv_dir) # we're all done here; move it to output (overwrite existing) dst = os.path.join(output_dir, os.path.basename(delocated_wheel)) diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 13fd4f54..da929bcc 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -1,12 +1,18 @@ from __future__ import print_function -import os, tempfile, subprocess, shutil +import os, tempfile, subprocess, shutil, sys from collections import namedtuple from glob import glob +try: + from shlex import quote as shlex_quote +except ImportError: + from pipes import quote as shlex_quote + from .util import prepare_command, get_build_verbosity_extra_flags IS_RUNNING_ON_AZURE = os.path.exists('C:\\hostedtoolcache') +IS_RUNNING_ON_TRAVIS = os.environ.get('TRAVIS_OS_NAME') == 'windows' def get_python_path(config): if IS_RUNNING_ON_AZURE: @@ -22,6 +28,12 @@ def get_python_path(config): return glob(path_pattern)[0] except IndexError: raise Exception('Could not find a Python install at ' + path_pattern) + elif IS_RUNNING_ON_TRAVIS: + if config.version == "3.4.x": + return config.path + else: + nuget_args = get_nuget_args(config) + return os.path.join(nuget_args[-1], nuget_args[0] + "." + config.nuget_version, "tools") else: # Assume we're running on AppVeyor major, minor = config.version.split('.')[:2] @@ -32,32 +44,49 @@ def get_python_path(config): ) +def get_nuget_args(configuration): + if configuration.nuget_version is None: + return None + python_name = "python" if configuration.version[0] == '3' else "python2" + if configuration.arch == "32": + python_name = python_name + "x86" + return [python_name, "-Version", configuration.nuget_version, "-OutputDirectory", "C:/python"] + def get_python_configurations(build_selector): - PythonConfiguration = namedtuple('PythonConfiguration', ['version', 'arch', 'identifier', 'path']) + PythonConfiguration = namedtuple('PythonConfiguration', ['version', 'arch', 'identifier', 'path', "nuget_version"]) python_configurations = [ - PythonConfiguration(version='2.7.x', arch="32", identifier='cp27-win32', path='C:\Python27'), - PythonConfiguration(version='2.7.x', arch="64", identifier='cp27-win_amd64', path='C:\Python27-x64'), - PythonConfiguration(version='3.4.x', arch="32", identifier='cp34-win32', path='C:\Python34'), - PythonConfiguration(version='3.4.x', arch="64", identifier='cp34-win_amd64', path='C:\Python34-x64'), - PythonConfiguration(version='3.5.x', arch="32", identifier='cp35-win32', path='C:\Python35'), - PythonConfiguration(version='3.5.x', arch="64", identifier='cp35-win_amd64', path='C:\Python35-x64'), - PythonConfiguration(version='3.6.x', arch="32", identifier='cp36-win32', path='C:\Python36'), - PythonConfiguration(version='3.6.x', arch="64", identifier='cp36-win_amd64', path='C:\Python36-x64'), - PythonConfiguration(version='3.7.x', arch="32", identifier='cp37-win32', path='C:\Python37'), - PythonConfiguration(version='3.7.x', arch="64", identifier='cp37-win_amd64', path='C:\Python37-x64'), + PythonConfiguration(version='2.7.x', arch="32", identifier='cp27-win32', path='C:\\Python27', nuget_version="2.7.16"), + PythonConfiguration(version='2.7.x', arch="64", identifier='cp27-win_amd64', path='C:\\Python27-x64', nuget_version="2.7.16"), + PythonConfiguration(version='3.4.x', arch="32", identifier='cp34-win32', path='C:\\Python34', nuget_version=None), + PythonConfiguration(version='3.4.x', arch="64", identifier='cp34-win_amd64', path='C:\\Python34-x64', nuget_version=None), + PythonConfiguration(version='3.5.x', arch="32", identifier='cp35-win32', path='C:\\Python35', nuget_version="3.5.4"), + PythonConfiguration(version='3.5.x', arch="64", identifier='cp35-win_amd64', path='C:\\Python35-x64', nuget_version="3.5.4"), + PythonConfiguration(version='3.6.x', arch="32", identifier='cp36-win32', path='C:\\Python36', nuget_version="3.6.8"), + PythonConfiguration(version='3.6.x', arch="64", identifier='cp36-win_amd64', path='C:\\Python36-x64', nuget_version="3.6.8"), + PythonConfiguration(version='3.7.x', arch="32", identifier='cp37-win32', path='C:\\Python37', nuget_version="3.7.4"), + PythonConfiguration(version='3.7.x', arch="64", identifier='cp37-win_amd64', path='C:\\Python37-x64', nuget_version="3.7.4") ] if IS_RUNNING_ON_AZURE: # Python 3.4 isn't supported on Azure. # See https://github.com/Microsoft/azure-pipelines-tasks/issues/9674 python_configurations = [c for c in python_configurations if c.version != '3.4.x'] + + if IS_RUNNING_ON_TRAVIS: + # cannot install VCForPython27.msi which is needed for compiling C software + # try with (and similar): msiexec /i VCForPython27.msi ALLUSERS=1 ACCEPT=YES /passive + # no easy and stable way fo installing python 3.4 + python_configurations = [c for c in python_configurations if c.version != '2.7.x' and c.version != '3.4.x'] + + # skip builds as required + python_configurations = [c for c in python_configurations if build_selector(c.identifier)] + + return python_configurations - # skip builds as required - return [c for c in python_configurations if build_selector(c.identifier)] def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, environment): - if IS_RUNNING_ON_AZURE: + if IS_RUNNING_ON_AZURE or IS_RUNNING_ON_TRAVIS: def shell(args, env=None, cwd=None): print('+ ' + ' '.join(args)) args = ['cmd', '/E:ON', '/V:ON', '/C'] + args @@ -77,10 +106,21 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef temp_dir = tempfile.mkdtemp(prefix='cibuildwheel') built_wheel_dir = os.path.join(temp_dir, 'built_wheel') - python_configurations = get_python_configurations(build_selector) + if IS_RUNNING_ON_TRAVIS: + # instal nuget as best way for provide python + shell(["choco", "install", "nuget.commandline"]) + # get pip fo this installation which not have. + get_pip_url = 'https://bootstrap.pypa.io/get-pip.py' + get_pip_script = 'C:\\get-pip.py' + shell(['curl', '-L', '-o', get_pip_script, get_pip_url]) + python_configurations = get_python_configurations(build_selector) for config in python_configurations: config_python_path = get_python_path(config) + if IS_RUNNING_ON_TRAVIS and config.nuget_version is not None and not os.path.exists(config_python_path): + shell(["nuget", "install"] + get_nuget_args(config)) + if not os.path.exists(os.path.join(config_python_path, 'Scripts', 'pip.exe')): + shell([os.path.join(config_python_path, 'python.exe'), get_pip_script ]) # check python & pip exist for this configuration assert os.path.exists(os.path.join(config_python_path, 'python.exe')) @@ -107,8 +147,12 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef shell(['python', '-c', '"import struct; print(struct.calcsize(\'P\') * 8)\"'], env=env) # prepare the Python environment - shell(['python', '-m', 'pip', 'install', '--upgrade', 'pip'], + if config.version == "3.4.x": + shell(['python', '-m', 'pip', 'install', 'pip==19.1.1'], env=env) + else: + shell(['python', '-m', 'pip', 'install', '--upgrade', 'pip'], + env=env) shell(['pip', 'install', '--upgrade', 'setuptools'], env=env) shell(['pip', 'install', 'wheel'], env=env) @@ -121,18 +165,37 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef shell(['pip', 'wheel', abs_project_dir, '-w', built_wheel_dir, '--no-deps'] + get_build_verbosity_extra_flags(build_verbosity), env=env) built_wheel = glob(built_wheel_dir+'/*.whl')[0] - # install the wheel - shell(['pip', 'install', built_wheel + test_extras], env=env) - - # test the wheel - if test_requires: - shell(['pip', 'install'] + test_requires, env=env) if test_command: + # set up a virtual environment to install and test from, to make sure + # there are no dependencies that were pulled in at build time. + shell(['pip', 'install', 'virtualenv'], env=env) + venv_dir = tempfile.mkdtemp() + shell(['python', '-m', 'virtualenv', venv_dir], env=env) + + virtualenv_env = env.copy() + virtualenv_env['PATH'] = os.pathsep.join([ + os.path.join(venv_dir, 'Scripts'), + virtualenv_env['PATH'], + ]) + + # check that we are using the Python from the virtual environment + shell(['which', 'python'], env=virtualenv_env) + + # install the wheel + shell(['pip', 'install', built_wheel + test_extras], env=virtualenv_env) + + # test the wheel + if test_requires: + shell(['pip', 'install'] + test_requires, env=virtualenv_env) + # run the tests from c:\, with an absolute path in the command # (this ensures that Python runs the tests against the installed wheel # and not the repo code) test_command_prepared = prepare_command(test_command, project=abs_project_dir) - shell([test_command_prepared], cwd='c:\\', env=env) + shell([test_command_prepared], cwd='c:\\', env=virtualenv_env) + + # clean up + shutil.rmtree(venv_dir) # we're all done here; move it to output (remove if already exists) dst = os.path.join(output_dir, os.path.basename(built_wheel)) diff --git a/docs/faq.md b/docs/faq.md index aa6faec8..dc424980 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -16,13 +16,13 @@ If your wheel didn't compile, check the list below for some debugging tips. ### Linux builds on Docker -Linux wheels are built in the [`manylinux` docker images](https://github.com/pypa/manylinux) to provide binary compatible wheels on Linux, according to [PEP 513](https://www.python.org/dev/peps/pep-0513/). Because of this, when building with `cibuildwheel` on Linux, a few things should be taken into account: +Linux wheels are built in the [`manylinux` docker images](https://github.com/pypa/manylinux) to provide binary compatible wheels on Linux, according to [PEP 571](https://www.python.org/dev/peps/pep-0571/). Because of this, when building with `cibuildwheel` on Linux, a few things should be taken into account: - Programs and libraries cannot be installed on the Travis CI Ubuntu host with `apt-get`, but can be installed inside of the Docker image using `yum` or manually. The same goes for environment variables that are potentially needed to customize the wheel building. `cibuildwheel` supports this by providing the `CIBW_ENVIRONMENT` and `CIBW_BEFORE_BUILD` options to setup the build environment inside the running Docker image. See [the options docs](options.md#build-environment) for details on these options. - The project directory is mounted in the running Docker instance as `/project`, the output directory for the wheels as `/output`. In general, this is handled transparently by `cibuildwheel`. For a more finegrained level of control however, the root of the host file system is mounted as `/host`, allowing for example to access shared files, caches, etc. on the host file system. Note that this is not available on CircleCI due to their Docker policies. -- Alternative dockers images can be specified with the `CIBW_MANYLINUX1_X86_64_IMAGE` and `CIBW_MANYLINUX1_I686_IMAGE` options to allow for a custom, preconfigured build environment for the Linux builds. See [options](options.md#manylinux-image) for more details. +- Alternative dockers images can be specified with the `CIBW_MANYLINUX_X86_64_IMAGE` and `CIBW_MANYLINUX_I686_IMAGE` options to allow for a custom, preconfigured build environment for the Linux builds. See [options](options.md#manylinux-image) for more details. ### Building packages with optional C extensions diff --git a/docs/options.md b/docs/options.md index e956382d..6e95755a 100644 --- a/docs/options.md +++ b/docs/options.md @@ -59,17 +59,19 @@ For `linux` you need Docker running, on Mac or Linux. For `macos`, you need a Ma > Choose the Python versions to build -Space-separated list of builds to build and skip. Each build has an identifier like `cp27-manylinux1_x86_64` or `cp34-macosx_10_6_intel` - you can list specific ones to build and `cibuildwheel` will only build those, and/or list ones to skip and `cibuildwheel` won't try to build them. +Space-separated list of builds to build and skip. Each build has an identifier like `cp27-manylinux_x86_64` or `cp34-macosx_10_6_intel` - you can list specific ones to build and `cibuildwheel` will only build those, and/or list ones to skip and `cibuildwheel` won't try to build them. When both options are specified, both conditions are applied and only builds with a tag that matches `CIBW_BUILD` and does not match `CIBW_SKIP` will be built. -The format is `python_tag-platform_tag`. The tags are as defined in [PEP 0425](https://www.python.org/dev/peps/pep-0425/#details). +The format is `python_tag-platform_tag`. The tags are similar but not identical to the ones defined in [PEP 425](https://www.python.org/dev/peps/pep-0425/#details). Python tags look like `cp27` `cp34` `cp35` `cp36` `cp37` -Platform tags look like `macosx_10_6_intel` `manylinux1_x86_64` `manylinux1_i686` `win32` `win_amd64` +Platform tags look like `macosx_10_6_intel` `manylinux_x86_64` `manylinux_i686` `win32` `win_amd64` -You can also use shell-style globbing syntax (as per `fnmatch`) +You can also use shell-style globbing syntax (as per `fnmatch`). + +The list of supported and currently selected build identifiers can be retrieved by passing the `--print-build-identifiers` flag to `cibuildwheel`. Examples: @@ -80,7 +82,7 @@ Examples: - Skip Python 2.7 on 32-bit Windows: `CIBW_SKIP=cp27-win32` - Skip Python 3.4 and Python 3.5: `CIBW_SKIP=cp34-* cp35-*` - Skip Python 3.6 on Linux: `CIBW_SKIP=cp36-manylinux*` -- Only build on Python 3 and skip 32-bit builds: `CIBW_BUILD=cp3?-*` and `CIBW_SKIP=*-win32 *-manylinux1_i686` +- Only build on Python 3 and skip 32-bit builds: `CIBW_BUILD=cp3?-*` and `CIBW_SKIP=*-win32 *-manylinux_i686` ## Build environment @@ -119,13 +121,18 @@ Example: `yum install -y libffi-dev && pip install .` Platform-specific variants also available: `CIBW_BEFORE_BUILD_MACOS` | `CIBW_BEFORE_BUILD_WINDOWS` | `CIBW_BEFORE_BUILD_LINUX` -### CIBW_MANYLINUX1_X86_64_IMAGE, CIBW_MANYLINUX1_I686_IMAGE {: #manylinux-image} -> Specify alternative manylinux1 x86_64 docker images +### CIBW_MANYLINUX_X86_64_IMAGE, CIBW_MANYLINUX_I686_IMAGE {: #manylinux-image} +> Specify alternative manylinux docker images -An alternative docker image to be used for building [`manylinux1`](https://github.com/pypa/manylinux) wheels. `cibuildwheel` will then pull these instead of the official images, [`quay.io/pypa/manylinux1_x86_64`](https://quay.io/pypa/manylinux1_i686) and [`quay.io/pypa/manylinux1_i686`](https://quay.io/pypa/manylinux1_i686). +An alternative Docker image to be used for building [`manylinux`](https://github.com/pypa/manylinux) wheels. `cibuildwheel` will then pull these instead of the default images, [`quay.io/pypa/manylinux2010_x86_64`](https://quay.io/pypa/manylinux2010_x86_64) and [`quay.io/pypa/manylinux2010_i686`](https://quay.io/pypa/manylinux2010_i686). -Beware to specify a valid docker image that can be used the same as the official, default docker images: all necessary Python and pip versions need to be present in `/opt/python/`, and the `auditwheel` tool needs to be present for `cibuildwheel` to work. Apart from that, the architecture and relevant shared system libraries need to be manylinux1-compatible in order to produce valid `manylinux1` wheels (see https://github.com/pypa/manylinux and [PEP 513](https://www.python.org/dev/peps/pep-0513/) for more details). +The value of this option can either be set to `manylinux1` or `manylinux2010` to use the [official `manylinux` images](https://github.com/pypa/manylinux), or any other valid Docker image name. +Beware to specify a valid Docker image that can be used in the same way as the official, default Docker images: all necessary Python and pip versions need to be present in `/opt/python/`, and the `auditwheel` tool needs to be present for `cibuildwheel` to work. Apart from that, the architecture and relevant shared system libraries need to be manylinux1- or manylinux2010-compatible in order to produce valid `manylinux1`/`manylinux2010` wheels (see https://github.com/pypa/manylinux, [PEP 513](https://www.python.org/dev/peps/pep-0513/), and [PEP 571](https://www.python.org/dev/peps/pep-0571/) for more details). + +Note that `auditwheel` detects the version of the `manylinux` standard in the Docker image through the `AUDITWHEEL_PLAT` environment variable, as `cibuildwheel` has no way of detecting the correct `--plat` command line argument to pass to `auditwheel` for a custom image. If a Docker image does not correctly set this `AUDITWHEEL_PLAT` environment variable, the `CIBW_ENVIRONMENT` option can be used to do so (e.g., `CIBW_ENVIRONMENT="manylinux2010_$(uname -m)"`). + +Example: `manylinux1` Example: `dockcross/manylinux-x64` Example: `dockcross/manylinux-x86` diff --git a/docs/setup.md b/docs/setup.md index 3eb80639..4cf1c538 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -73,6 +73,18 @@ script: - cibuildwheel --output-dir wheelhouse ``` +To build on Windows too, add this matrix entry: +```yaml + - os: windows + language: shell + before_install: + - choco install python3 --version 3.6.8 --no-progress -y + env: + - PATH=/c/Python36:/c/Python36/Scripts:$PATH +``` + +Note that building Windows Python 2.7 wheels on Travis is unsupported. + Commit this file, enable building of your repo on Travis CI, and push. Then setup a deployment method by following the [Travis CI deployment docs](https://docs.travis-ci.com/user/deployment/), or see [Delivering to PyPI](deliver-to-pypi.md). For more info on `.travis.yml`, check out the [docs](https://docs.travis-ci.com/). diff --git a/test/01_basic/cibuildwheel_test.py b/test/01_basic/cibuildwheel_test.py index 93c67829..998b55de 100644 --- a/test/01_basic/cibuildwheel_test.py +++ b/test/01_basic/cibuildwheel_test.py @@ -17,6 +17,10 @@ def test(): def test_build_identifiers(): # check that the number of expected wheels matches the number of build # identifiers - expected_wheels = utils.expected_wheels('spam', '0.1.0') + # after adding CIBW_MANYLINUX_IMAGE to support manylinux2010, there + # can be multiple wheels for each wheel, though, so we need to limit + # the expected wheels + expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0') + if not '-manylinux' in w or '-manylinux1' in w] build_identifiers = utils.cibuildwheel_get_build_identifiers(project_dir) assert len(expected_wheels) == len(build_identifiers) diff --git a/test/02_test/cibuildwheel_test.py b/test/02_test/cibuildwheel_test.py index 54973573..99324e87 100644 --- a/test/02_test/cibuildwheel_test.py +++ b/test/02_test/cibuildwheel_test.py @@ -1,4 +1,5 @@ -import os +import os, subprocess +import pytest import utils def test(): @@ -35,3 +36,16 @@ def test_extras_require(): expected_wheels = utils.expected_wheels('spam', '0.1.0') actual_wheels = os.listdir('wheelhouse') assert set(actual_wheels) == set(expected_wheels) + + +def test_failing_test(): + '''Ensure a failing test causes cibuildwheel to error out and exit''' + project_dir = os.path.dirname(__file__) + + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_TEST_COMMAND': 'false', + }) + + assert len(os.listdir('wheelhouse')) + diff --git a/test/06_docker_images/cibuildwheel_test.py b/test/06_docker_images/cibuildwheel_test.py index a5448485..9067bc0f 100644 --- a/test/06_docker_images/cibuildwheel_test.py +++ b/test/06_docker_images/cibuildwheel_test.py @@ -5,14 +5,17 @@ def test(): project_dir = os.path.dirname(__file__) if utils.platform != 'linux': - pytest.skip('the docker test is only relevant to the linux build') + pytest.skip('the test is only relevant to the linux build') utils.cibuildwheel_run(project_dir, add_env={ - 'CIBW_MANYLINUX1_X86_64_IMAGE': 'dockcross/manylinux-x64', - 'CIBW_MANYLINUX1_I686_IMAGE': 'dockcross/manylinux-x86', + 'CIBW_MANYLINUX_X86_64_IMAGE': 'dockcross/manylinux2010-x64', + 'CIBW_MANYLINUX_I686_IMAGE': 'dockcross/manylinux1-x86', + 'CIBW_BEFORE_BUILD': '/opt/python/cp36-cp36m/bin/pip install -U auditwheel', # Currently necessary on dockcross images to get auditwheel 2.1 supporting AUDITWHEEL_PLAT + 'CIBW_ENVIRONMENT': 'AUDITWHEEL_PLAT=`if [ $(uname -i) == "x86_64" ]; then echo "manylinux2010_x86_64"; else echo "manylinux1_i686"; fi`', }) # also check that we got the right wheels built - expected_wheels = utils.expected_wheels('spam', '0.1.0') + expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0') + if '-manylinux2010_i686' not in w] actual_wheels = os.listdir('wheelhouse') assert set(actual_wheels) == set(expected_wheels) diff --git a/test/08_manylinux2010_only/cibuildwheel_test.py b/test/08_manylinux2010_only/cibuildwheel_test.py new file mode 100644 index 00000000..dab763e7 --- /dev/null +++ b/test/08_manylinux2010_only/cibuildwheel_test.py @@ -0,0 +1,21 @@ +import os, pytest +import utils + +def test(): + project_dir = os.path.dirname(__file__) + + if utils.platform != 'linux': + pytest.skip('the docker test is only relevant to the linux build') + + # build the wheels + # CFLAGS environment veriable is ecessary to fail on 'malloc_info' (on manylinux1) during compilation/linking, + # rather than when dynamically loading the Python + utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_ENVIRONMENT': 'CFLAGS="$CFLAGS -Werror=implicit-function-declaration"', + }) + + # also check that we got the right wheels + expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0') + if not '-manylinux' in w or '-manylinux2010' in w] + actual_wheels = os.listdir('wheelhouse') + assert set(actual_wheels) == set(expected_wheels) diff --git a/test/08_manylinux2010_only/setup.py b/test/08_manylinux2010_only/setup.py new file mode 100644 index 00000000..866fa22c --- /dev/null +++ b/test/08_manylinux2010_only/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, Extension + +setup( + name="spam", + ext_modules=[Extension('spam', sources=['spam.c'])], + version="0.1.0", +) diff --git a/test/08_manylinux2010_only/spam.c b/test/08_manylinux2010_only/spam.c new file mode 100644 index 00000000..0e50eba9 --- /dev/null +++ b/test/08_manylinux2010_only/spam.c @@ -0,0 +1,57 @@ +#include +#if defined(__linux__) +#include +#endif + +static PyObject * +spam_system(PyObject *self, PyObject *args) +{ + const char *command; + int sts = 0; + + if (!PyArg_ParseTuple(args, "s", &command)) + return NULL; + +#if defined(__linux__) + sts = malloc_info(0, stdout); +#endif + if (sts == 0) { + sts = system(command); + } + return PyLong_FromLong(sts); +} + +/* Module initialization */ + +#if PY_MAJOR_VERSION >= 3 + #define MOD_INIT(name) PyMODINIT_FUNC PyInit_##name(void) + #define MOD_DEF(m, name, doc, methods, module_state_size) \ + static struct PyModuleDef moduledef = { \ + PyModuleDef_HEAD_INIT, name, doc, module_state_size, methods, }; \ + m = PyModule_Create(&moduledef); + #define MOD_RETURN(m) return m; +#else + #define MOD_INIT(name) PyMODINIT_FUNC init##name(void) + #define MOD_DEF(m, name, doc, methods, module_state_size) \ + m = Py_InitModule3(name, methods, doc); + #define MOD_RETURN(m) return; +#endif + +static PyMethodDef module_methods[] = { + {"system", (PyCFunction)spam_system, METH_VARARGS, + "Execute a shell command."}, + {NULL} /* Sentinel */ +}; + +MOD_INIT(spam) +{ + PyObject* m; + + MOD_DEF(m, + "spam", + "Example module", + module_methods, + -1) + + MOD_RETURN(m) +} diff --git a/test/shared/utils.py b/test/shared/utils.py index 0284dd4a..78f09050 100644 --- a/test/shared/utils.py +++ b/test/shared/utils.py @@ -6,7 +6,8 @@ This file is added to the PYTHONPATH in the test runner at bin/run_test.py. import subprocess, sys, os -IS_RUNNING_ON_AZURE = os.path.exists('C:\\hostedtoolcache') +IS_WINDOWS_RUNNING_ON_AZURE = os.path.exists('C:\\hostedtoolcache') +IS_WINDOWS_RUNNING_ON_TRAVIS = os.environ.get('TRAVIS_OS_NAME') == 'windows' def cibuildwheel_get_build_identifiers(project_path, env=None): @@ -54,12 +55,24 @@ def expected_wheels(package_name, package_version): '{package_name}-{package_version}-cp35-cp35m-manylinux1_x86_64.whl', '{package_name}-{package_version}-cp36-cp36m-manylinux1_x86_64.whl', '{package_name}-{package_version}-cp37-cp37m-manylinux1_x86_64.whl', + '{package_name}-{package_version}-cp27-cp27m-manylinux2010_x86_64.whl', + '{package_name}-{package_version}-cp27-cp27mu-manylinux2010_x86_64.whl', + '{package_name}-{package_version}-cp34-cp34m-manylinux2010_x86_64.whl', + '{package_name}-{package_version}-cp35-cp35m-manylinux2010_x86_64.whl', + '{package_name}-{package_version}-cp36-cp36m-manylinux2010_x86_64.whl', + '{package_name}-{package_version}-cp37-cp37m-manylinux2010_x86_64.whl', '{package_name}-{package_version}-cp27-cp27m-manylinux1_i686.whl', '{package_name}-{package_version}-cp27-cp27mu-manylinux1_i686.whl', '{package_name}-{package_version}-cp34-cp34m-manylinux1_i686.whl', '{package_name}-{package_version}-cp35-cp35m-manylinux1_i686.whl', '{package_name}-{package_version}-cp36-cp36m-manylinux1_i686.whl', '{package_name}-{package_version}-cp37-cp37m-manylinux1_i686.whl', + '{package_name}-{package_version}-cp27-cp27m-manylinux2010_i686.whl', + '{package_name}-{package_version}-cp27-cp27mu-manylinux2010_i686.whl', + '{package_name}-{package_version}-cp34-cp34m-manylinux2010_i686.whl', + '{package_name}-{package_version}-cp35-cp35m-manylinux2010_i686.whl', + '{package_name}-{package_version}-cp36-cp36m-manylinux2010_i686.whl', + '{package_name}-{package_version}-cp37-cp37m-manylinux2010_i686.whl', ] elif platform == 'windows': templates = [ @@ -85,9 +98,12 @@ def expected_wheels(package_name, package_version): else: raise Exception('unsupported platform') - if IS_RUNNING_ON_AZURE: + if IS_WINDOWS_RUNNING_ON_AZURE: # Python 3.4 isn't supported on Azure. templates = [t for t in templates if '-cp34-' not in t] + if IS_WINDOWS_RUNNING_ON_TRAVIS: + # Python 2.7 and 3.4 isn't supported on Travis. + templates = [t for t in templates if '-cp27-' not in t and '-cp34-' not in t] return [filename.format(package_name=package_name, package_version=package_version) for filename in templates]