diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ab6656cb..a9d6f7ecb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest] python_version: ['3.7'] timeout-minutes: 180 steps: @@ -55,3 +55,24 @@ jobs: - name: Test cibuildwheel run: | python ./bin/run_tests.py + + test-emulated: + name: Test emulated cibuildwheel using qemu + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install -r requirements-dev.txt + + - name: Set up QEMU + id: qemu + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + - name: Run the emulation tests + run: | + pytest --run-emulation test/test_emulation.py diff --git a/bin/run_example_ci_configs.py b/bin/run_example_ci_configs.py index 33f54de37..9ffe1717b 100755 --- a/bin/run_example_ci_configs.py +++ b/bin/run_example_ci_configs.py @@ -8,6 +8,7 @@ import textwrap import time import click +from glob import glob from collections import namedtuple from urllib.parse import quote @@ -30,49 +31,70 @@ def generate_basic_project(path): project.generate(path) -CIService = namedtuple('CIService', 'name src_config_path dst_config_path badge_md') +CIService = namedtuple('CIService', 'name dst_config_path badge_md') services = [ CIService( name='appveyor', - src_config_path='examples/appveyor-minimal.yml', dst_config_path='appveyor.yml', badge_md='[![Build status](https://ci.appveyor.com/api/projects/status/wbsgxshp05tt1tif/branch/{branch}?svg=true)](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/{branch})', ), CIService( name='azure-pipelines', - src_config_path='examples/azure-pipelines-minimal.yml', dst_config_path='azure-pipelines.yml', badge_md='[![Build Status](https://dev.azure.com/joerick0429/cibuildwheel/_apis/build/status/joerick.cibuildwheel?branchName={branch})](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=2&branchName={branch})', ), CIService( - name='circle-ci', - src_config_path='examples/circleci-minimal.yml', + name='circleci', dst_config_path='.circleci/config.yml', badge_md='[![CircleCI](https://circleci.com/gh/joerick/cibuildwheel/tree/{branch_escaped}.svg?style=svg)](https://circleci.com/gh/joerick/cibuildwheel/tree/{branch})', ), CIService( name='github', - src_config_path='examples/github-minimal.yml', dst_config_path='.github/workflows/example.yml', badge_md='[![Build](https://github.com/joerick/cibuildwheel/workflows/Build/badge.svg?branch={branch})](https://github.com/joerick/cibuildwheel/actions)', ), CIService( name='travis-ci', - src_config_path='examples/travis-ci-minimal.yml', dst_config_path='.travis.yml', badge_md='[![Build Status](https://travis-ci.org/joerick/cibuildwheel.svg?branch={branch})](https://travis-ci.org/joerick/cibuildwheel)', ), CIService( name='gitlab', - src_config_path='examples/gitlab-minimal.yml', dst_config_path='.gitlab-ci.yml', badge_md='[![Gitlab](https://gitlab.com/joerick/cibuildwheel/badges/{branch}/pipeline.svg)](https://gitlab.com/joerick/cibuildwheel/-/commits/{branch})' ), ] +def ci_service_for_config_file(config_file): + service_name = Path(config_file).name.rsplit('-', 1)[0] + + for service in services: + if service.name == service_name: + return service + + raise ValueError(f'unknown ci service for config file {config_file}') + + @click.command() -def run_example_ci_configs(): +@click.argument('config_files', nargs=-1, type=click.Path()) +def run_example_ci_configs(config_files=None): + ''' + Test the example configs. If no files are specified, will test + examples/*-minimal.yml + ''' + + if len(config_files) == 0: + config_files = glob('examples/*-minimal.yml') + + # check each CI service has at most 1 config file + configs_by_service = {} + for config_file in config_files: + service = ci_service_for_config_file(config_file) + if service.name in configs_by_service: + raise Exception('You cannot specify more than one config per CI service') + configs_by_service[service.name] = config_file + if git_repo_has_changes(): print('Your git repo has uncommitted changes. Commit or stash before continuing.') exit(1) @@ -91,18 +113,19 @@ def run_example_ci_configs(): example_project = Path('example_root') generate_basic_project(example_project) - for service in services: - src_config_file = Path(service.src_config_path) + for config_file in config_files: + service = ci_service_for_config_file(config_file) + src_config_file = Path(config_file) dst_config_file = example_project / service.dst_config_path dst_config_file.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(src_config_file, dst_config_file) run(['git', 'add', example_project], check=True) - run(['git', 'commit', '-m', textwrap.dedent(f''' + run(['git', 'commit', '--no-verify', '-m', textwrap.dedent(f''' Test example minimal configs - Testing files: {[s.src_config_path for s in services]} + Testing files: {config_files} Generated from branch: {previous_branch} Time: {timestamp} ''')], check=True) @@ -116,9 +139,10 @@ def run_example_ci_configs(): print('> ') print('> | Service | Config | Status |') print('> |---|---|---|') - for service in services: + for config_file in config_files: + service = ci_service_for_config_file(config_file) badge = service.badge_md.format(branch=branch_name, branch_escaped=quote(branch_name, safe='')) - print(f'> | {service.name} | `{service.src_config_path}` | {badge} |') + print(f'> | {service.name} | `{config_file}` | {badge} |') print('> ') print('> Generated by `bin/run_example_ci_config.py`') print() diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index e171ab20d..5f48070f6 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -17,6 +17,7 @@ parse_environment, ) from cibuildwheel.util import ( + Architecture, BuildOptions, BuildSelector, DependencyConstraints, @@ -67,6 +68,17 @@ def main() -> None: run in Windows, and it will build and test for all versions of Python. Default: auto. ''') + + parser.add_argument('--archs', + default=None, + help=''' + Comma-separated list of CPU architectures to build for. + When set to 'auto', builds the architectures natively supported + on this machine. Set this option to build an architecture + via emulation, for example, using binfmt_misc and QEMU. + Default: auto. + Choices: auto, {} + '''.format(", ".join(a.name for a in Architecture))) parser.add_argument('--output-dir', default=os.environ.get('CIBW_OUTPUT_DIR', 'wheelhouse'), help='Destination folder for the wheels.') @@ -168,8 +180,14 @@ def main() -> None: print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr) exit(2) + if args.archs is not None: + archs_config_str = args.archs + else: + archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto') + archs = Architecture.parse_config(archs_config_str, platform=platform) + if args.print_build_identifiers: - print_build_identifiers(platform, build_selector) + print_build_identifiers(platform, build_selector, archs) exit(0) manylinux_images: Optional[Dict[str, str]] = None @@ -202,6 +220,7 @@ def main() -> None: manylinux_images[build_platform] = image build_options = BuildOptions( + architectures=archs, package_dir=package_dir, output_dir=output_dir, test_command=test_command, @@ -284,10 +303,12 @@ def print_preamble(platform: str, build_options: BuildOptions) -> None: print('\nHere we go!\n') -def print_build_identifiers(platform: str, build_selector: BuildSelector) -> None: +def print_build_identifiers( + platform: str, build_selector: BuildSelector, architectures: List[Architecture] +) -> None: python_configurations: List[Any] = [] if platform == 'linux': - python_configurations = cibuildwheel.linux.get_python_configurations(build_selector) + python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures) elif platform == 'windows': python_configurations = cibuildwheel.windows.get_python_configurations(build_selector) elif platform == 'macos': diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index cfd4952c5..ce1bc057f 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -1,4 +1,3 @@ -import platform import subprocess import sys import textwrap @@ -8,29 +7,10 @@ from .docker_container import DockerContainer from .logger import log -from .util import (BuildOptions, BuildSelector, NonPlatformWheelError, - get_build_verbosity_extra_flags, prepare_command) - - -def matches_platform(identifier: str) -> bool: - 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 +from .util import ( + Architecture, BuildOptions, BuildSelector, NonPlatformWheelError, + get_build_verbosity_extra_flags, prepare_command, +) class PythonConfiguration(NamedTuple): @@ -43,7 +23,9 @@ def path(self): return PurePath(self.path_str) -def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfiguration]: +def get_python_configurations( + build_selector: BuildSelector, architectures: List[Architecture] +) -> List[PythonConfiguration]: python_configurations = [ PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path_str='/opt/python/cp27-cp27m'), PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path_str='/opt/python/cp27-cp27mu'), @@ -78,8 +60,14 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path_str='/opt/python/cp38-cp38'), PythonConfiguration(version='3.9', identifier='cp39-manylinux_s390x', path_str='/opt/python/cp39-cp39'), ] - # skip builds as required - return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)] + + # return all configurations whose arch is in our `architectures` list, + # and match the build/skip rules + return [ + c for c in python_configurations + if any(c.identifier.endswith(arch.value) for arch in architectures) + and build_selector(c.identifier) + ] def build(options: BuildOptions) -> None: @@ -93,7 +81,7 @@ def build(options: BuildOptions) -> None: exit(2) assert options.manylinux_images is not None - python_configurations = get_python_configurations(options.build_selector) + python_configurations = get_python_configurations(options.build_selector, options.architectures) platforms = [ ('cp', 'manylinux_x86_64', options.manylinux_images['x86_64']), ('cp', 'manylinux_i686', options.manylinux_images['i686']), diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 9ffca89dd..568635502 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -4,13 +4,14 @@ import subprocess import sys import tempfile +import textwrap from os import PathLike from pathlib import Path from typing import Dict, List, NamedTuple, Optional, Sequence, Union from .environment import ParsedEnvironment from .logger import log -from .util import (BuildOptions, BuildSelector, NonPlatformWheelError, +from .util import (Architecture, BuildOptions, BuildSelector, NonPlatformWheelError, download, get_build_verbosity_extra_flags, get_pip_script, install_certifi_script, prepare_command) @@ -182,6 +183,12 @@ def setup_python(python_configuration: PythonConfiguration, def build(options: BuildOptions) -> None: + if options.architectures != [Architecture.x86_64]: + raise ValueError(textwrap.dedent(f''' + Invalid archs option {options.architectures}. macOS only supports x86_64 for the moment. + If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead. + ''')) + temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel')) built_wheel_dir = temp_dir / 'built_wheel' repaired_wheel_dir = temp_dir / 'repaired_wheel' diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 87ce5d178..70eaa44d1 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -1,4 +1,6 @@ import os +import platform as platform_module +import re import ssl import textwrap import urllib.request @@ -121,10 +123,45 @@ def __repr__(self): return f'{self.__class__.__name__}{self.base_file_path!r})' +class Architecture(Enum): + # mac/linux archs + x86_64 = 'x86_64' + i686 = 'i686' + aarch64 = 'aarch64' + ppc64le = 'ppc64le' + s390x = 's390x' + + # windows archs + x86 = 'x86' + AMD64 = 'AMD64' + + @staticmethod + def parse_config(config: str, platform: str) -> 'List[Architecture]': + result = [] + for arch_str in re.split(r'[\s,]+', config): + if arch_str == 'auto': + result += Architecture.auto_archs(platform=platform) + else: + result.append(Architecture(arch_str)) + return result + + @staticmethod + def auto_archs(platform: str) -> 'List[Architecture]': + native_architecture = Architecture(platform_module.machine()) + result = [native_architecture] + if platform == 'linux' and native_architecture == Architecture.x86_64: + # x86_64 machines can run i686 docker containers + result.append(Architecture.i686) + if platform == 'windows' and native_architecture == Architecture.AMD64: + result.append(Architecture.x86) + return result + + class BuildOptions(NamedTuple): package_dir: Path output_dir: Path build_selector: BuildSelector + architectures: List[Architecture] environment: ParsedEnvironment before_all: str before_build: Optional[str] @@ -164,7 +201,7 @@ def strtobool(val: str) -> bool: return False -class CIProvider(str, Enum): +class CIProvider(Enum): travis_ci = 'travis' appveyor = 'appveyor' circle_ci = 'circle_ci' diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 4331915ac..bbd0dfc29 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -3,6 +3,7 @@ import subprocess import sys import tempfile +import textwrap from os import PathLike from pathlib import Path from typing import Dict, List, NamedTuple, Optional, Sequence, Union @@ -12,7 +13,7 @@ from .environment import ParsedEnvironment from .logger import log -from .util import (BuildOptions, BuildSelector, NonPlatformWheelError, +from .util import (Architecture, BuildOptions, BuildSelector, NonPlatformWheelError, download, get_build_verbosity_extra_flags, get_pip_script, prepare_command) @@ -201,6 +202,13 @@ def pep_518_cp35_workaround(package_dir: Path, env: Dict[str, str]) -> None: def build(options: BuildOptions) -> None: + if options.architectures != [Architecture.AMD64, Architecture.x86]: + raise ValueError(textwrap.dedent(f''' + Invalid archs option {options.architectures}. Windows only supports 'amd64,x86' for the + moment. If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX + instead. + ''')) + temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel')) built_wheel_dir = temp_dir / 'built_wheel' repaired_wheel_dir = temp_dir / 'repaired_wheel' diff --git a/docs/faq.md b/docs/faq.md index 2ff777f6b..5fd02f648 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -24,6 +24,26 @@ Linux wheels are built in the [`manylinux` docker images](https://github.com/pyp - Alternative dockers images can be specified with the `CIBW_MANYLINUX_X86_64_IMAGE`, `CIBW_MANYLINUX_I686_IMAGE`, and `CIBW_MANYLINUX_PYPY_X86_64_IMAGE` options to allow for a custom, preconfigured build environment for the Linux builds. See [options](options.md#manylinux-image) for more details. +### Building non-native architectures using emulation {: #emulation} + +cibuildwheel supports building non-native architectures on Linux, via +emulation through the binfmt_misc kernel feature. The easiest way to use this +is via the [docker/setup-qemu-action][setup-qemu-action] on Github Actions or +[tonistiigi/binfmt][binfmt]. + +[setup-qemu-action]: https://github.com/docker/setup-qemu-action +[binfmt]: https://hub.docker.com/r/tonistiigi/binfmt + +Check out the following config for an example of how to set it up on Github +Actions. Once QEMU is set up and registered, you just need to set the +`CIBW_ARCHS_LINUX` environment variable (or use the `--archs` option on +Linux), and the other architectures are emulated automatically. + +> .github/workflows/build.yml +```yaml +{% include "../examples/github-with-qemu.yml" %} +``` + ### Building packages with optional C extensions `cibuildwheel` defines the environment variable `CIBUILDWHEEL` to the value `1` allowing projects for which the C extension is optional to make it mandatory when building wheels. diff --git a/docs/options.md b/docs/options.md index 73c25b3b5..e12ad1865 100644 --- a/docs/options.md +++ b/docs/options.md @@ -155,6 +155,28 @@ CIBW_SKIP: pp* } +### `CIBW_ARCHS_LINUX` {: #archs} +> Build non-native architectures + +A space-separated list of architectures to build. Use this in conjunction with +emulation, such as that provided by [docker/setup-qemu-action][setup-qemu-action] +or [tonistiigi/binfmt][binfmt], to build architectures other than those your +machine natively supports. + +Options: `auto` `x86_64` `i686` `aarch64` `ppc64le` `s390x` + +Default: `auto`, meaning the native archs supported on the build machine. For +example, on an `x86_64` machine, `auto` expands to `x86_64` and `i686`. + +[setup-qemu-action]: https://github.com/docker/setup-qemu-action +[binfmt]: https://hub.docker.com/r/tonistiigi/binfmt + +#### Examples + +```yaml +# On an intel runner with qemu installed, build Intel and ARM wheels +CIBW_ARCHS_LINUX: "auto aarch64" +``` ## Build customization @@ -488,7 +510,8 @@ CIBW_BUILD_VERBOSITY: 1 ```text usage: cibuildwheel [-h] [--platform {auto,linux,macos,windows}] - [--output-dir OUTPUT_DIR] [--print-build-identifiers] + [--archs ARCHS] [--output-dir OUTPUT_DIR] + [--print-build-identifiers] [package_dir] Build wheels for all the platforms. @@ -510,6 +533,12 @@ optional arguments: don't run on your development machine. For "windows", you need to run in Windows, and it will build and test for all versions of Python. Default: auto. + --archs ARCHS Comma-separated list of CPU architectures to build + for. If unspecified, builds the architectures natively + supported on this machine. Set this option to build an + architecture via emulation, for example, using + binfmt_misc and qemu. Default: auto Choices: auto, + x86_64, i686, aarch64, ppc64le, s390x, x86, AMD64 --output-dir OUTPUT_DIR Destination folder for the wheels. --print-build-identifiers diff --git a/examples/github-deploy.yml b/examples/github-deploy.yml index 5e33a4b1b..49bd5cc0b 100644 --- a/examples/github-deploy.yml +++ b/examples/github-deploy.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04, windows-latest, macos-latest] + os: [ubuntu-18.04, windows-2019, macos-10.15] steps: - uses: actions/checkout@v2 diff --git a/examples/github-minimal.yml b/examples/github-minimal.yml index 9c3620f38..b7b4a1aaf 100644 --- a/examples/github-minimal.yml +++ b/examples/github-minimal.yml @@ -8,7 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04, windows-latest, macos-latest] + os: [ubuntu-18.04, windows-2019, macos-10.15] steps: - uses: actions/checkout@v2 diff --git a/examples/github-with-qemu.yml b/examples/github-with-qemu.yml new file mode 100644 index 000000000..95a980f2b --- /dev/null +++ b/examples/github-with-qemu.yml @@ -0,0 +1,46 @@ +name: Build + +on: [push, pull_request] + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04, windows-2019, macos-10.15] + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.7' + + - name: Install cibuildwheel + run: | + python -m pip install cibuildwheel==1.7.1 + + - name: Install Visual C++ for Python 2.7 + if: runner.os == 'Windows' + run: | + choco install vcpython27 -f -y + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + - name: Build wheels + run: | + python -m cibuildwheel --output-dir wheelhouse + env: + # configure cibuildwheel to build native archs ('auto'), and some + # emulated ones + CIBW_ARCHS_LINUX: auto aarch64 ppc64le s390x + + - uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..410c07076 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--run-emulation", action="store_true", default=False, help="run emulation tests" + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "emulation: mark test requiring qemu binfmt_misc to run") + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--run-emulation"): + # --run-emulation given in cli: do not skip emulation tests + return + skip_emulation = pytest.mark.skip(reason="need --run-emulation option to run") + for item in items: + if "emulation" in item.keywords: + item.add_marker(skip_emulation) diff --git a/test/test_emulation.py b/test/test_emulation.py new file mode 100644 index 000000000..e486327ca --- /dev/null +++ b/test/test_emulation.py @@ -0,0 +1,53 @@ +import subprocess +import pytest +from . import utils +from . import test_projects + + +project_with_a_test = test_projects.new_c_project() + +project_with_a_test.files['test/spam_test.py'] = r''' +import spam + +def test_spam(): + assert spam.system('python -c "exit(0)"') == 0 + assert spam.system('python -c "exit(1)"') != 0 +''' + + +@pytest.mark.emulation +def test(tmp_path): + project_dir = tmp_path / 'project' + project_with_a_test.generate(project_dir) + + # build and test the wheels + actual_wheels = utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_TEST_REQUIRES': 'pytest', + 'CIBW_TEST_COMMAND': 'pytest {project}/test', + 'CIBW_ARCHS': 'aarch64 ppc64le s390x', + }) + + # also check that we got the right wheels + expected_wheels = ( + utils.expected_wheels('spam', '0.1.0', machine_arch='aarch64') + + utils.expected_wheels('spam', '0.1.0', machine_arch='ppc64le') + + utils.expected_wheels('spam', '0.1.0', machine_arch='s390x') + ) + assert set(actual_wheels) == set(expected_wheels) + + +def test_setting_arch_on_other_platforms(tmp_path, capfd): + if utils.platform == 'linux': + pytest.skip('this test checks the behaviour on platforms other than linux') + + project_dir = tmp_path / 'project' + project_with_a_test.generate(project_dir) + + # build and test the wheels + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_ARCHS': 'aarch64', + }) + + captured = capfd.readouterr() + assert "Invalid archs option" in captured.err diff --git a/test/utils.py b/test/utils.py index 04f3e1476..bae3853b9 100644 --- a/test/utils.py +++ b/test/utils.py @@ -76,7 +76,7 @@ def cibuildwheel_run(project_path, package_dir='.', env=None, add_env=None, outp def expected_wheels(package_name, package_version, manylinux_versions=None, - macosx_deployment_target='10.9'): + macosx_deployment_target='10.9', machine_arch=None): ''' Returns a list of expected wheels from a run of cibuildwheel. ''' @@ -85,15 +85,18 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, # {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 + if machine_arch is None: + machine_arch = pm.machine() + if manylinux_versions is None: - if pm.machine() == 'x86_64': + if machine_arch == 'x86_64': manylinux_versions = ['manylinux1', 'manylinux2010'] else: manylinux_versions = ['manylinux2014'] python_abi_tags = ['cp35-cp35m', 'cp36-cp36m', 'cp37-cp37m', 'cp38-cp38', 'cp39-cp39'] - if pm.machine() in ['x86_64', 'AMD64', 'x86']: + if machine_arch in ['x86_64', 'AMD64', 'x86']: python_abi_tags += ['cp27-cp27m', 'pp27-pypy_73', 'pp36-pypy36_pp73', 'pp37-pypy37_pp73'] if platform == 'linux': @@ -105,9 +108,9 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, platform_tags = [] if platform == 'linux': - architectures = [pm.machine()] + architectures = [machine_arch] - if pm.machine() == 'x86_64' and python_abi_tag.startswith('cp'): + if machine_arch == 'x86_64' and python_abi_tag.startswith('cp'): architectures.append('i686') platform_tags = [ diff --git a/unit_test/main_tests/conftest.py b/unit_test/main_tests/conftest.py index 3f684b6c0..8a1655371 100644 --- a/unit_test/main_tests/conftest.py +++ b/unit_test/main_tests/conftest.py @@ -70,5 +70,14 @@ def platform(request, monkeypatch): @pytest.fixture def intercepted_build_args(platform, monkeypatch): intercepted = ArgsInterceptor() - monkeypatch.setattr(globals()[platform], 'build', intercepted) + + if platform == 'linux': + monkeypatch.setattr(linux, 'build', intercepted) + elif platform == 'macos': + monkeypatch.setattr(macos, 'build', intercepted) + elif platform == 'windows': + monkeypatch.setattr(windows, 'build', intercepted) + else: + raise ValueError(f'unknown platform value: {platform}') + return intercepted diff --git a/unit_test/main_tests/main_platform_test.py b/unit_test/main_tests/main_platform_test.py index 004855e8d..9ac84ea85 100644 --- a/unit_test/main_tests/main_platform_test.py +++ b/unit_test/main_tests/main_platform_test.py @@ -1,3 +1,5 @@ +import platform as platform_module +from cibuildwheel.util import Architecture import sys import pytest @@ -64,3 +66,48 @@ def test_platform_environment(platform, intercepted_build_args, monkeypatch): main() assert intercepted_build_args.args[0].package_dir == MOCK_PACKAGE_DIR + + +def test_archs_default(platform, intercepted_build_args, monkeypatch): + monkeypatch.setattr(platform_module, 'machine', lambda: 'x86_64') + + main() + build_options = intercepted_build_args.args[0] + + if platform == 'linux': + assert build_options.architectures == [Architecture.x86_64, Architecture.i686] + else: + assert build_options.architectures == [Architecture.x86_64] + + +@pytest.mark.parametrize('use_env_var', [False, True]) +def test_archs_argument(platform, intercepted_build_args, monkeypatch, use_env_var): + monkeypatch.setattr(platform_module, 'machine', lambda: 'x86_64') + if use_env_var: + monkeypatch.setenv('CIBW_ARCHS', 'ppc64le') + else: + monkeypatch.setenv('CIBW_ARCHS', 'unused') + monkeypatch.setattr(sys, 'argv', sys.argv + ['--archs', 'ppc64le']) + + main() + build_options = intercepted_build_args.args[0] + + assert build_options.architectures == [Architecture.ppc64le] + + +def test_archs_platform_specific(platform, intercepted_build_args, monkeypatch): + monkeypatch.setattr(platform_module, 'machine', lambda: 'x86_64') + monkeypatch.setenv('CIBW_ARCHS', 'unused') + monkeypatch.setenv('CIBW_ARCHS_LINUX', 'ppc64le') + monkeypatch.setenv('CIBW_ARCHS_WINDOWS', 'x86') + monkeypatch.setenv('CIBW_ARCHS_MACOS', 'x86_64') + + main() + build_options = intercepted_build_args.args[0] + + if platform == 'linux': + assert build_options.architectures == [Architecture.ppc64le] + elif platform == 'windows': + assert build_options.architectures == [Architecture.x86] + elif platform == 'macos': + assert build_options.architectures == [Architecture.x86_64]