diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index f5a7ace9..8f65f28b 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -5,16 +5,16 @@ import textwrap import traceback from configparser import ConfigParser from pathlib import Path -from typing import Any, Dict, List, Optional, Set, overload +from typing import Dict, List, Optional, Set, Union, overload import cibuildwheel import cibuildwheel.linux import cibuildwheel.macos import cibuildwheel.windows +from cibuildwheel.architecture import Architecture, allowed_architectures_check from cibuildwheel.environment import EnvironmentParseError, parse_environment from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never from cibuildwheel.util import ( - Architecture, BuildOptions, BuildSelector, DependencyConstraints, @@ -95,6 +95,9 @@ def main() -> None: parser.add_argument('--print-build-identifiers', action='store_true', help='Print the build identifiers matched by the current invocation and exit.') + parser.add_argument('--allow-empty', + action='store_true', + help='Do not report an error code if the build does not match any wheels.') args = parser.parse_args() @@ -194,8 +197,11 @@ def main() -> None: archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto') archs = Architecture.parse_config(archs_config_str, platform=platform) + identifiers = get_build_identifiers(platform, build_selector, archs) + if args.print_build_identifiers: - print_build_identifiers(platform, build_selector, archs) + for identifier in identifiers: + print(identifier) sys.exit(0) manylinux_images: Optional[Dict[str, str]] = None @@ -251,6 +257,17 @@ def main() -> None: print_preamble(platform, build_options) + try: + allowed_architectures_check(platform, build_options.architectures) + except ValueError as err: + print("cibuildwheel:", *err.args, file=sys.stderr) + sys.exit(4) + + if not identifiers: + print(f'cibuildwheel: No build identifiers selected: {build_selector}', file=sys.stderr) + if not args.allow_empty: + sys.exit(3) + if not output_dir.exists(): output_dir.mkdir(parents=True) @@ -311,20 +328,22 @@ def print_preamble(platform: str, build_options: BuildOptions) -> None: print('\nHere we go!\n') -def print_build_identifiers( - platform: str, build_selector: BuildSelector, architectures: Set[Architecture] -) -> None: - - python_configurations: List[Any] = [] +def get_build_identifiers( + platform: PlatformName, build_selector: BuildSelector, architectures: Set[Architecture] +) -> List[str]: + python_configurations: Union[List[cibuildwheel.linux.PythonConfiguration], + List[cibuildwheel.windows.PythonConfiguration], + List[cibuildwheel.macos.PythonConfiguration]] if platform == 'linux': python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures) elif platform == 'windows': python_configurations = cibuildwheel.windows.get_python_configurations(build_selector, architectures) elif platform == 'macos': python_configurations = cibuildwheel.macos.get_python_configurations(build_selector) + else: + assert_never(platform) - for config in python_configurations: - print(config.identifier) + return [config.identifier for config in python_configurations] def detect_warnings(platform: str, build_options: BuildOptions) -> List[str]: diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py new file mode 100644 index 00000000..f7bf4354 --- /dev/null +++ b/cibuildwheel/architecture.py @@ -0,0 +1,86 @@ +import functools +import platform as platform_module +import re +from enum import Enum +from typing import Set + +from .typing import PlatformName, assert_never + +PRETTY_NAMES = {'linux': 'Linux', 'macos': 'macOS', 'windows': 'Windows'} + + +@functools.total_ordering +class Architecture(Enum): + value: str + + # mac/linux archs + x86_64 = 'x86_64' + i686 = 'i686' + aarch64 = 'aarch64' + ppc64le = 'ppc64le' + s390x = 's390x' + + # windows archs + x86 = 'x86' + AMD64 = 'AMD64' + + # Allow this to be sorted + def __lt__(self, other: "Architecture") -> bool: + return self.value < other.value + + @staticmethod + def parse_config(config: str, platform: PlatformName) -> 'Set[Architecture]': + result = set() + for arch_str in re.split(r'[\s,]+', config): + if arch_str == 'auto': + result |= Architecture.auto_archs(platform=platform) + elif arch_str == 'native': + result.add(Architecture(platform_module.machine())) + elif arch_str == 'all': + result |= Architecture.all_archs(platform=platform) + else: + result.add(Architecture(arch_str)) + return result + + @staticmethod + def auto_archs(platform: PlatformName) -> 'Set[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.add(Architecture.i686) + if platform == 'windows' and native_architecture == Architecture.AMD64: + result.add(Architecture.x86) + return result + + @staticmethod + def all_archs(platform: PlatformName) -> 'Set[Architecture]': + if platform == 'linux': + return {Architecture.x86_64, Architecture.i686, Architecture.aarch64, Architecture.ppc64le, Architecture.s390x} + elif platform == 'macos': + return {Architecture.x86_64} + elif platform == 'windows': + return {Architecture.x86, Architecture.AMD64} + else: + assert_never(platform) + + +def allowed_architectures_check( + platform: PlatformName, + architectures: Set[Architecture], +) -> None: + + allowed_architectures = Architecture.all_archs(platform) + + msg = f'{PRETTY_NAMES[platform]} only supports {sorted(allowed_architectures)} at the moment.' + + if platform != 'linux': + msg += ' If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead.' + + if not architectures <= allowed_architectures: + msg = f'Invalid archs option {architectures}. ' + msg + raise ValueError(msg) + + if not architectures: + msg = 'Empty archs option set. ' + msg + raise ValueError(msg) diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 1a104d7c..13354f43 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -4,15 +4,14 @@ import textwrap from pathlib import Path, PurePath from typing import List, NamedTuple, Set +from .architecture import Architecture from .docker_container import DockerContainer from .logger import log from .typing import PathOrStr from .util import ( - Architecture, BuildOptions, BuildSelector, NonPlatformWheelError, - allowed_architectures_check, get_build_verbosity_extra_flags, prepare_command, read_python_configs, @@ -49,8 +48,6 @@ def get_python_configurations( def build(options: BuildOptions) -> None: - allowed_architectures_check('linux', options) - try: subprocess.check_output(['docker', '--version']) except Exception: diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 66b41b3f..cf691995 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -14,7 +14,6 @@ from .util import ( BuildOptions, BuildSelector, NonPlatformWheelError, - allowed_architectures_check, download, get_build_verbosity_extra_flags, get_pip_script, @@ -188,8 +187,6 @@ def setup_python(python_configuration: PythonConfiguration, def build(options: BuildOptions) -> None: - allowed_architectures_check("macos", options) - 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 33435c16..1baf71a9 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -1,9 +1,6 @@ import fnmatch -import functools import itertools import os -import platform as platform_module -import re import ssl import sys import textwrap @@ -17,8 +14,9 @@ import bracex import certifi import toml +from .architecture import Architecture from .environment import ParsedEnvironment -from .typing import PathOrStr, PlatformName, assert_never +from .typing import PathOrStr, PlatformName if sys.version_info < (3, 9): from importlib_resources import files @@ -163,62 +161,6 @@ class DependencyConstraints: return f'{self.__class__.__name__}{self.base_file_path!r})' -@functools.total_ordering -class Architecture(Enum): - value: str - - # mac/linux archs - x86_64 = 'x86_64' - i686 = 'i686' - aarch64 = 'aarch64' - ppc64le = 'ppc64le' - s390x = 's390x' - - # windows archs - x86 = 'x86' - AMD64 = 'AMD64' - - # Allow this to be sorted - def __lt__(self, other: "Architecture") -> bool: - return self.value < other.value - - @staticmethod - def parse_config(config: str, platform: PlatformName) -> 'Set[Architecture]': - result = set() - for arch_str in re.split(r'[\s,]+', config): - if arch_str == 'auto': - result |= Architecture.auto_archs(platform=platform) - elif arch_str == 'native': - result.add(Architecture(platform_module.machine())) - elif arch_str == 'all': - result |= Architecture.all_archs(platform=platform) - else: - result.add(Architecture(arch_str)) - return result - - @staticmethod - def auto_archs(platform: PlatformName) -> 'Set[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.add(Architecture.i686) - if platform == 'windows' and native_architecture == Architecture.AMD64: - result.add(Architecture.x86) - return result - - @staticmethod - def all_archs(platform: PlatformName) -> 'Set[Architecture]': - if platform == 'linux': - return {Architecture.x86_64, Architecture.i686, Architecture.aarch64, Architecture.ppc64le, Architecture.s390x} - elif platform == 'macos': - return {Architecture.x86_64} - elif platform == 'windows': - return {Architecture.x86, Architecture.AMD64} - else: - assert_never(platform) - - class BuildOptions(NamedTuple): package_dir: Path output_dir: Path @@ -286,27 +228,3 @@ def detect_ci_provider() -> Optional[CIProvider]: return CIProvider.other else: return None - - -PRETTY_NAMES = {'linux': 'Linux', 'macos': 'macOS', 'windows': 'Windows'} - - -def allowed_architectures_check( - platform: PlatformName, - options: BuildOptions, -) -> None: - - allowed_architectures = Architecture.all_archs(platform) - - msg = f'{PRETTY_NAMES[platform]} only supports {sorted(allowed_architectures)} at the moment.' - - if platform != 'linux': - msg += ' If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead.' - - if not options.architectures <= allowed_architectures: - msg = f'Invalid archs option {options.architectures}. ' + msg - raise ValueError(msg) - - if not options.architectures: - msg = 'Empty archs option set. ' + msg - raise ValueError(msg) diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index bd929358..4d3487ca 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -9,15 +9,14 @@ from zipfile import ZipFile import toml +from .architecture import Architecture from .environment import ParsedEnvironment from .logger import log from .typing import PathOrStr from .util import ( - Architecture, BuildOptions, BuildSelector, NonPlatformWheelError, - allowed_architectures_check, download, get_build_verbosity_extra_flags, get_pip_script, @@ -70,7 +69,8 @@ def get_python_configurations( '64': Architecture.AMD64, } - if IS_RUNNING_ON_TRAVIS: + custom_compiler = os.environ.get('DISTUTILS_USE_SDK') and os.environ.get('MSSdk') + if IS_RUNNING_ON_TRAVIS and not custom_compiler: # cannot install VCForPython27.msi which is needed for compiling C software # try with (and similar): msiexec /i VCForPython27.msi ALLUSERS=1 ACCEPT=YES /passive python_configurations = [c for c in python_configurations if not c.version.startswith('2.7')] @@ -206,8 +206,6 @@ def pep_518_cp35_workaround(package_dir: Path, env: Dict[str, str]) -> None: def build(options: BuildOptions) -> None: - allowed_architectures_check('windows', options) - 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/setup.md b/docs/setup.md index 282edde9..c86ebf13 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -44,7 +44,7 @@ To build Linux, Mac, and Windows wheels on Travis CI, create a `.travis.yml` fil {% include "../examples/travis-ci-minimal.yml" %} ``` -Note that building Windows Python 2.7 wheels on Travis is unsupported. +Note that building Windows Python 2.7 wheels on Travis is unsupported unless using a newer compiler [via a workaround](cpp-standards.md). Commit this file, enable building of your repo on Travis CI, and push. diff --git a/test/test_cpp_standards.py b/test/test_cpp_standards.py index 0ecf196d..762414bf 100644 --- a/test/test_cpp_standards.py +++ b/test/test_cpp_standards.py @@ -202,7 +202,7 @@ def test_cpp17_py27_modern_msvc_workaround(tmp_path): add_env_x64['CIBW_BUILD'] = 'cp27-win_amd64' actual_wheels += utils.cibuildwheel_run(project_dir, add_env=add_env_x64) - expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0') + expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0', exclude_27=False) if 'cp27-cp27m-win' in w or 'pp27-pypy_73-win32' in w] diff --git a/test/test_dependency_versions.py b/test/test_dependency_versions.py index 07d22e24..1b415bb4 100644 --- a/test/test_dependency_versions.py +++ b/test/test_dependency_versions.py @@ -52,6 +52,9 @@ def test_pinned_versions(tmp_path, python_version): if utils.platform == 'linux': pytest.skip('linux doesn\'t pin individual tool versions, it pins manylinux images instead') + if utils.IS_WINDOWS_RUNNING_ON_TRAVIS and python_version == '2.7': + pytest.skip('Windows + Travis CI requires a workaround') + project_dir = tmp_path / 'project' project_with_expected_version_checks.generate(project_dir) @@ -114,6 +117,9 @@ def test_dependency_constraints_file(tmp_path, python_version): if utils.platform == 'linux': pytest.skip('linux doesn\'t pin individual tool versions, it pins manylinux images instead') + if utils.IS_WINDOWS_RUNNING_ON_TRAVIS and python_version == '2.7': + pytest.skip('Windows + Travis CI requires a workaround') + project_dir = tmp_path / 'project' project_with_expected_version_checks.generate(project_dir) diff --git a/test/utils.py b/test/utils.py index bae3853b..8b11a0e6 100644 --- a/test/utils.py +++ b/test/utils.py @@ -76,7 +76,8 @@ 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', machine_arch=None): + macosx_deployment_target='10.9', machine_arch=None, *, + exclude_27=IS_WINDOWS_RUNNING_ON_TRAVIS): ''' Returns a list of expected wheels from a run of cibuildwheel. ''' @@ -134,8 +135,9 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, for platform_tag in platform_tags: wheels.append(f'{package_name}-{package_version}-{python_abi_tag}-{platform_tag}.whl') - if IS_WINDOWS_RUNNING_ON_TRAVIS: - # Python 2.7 isn't supported on Travis. + # Travis on Windows does not support using the default Python 2.7 compiler, + # so we support skipping here. + if exclude_27: wheels = [w for w in wheels if '-cp27-' not in w and '-pp2' not in w] return wheels diff --git a/unit_test/main_tests/conftest.py b/unit_test/main_tests/conftest.py index 1122a88c..3ce025a0 100644 --- a/unit_test/main_tests/conftest.py +++ b/unit_test/main_tests/conftest.py @@ -1,3 +1,4 @@ +import platform as platform_module import subprocess import sys from pathlib import Path @@ -51,14 +52,27 @@ def fake_package_dir(monkeypatch): else: return real_path_exists(path) + args = ['cibuildwheel', str(MOCK_PACKAGE_DIR)] monkeypatch.setattr(Path, 'exists', mock_path_exists) - monkeypatch.setattr(sys, 'argv', ['cibuildwheel', str(MOCK_PACKAGE_DIR)]) + monkeypatch.setattr(sys, 'argv', args) + return args + + +@pytest.fixture +def allow_empty(request, monkeypatch, fake_package_dir): + monkeypatch.setattr(sys, 'argv', fake_package_dir + ['--allow-empty']) @pytest.fixture(params=['linux', 'macos', 'windows']) def platform(request, monkeypatch): platform_value = request.param monkeypatch.setenv('CIBW_PLATFORM', platform_value) + + if platform_value == 'windows': + monkeypatch.setattr(platform_module, 'machine', lambda: 'AMD64') + else: + monkeypatch.setattr(platform_module, 'machine', lambda: 'x86_64') + return platform_value diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index f28b1ed4..f6c80ff7 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -40,7 +40,7 @@ def test_output_dir_argument(also_set_environment, platform, intercepted_build_a assert intercepted_build_args.args[0].output_dir == OUTPUT_DIR -def test_build_selector(platform, intercepted_build_args, monkeypatch): +def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_empty): BUILD = 'some build* *-selector' SKIP = 'some skip* *-selector' @@ -57,6 +57,15 @@ def test_build_selector(platform, intercepted_build_args, monkeypatch): # Unit tests for BuildSelector are in build_selector_test.py +def test_empty_selector(platform, intercepted_build_args, monkeypatch): + monkeypatch.setenv('CIBW_SKIP', '*') + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 3 + + @pytest.mark.parametrize('architecture, image, full_image', [ ('x86_64', None, 'quay.io/pypa/manylinux2010_x86_64:*'), ('x86_64', 'manylinux1', 'quay.io/pypa/manylinux1_x86_64:*'), @@ -220,7 +229,7 @@ def test_build_verbosity(build_verbosity, platform_specific, platform, intercept ('*-macosx_10_9_x86_64', ['*-macosx_x86_64']), ('cp37-macosx_10_9_x86_64', ['cp37-macosx_x86_64']), ]) -def test_build_selector_migrations(intercepted_build_args, monkeypatch, option_name, option_value, build_selector_patterns): +def test_build_selector_migrations(intercepted_build_args, monkeypatch, option_name, option_value, build_selector_patterns, allow_empty): monkeypatch.setenv(option_name, option_value) main() diff --git a/unit_test/main_tests/main_platform_test.py b/unit_test/main_tests/main_platform_test.py index 9b7c06fe..a06ffe68 100644 --- a/unit_test/main_tests/main_platform_test.py +++ b/unit_test/main_tests/main_platform_test.py @@ -1,4 +1,3 @@ -import platform as platform_module import sys import pytest @@ -68,34 +67,39 @@ def test_platform_environment(platform, intercepted_build_args, monkeypatch): 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} + elif platform == 'windows': + assert build_options.architectures == {Architecture.AMD64, Architecture.x86} 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] + if platform in {'macos', 'windows'}: + with pytest.raises(SystemExit) as exit: + main() + assert exit.value.args == (4,) - assert build_options.architectures == {Architecture.ppc64le} + else: + 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') @@ -113,7 +117,6 @@ def test_archs_platform_specific(platform, intercepted_build_args, monkeypatch): def test_archs_platform_native(platform, intercepted_build_args, monkeypatch): - monkeypatch.setattr(platform_module, 'machine', lambda: 'x86_64') monkeypatch.setenv('CIBW_ARCHS', 'native') main() @@ -122,13 +125,12 @@ def test_archs_platform_native(platform, intercepted_build_args, monkeypatch): if platform == 'linux': assert build_options.architectures == {Architecture.x86_64} elif platform == 'windows': - assert build_options.architectures == {Architecture.x86_64} + assert build_options.architectures == {Architecture.AMD64} elif platform == 'macos': assert build_options.architectures == {Architecture.x86_64} def test_archs_platform_all(platform, intercepted_build_args, monkeypatch): - monkeypatch.setattr(platform_module, 'machine', lambda: 'x86_64') monkeypatch.setenv('CIBW_ARCHS', 'all') main()