diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index d3ed3492..20ee2baa 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -312,7 +312,7 @@ def print_build_identifiers( elif platform == 'windows': python_configurations = cibuildwheel.windows.get_python_configurations(build_selector) elif platform == 'macos': - python_configurations = cibuildwheel.macos.get_python_configurations(build_selector) + python_configurations = cibuildwheel.macos.get_python_configurations(build_selector, architectures) for config in python_configurations: print(config.identifier) diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 0dd5ebac..e12ea648 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -46,7 +46,8 @@ class PythonConfiguration(NamedTuple): url: str -def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfiguration]: +def get_python_configurations(build_selector: BuildSelector, + architectures: List[Architecture]) -> List[PythonConfiguration]: python_configurations = [ # CPython PythonConfiguration(version='2.7', identifier='cp27-macosx_x86_64', url='https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg'), @@ -58,19 +59,22 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi PythonConfiguration(version='3.9', identifier='cp39-macosx_universal2', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'), PythonConfiguration(version='3.9', identifier='cp39-macosx_arm64', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'), # PyPy - # TODO: may not support 11.0 yet PythonConfiguration(version='2.7', identifier='pp27-macosx_x86_64', url='https://downloads.python.org/pypy/pypy2.7-v7.3.3-osx64.tar.bz2'), PythonConfiguration(version='3.6', identifier='pp36-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.6-v7.3.3-osx64.tar.bz2'), PythonConfiguration(version='3.7', identifier='pp37-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2'), ] - # skip builds as required + # filter out configs that don't match any of the selected architectures + python_configurations = [c for c in python_configurations + if any(c.identifier.endswith(a.value) for a in architectures)] + + # skip builds as required by BUILD/SKIP python_configurations = [c for c in python_configurations if build_selector(c.identifier)] if get_macos_version() >= (11, 0): - # pypy doesn't work on macOS 11 yet - # See https://foss.heptapod.net/pypy/pypy/-/issues/3314 if any(c.identifier.startswith('pp') for c in python_configurations): + # pypy doesn't work on macOS 11 yet + # See https://foss.heptapod.net/pypy/pypy/-/issues/3314 log.warning(wrap_text(''' PyPy is currently unsupported when building on macOS 11. To build macOS PyPy wheels, build on an older OS, such as macOS 10.15. To silence this warning, deselect PyPy by @@ -81,14 +85,12 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi if any(c.identifier.startswith('cp35') for c in python_configurations): # CPython 3.5 doesn't work on macOS 11 log.warning(wrap_text(''' - CPython is unsupported when building on macOS 11. To build CPython 3.5 wheels, build - on an older OS, such as macOS 10.15. To silence this warning, deselect CPython 3.5 - by adding "cp35-macosx_x86_64" to your CIBW_SKIP option. + CPython 3.5 is unsupported when building on macOS 11. To build CPython 3.5 wheels, + build on an older OS, such as macOS 10.15. To silence this warning, deselect CPython + 3.5 by adding "cp35-macosx_x86_64" to your CIBW_SKIP option. ''')) python_configurations = [c for c in python_configurations if not c.identifier.startswith('cp35')] - python_configurations = [c for c in python_configurations if not c.identifier.startswith('cp35')] - return python_configurations @@ -236,10 +238,11 @@ def setup_python(python_configuration: PythonConfiguration, def build(options: BuildOptions) -> None: - if options.architectures != [Architecture.x86_64]: + allowed_archs = {Architecture.x86_64, Architecture.universal2, Architecture.arm64} + if set(options.architectures) <= allowed_archs: 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. + Invalid archs option {options.architectures}. macOS only supports + these architectures: {', '.join(a.value for a in allowed_archs)}. ''')) temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel')) @@ -253,7 +256,7 @@ def build(options: BuildOptions) -> None: before_all_prepared = prepare_command(options.before_all, project='.', package=options.package_dir) call([before_all_prepared], shell=True, env=env) - python_configurations = get_python_configurations(options.build_selector) + python_configurations = get_python_configurations(options.build_selector, options.architectures) for config in python_configurations: log.build_start(config.identifier) diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 3b4935ff..3654b51e 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -127,11 +127,17 @@ class DependencyConstraints: class Architecture(Enum): # mac/linux archs x86_64 = 'x86_64' + + # linux archs i686 = 'i686' aarch64 = 'aarch64' ppc64le = 'ppc64le' s390x = 's390x' + # mac archs + universal2 = 'universal2' + arm64 = 'arm64' + # windows archs x86 = 'x86' AMD64 = 'AMD64' @@ -150,11 +156,18 @@ class Architecture(Enum): 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) + + if platform == 'macos' and native_architecture == Architecture.arm64: + # arm64 can build and test both archs of a universal2 wheel. + result.append(Architecture.universal2) + return result diff --git a/test/test_macos_archs.py b/test/test_macos_archs.py new file mode 100644 index 00000000..cfcaddeb --- /dev/null +++ b/test/test_macos_archs.py @@ -0,0 +1,25 @@ +import pytest + +from . import test_projects, utils + +basic_project = test_projects.new_c_project() + + +def test_cross_compiled_build(tmp_path): + if utils.platform != 'macos': + pytest.skip('this test is only relevant to macos') + + project_dir = tmp_path / 'project' + basic_project.generate(project_dir) + + actual_wheels = utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_ARCHS': 'x86_64, universal2, arm64', + }) + + expected_wheels = ( + utils.expected_wheels('spam', '0.1.0', machine_arch='x86_64') + + utils.expected_wheels('spam', '0.1.0', machine_arch='arm64') + ) + assert set(actual_wheels) == set(expected_wheels) + +# TODO: add a TEST_COMMAND test when using cross-compiling diff --git a/test/utils.py b/test/utils.py index d8aa50ee..5be952d0 100644 --- a/test/utils.py +++ b/test/utils.py @@ -100,9 +100,11 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, if platform == 'linux': python_abi_tags.append('cp27-cp27mu') # python 2.7 has 2 different ABI on manylinux - if platform == 'macos': - # TODO: perhaps drop Python 3.5 across the board? + if platform == 'macos' and get_macos_version() >= (11, 0): + # CPython 3.5 doesn't work on macOS 11. python_abi_tags.remove('cp35-cp35m') + # pypy not supported on macOS 11. + python_abi_tags = [t for t in python_abi_tags if not t.startswith('pp')] wheels = [] @@ -129,11 +131,17 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, elif platform == 'macos': if python_abi_tag == 'cp39-cp39': - platform_tags = [ - f'macosx_{macosx_deployment_target.replace(".", "_")}_x86_64', - f'macosx_{macosx_deployment_target.replace(".", "_")}_universal2.macosx_11_0_universal2', - 'macosx_11_0_arm64', - ] + if machine_arch == 'x86_64': + platform_tags = [ + f'macosx_{macosx_deployment_target.replace(".", "_")}_x86_64', + ] + elif machine_arch == 'arm64': + platform_tags = [ + f'macosx_{macosx_deployment_target.replace(".", "_")}_universal2.macosx_11_0_universal2', + # macosx_deployment_target is ignored on arm64, because arm64 isn't supported on + # macOS earlier than 11.0 + 'macosx_11_0_arm64', + ] else: platform_tags = [ f'macosx_{macosx_deployment_target.replace(".", "_")}_x86_64', @@ -152,6 +160,18 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, return wheels +def get_macos_version(): + ''' + Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0) + + These tuples can be used in comparisons, e.g. + (10, 14) <= (11, 0) == True + (11, 2) <= (11, 0) != True + ''' + version_str, _, _ = pm.mac_ver() + return tuple(map(int, version_str.split(".")[:2])) + + platform = None if 'CIBW_PLATFORM' in os.environ: