Repository: pypa/cibuildwheel

Issue:
Add support for non-x86 architectures with manylinux2014
With manylinux2014 there is support for publishing wheels on non-x86 architectures. There are docker images available for all the supported architectures: https://github.com/pypa/manylinux#manylinux2014 and travis supports running CI on these architectures too: https://docs.travis-ci.com/user/multi-cpu-architectures/. Having options available to specify a non-x86 builds and docker images would be great for projects that want to support users on other architectures.


Discussion:
Totally agree this would be great! By the way, nothing in the current manylinux build is specific to x86, so I tried one on the manylinux2014-aarch64 (arm64) build. It [ran, but didn't get very far](https://travis-ci.org/joerick/cibuildwheel-autopypi-example/builds/648057310),  I guess the paths to each Python are different on those images.

PRs welcome! If anyone wants to take a crack at this, let's chat API and build identifiers before you start :)

EDIT: building only Python 3.8 gets around the python path issue above - [it seems to work](https://travis-ci.org/joerick/cibuildwheel-autopypi-example)! so changes would hopefully be pretty minimal :)
I think I might have time to take a crack at this over the next week or so. For the new identifiers I was thinking of just following the same pattern: `cp*--manylinux_aarch64`, `cp*-manylinux_ppc64le` and `cp*--manylinux_s390x`. Looking at the [manylinux2014 spec](https://www.python.org/dev/peps/pep-0599/#the-manylinux2014-policy) there is also support for `armv7l` and  `ppc64` (big endian) but without a docker image or host env in CI for either I'm not sure we'll be able to support those.

I was thinking of also adding options for the docker image: `CIBW_MANYLINUX_AARCH64_IMAGE`, `CIBW_MANYLINUX_PPC64LE_IMAGE`, and `CIBW_MANYLINUX_S390X_IMAGE`.
@mtreinish That would be amazing. I agree with those names; they're the same as in the manylinux standard, so no need to make up something else if there's not specific reason :-)

PyPy also supports some of these architectures, btw: https://www.pypy.org/download.html#python2-7-compatible-pypy-7-3-0. Though I don't think they have manylinux images :-/

Resolving patch:
diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py
--- a/cibuildwheel/__main__.py
+++ b/cibuildwheel/__main__.py
@@ -160,6 +160,9 @@ def main():
         manylinux_x86_64_image = os.environ.get('CIBW_MANYLINUX_X86_64_IMAGE', 'manylinux2010')
         manylinux_i686_image = os.environ.get('CIBW_MANYLINUX_I686_IMAGE', 'manylinux2010')
         manylinux_pypy_x86_64_image = os.environ.get('CIBW_MANYLINUX_PYPY_X86_64_IMAGE', 'manylinux2010')
+        manylinux_aarch64_image = os.environ.get('CIBW_MANYLINUX_AARCH64_IMAGE', 'manylinux2014')
+        manylinux_ppc64le_image = os.environ.get('CIBW_MANYLINUX_PPC64LE_IMAGE', 'manylinux2014')
+        manylinux_s390x_image = os.environ.get('CIBW_MANYLINUX_S390X_IMAGE', 'manylinux2014')
 
         default_manylinux_images_x86_64 = {'manylinux1': 'quay.io/pypa/manylinux1_x86_64',
                                            'manylinux2010': 'quay.io/pypa/manylinux2010_x86_64',
@@ -168,11 +171,18 @@ def main():
                                          'manylinux2010': 'quay.io/pypa/manylinux2010_i686',
                                          'manylinux2014': 'quay.io/pypa/manylinux2014_i686'}
         default_manylinux_images_pypy_x86_64 = {'manylinux2010': 'pypywheels/manylinux2010-pypy_x86_64'}
+        default_manylinux_images_aarch64 = {'manylinux2014': 'quay.io/pypa/manylinux2014_aarch64'}
+        default_manylinux_images_ppc64le = {'manylinux2014': 'quay.io/pypa/manylinux2014_ppc64le'}
+        default_manylinux_images_s390x = {'manylinux2014': 'quay.io/pypa/manylinux2014_s390x'}
 
         build_options.update(
             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,
-                              'pypy_x86_64': default_manylinux_images_pypy_x86_64.get(manylinux_pypy_x86_64_image) or manylinux_pypy_x86_64_image},
+                              'pypy_x86_64': default_manylinux_images_pypy_x86_64.get(manylinux_pypy_x86_64_image) or manylinux_pypy_x86_64_image,
+                              'aarch64': default_manylinux_images_aarch64.get(manylinux_aarch64_image) or manylinux_aarch64_image,
+                              'ppc64le': default_manylinux_images_ppc64le.get(manylinux_ppc64le_image) or manylinux_ppc64le_image,
+                              's390x': default_manylinux_images_s390x.get(manylinux_s390x_image) or manylinux_s390x_image,
+                              },
         )
     elif platform == 'macos':
         pass
diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py
--- a/cibuildwheel/linux.py
+++ b/cibuildwheel/linux.py
@@ -1,4 +1,5 @@
 import os
+import platform
 import shlex
 import subprocess
 import sys
@@ -12,6 +13,27 @@
 )
 
 
+def matches_platform(identifier):
+    pm = platform.machine()
+    if pm == "x86_64":
+        # x86_64 machines can run i686 docker containers
+        if identifier.endswith('x86_64') or identifier.endswith('i686'):
+            return True
+    elif pm == "i686":
+        if identifier.endswith('i686'):
+            return True
+    elif pm == "aarch64":
+        if identifier.endswith('aarch64'):
+            return True
+    elif pm == "ppc64le":
+        if identifier.endswith('ppc64le'):
+            return True
+    elif pm == "s390x":
+        if identifier.endswith('s390x'):
+            return True
+    return False
+
+
 def get_python_configurations(build_selector):
     PythonConfiguration = namedtuple('PythonConfiguration', ['identifier', 'path'])
     python_configurations = [
@@ -29,10 +51,21 @@ def get_python_configurations(build_selector):
         PythonConfiguration(identifier='cp38-manylinux_i686', path='/opt/python/cp38-cp38'),
         PythonConfiguration(identifier='pp27-manylinux_x86_64', path='/opt/python/pp27-pypy_73'),
         PythonConfiguration(identifier='pp36-manylinux_x86_64', path='/opt/python/pp36-pypy36_pp73'),
+        PythonConfiguration(identifier='cp35-manylinux_aarch64', path='/opt/python/cp35-cp35m'),
+        PythonConfiguration(identifier='cp36-manylinux_aarch64', path='/opt/python/cp36-cp36m'),
+        PythonConfiguration(identifier='cp37-manylinux_aarch64', path='/opt/python/cp37-cp37m'),
+        PythonConfiguration(identifier='cp38-manylinux_aarch64', path='/opt/python/cp38-cp38'),
+        PythonConfiguration(identifier='cp35-manylinux_ppc64le', path='/opt/python/cp35-cp35m'),
+        PythonConfiguration(identifier='cp36-manylinux_ppc64le', path='/opt/python/cp36-cp36m'),
+        PythonConfiguration(identifier='cp37-manylinux_ppc64le', path='/opt/python/cp37-cp37m'),
+        PythonConfiguration(identifier='cp38-manylinux_ppc64le', path='/opt/python/cp38-cp38'),
+        PythonConfiguration(identifier='cp35-manylinux_s390x', path='/opt/python/cp35-cp35m'),
+        PythonConfiguration(identifier='cp36-manylinux_s390x', path='/opt/python/cp36-cp36m'),
+        PythonConfiguration(identifier='cp37-manylinux_s390x', path='/opt/python/cp37-cp37m'),
+        PythonConfiguration(identifier='cp38-manylinux_s390x', path='/opt/python/cp38-cp38'),
     ]
-
     # skip builds as required
-    return [c for c in python_configurations if build_selector(c.identifier)]
+    return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)]
 
 
 def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment, manylinux_images):
@@ -49,6 +82,9 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef
     platforms = [
         ('cp', 'manylinux_x86_64', manylinux_images['x86_64']),
         ('cp', 'manylinux_i686', manylinux_images['i686']),
+        ('cp', 'manylinux_aarch64', manylinux_images['aarch64']),
+        ('cp', 'manylinux_ppc64le', manylinux_images['ppc64le']),
+        ('cp', 'manylinux_s390x', manylinux_images['s390x']),
         ('pp', 'manylinux_x86_64', manylinux_images['pypy_x86_64']),
     ]
 


Test patch:
diff --git a/test/01_basic/cibuildwheel_test.py b/test/01_basic/cibuildwheel_test.py
--- a/test/01_basic/cibuildwheel_test.py
+++ b/test/01_basic/cibuildwheel_test.py
@@ -1,4 +1,5 @@
 import os
+import platform
 
 import utils
 
@@ -20,7 +21,10 @@ def test_build_identifiers():
     # 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 '-manylinux' not in w or '-manylinux1' in w]
+    if platform.machine() in ['x86_64', 'i686']:
+        expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0')
+                           if '-manylinux' not in w or '-manylinux1' in w]
+    else:
+        expected_wheels = utils.expected_wheels('spam', '0.1.0')
     build_identifiers = utils.cibuildwheel_get_build_identifiers(project_dir)
     assert len(expected_wheels) == len(build_identifiers)
diff --git a/test/06_docker_images/cibuildwheel_test.py b/test/06_docker_images/cibuildwheel_test.py
--- a/test/06_docker_images/cibuildwheel_test.py
+++ b/test/06_docker_images/cibuildwheel_test.py
@@ -1,4 +1,5 @@
 import os
+import platform
 
 import pytest
 
@@ -10,6 +11,8 @@ def test():
 
     if utils.platform != 'linux':
         pytest.skip('the test is only relevant to the linux build')
+    if platform.machine() not in ['x86_64', 'i686']:
+        pytest.skip('this test is currently only possible on x86_64/i686 due to availability of alternative images')
 
     actual_wheels = utils.cibuildwheel_run(project_dir, add_env={
         'CIBW_MANYLINUX_X86_64_IMAGE': 'dockcross/manylinux2010-x64',
diff --git a/test/08_manylinuxXXXX_only/cibuildwheel_test.py b/test/08_manylinuxXXXX_only/cibuildwheel_test.py
--- a/test/08_manylinuxXXXX_only/cibuildwheel_test.py
+++ b/test/08_manylinuxXXXX_only/cibuildwheel_test.py
@@ -1,4 +1,5 @@
 import os
+import platform
 
 import pytest
 
@@ -11,6 +12,9 @@ def test(manylinux_image):
 
     if utils.platform != 'linux':
         pytest.skip('the docker test is only relevant to the linux build')
+    elif platform.machine not in ['x86_64', 'i686']:
+        if manylinux_image in ['manylinux1', 'manylinux2010']:
+            pytest.skip("manylinux1 and 2010 doesn't exist for non-x86 architectures")
 
     # build the wheels
     # CFLAGS environment variable is necessary to fail on 'malloc_info' (on manylinux1) during compilation/linking,
@@ -20,6 +24,9 @@ def test(manylinux_image):
         'CIBW_MANYLINUX_X86_64_IMAGE': manylinux_image,
         'CIBW_MANYLINUX_I686_IMAGE': manylinux_image,
         'CIBW_MANYLINUX_PYPY_X86_64_IMAGE': manylinux_image,
+        'CIBW_MANYLINUX_AARCH64_IMAGE': manylinux_image,
+        'CIBW_MANYLINUX_PPC64LE_IMAGE': manylinux_image,
+        'CIBW_MANYLINUX_S390X_IMAGE': manylinux_image,
     }
     if manylinux_image == 'manylinux1':
         # We don't have a manylinux1 image for PyPy
diff --git a/test/shared/utils.py b/test/shared/utils.py
--- a/test/shared/utils.py
+++ b/test/shared/utils.py
@@ -5,6 +5,7 @@
 '''
 
 import os
+import platform as pm
 import shutil
 import subprocess
 import sys
@@ -70,7 +71,7 @@ def cibuildwheel_run(project_path, env=None, add_env=None, output_dir=None):
     return wheels
 
 
-def expected_wheels(package_name, package_version, manylinux_versions=['manylinux1', 'manylinux2010'],
+def expected_wheels(package_name, package_version, manylinux_versions=None,
                     macosx_deployment_target=None):
     '''
     Returns a list of expected wheels from a run of cibuildwheel.
@@ -79,11 +80,21 @@ def expected_wheels(package_name, package_version, manylinux_versions=['manylinu
     # {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
     # {python tag} and {abi tag} are closely related to the python interpreter used to build the wheel
     # so we'll merge them below as python_abi_tag
-    python_abi_tags = ['cp27-cp27m', 'cp35-cp35m', 'cp36-cp36m', 'cp37-cp37m', 'cp38-cp38',
-                       'pp27-pypy_73', 'pp36-pypy36_pp73']
+
+    python_abi_tags = ['cp35-cp35m', 'cp36-cp36m', 'cp37-cp37m', 'cp38-cp38']
+    extra_x86_python_abi_tags = ['cp27-cp27m', 'pp27-pypy_73', 'pp36-pypy36_pp73']
+
     if platform == 'linux':
-        python_abi_tags.append('cp27-cp27mu')  # python 2.7 has 2 different ABI on manylinux
-        architectures = {'cp': ['x86_64', 'i686'], 'pp': ['x86_64']}
+        if pm.machine() not in ['x86_64', 'i686']:
+            if manylinux_versions is None:
+                manylinux_versions = ['manylinux2014']
+            architectures = {'cp': [pm.machine()]}
+        else:
+            if manylinux_versions is None:
+                manylinux_versions = ['manylinux1', 'manylinux2010']
+            python_abi_tags += extra_x86_python_abi_tags
+            python_abi_tags.append('cp27-cp27mu')  # python 2.7 has 2 different ABI on manylinux
+            architectures = {'cp': ['x86_64', 'i686'], 'pp': ['x86_64']}
         platform_tags = {}
         for python_implemention in architectures:
             platform_tags[python_implemention] = [
@@ -96,12 +107,15 @@ def expected_wheels(package_name, package_version, manylinux_versions=['manylinu
         def get_platform_tags(python_abi_tag):
             return platform_tags[python_abi_tag[:2]]
     elif platform == 'windows':
+        python_abi_tags += extra_x86_python_abi_tags
         platform_tags = {'cp': ['win32', 'win_amd64'], 'pp': ['win32']}
 
         def get_platform_tags(python_abi_tag):
             return platform_tags[python_abi_tag[:2]]
 
     elif platform == 'macos':
+        python_abi_tags += extra_x86_python_abi_tags
+
         def get_platform_tags(python_abi_tag):
             default_version = '10.7' if python_abi_tag.startswith('pp') else '10.9'
             return ['macosx_{}_x86_64'.format((macosx_deployment_target or default_version).replace('.', '_'))]