diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 5d19dbd20..2573f5af8 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -218,6 +218,18 @@ def build(options: BuildOptions) -> None: # clean up test environment docker.call(['rm', '-rf', venv_dir]) + existing_output_files = docker.call(['ls'], capture_output=True, cwd=container_output_dir).split('\n') + for repaired_wheel in repaired_wheels: + if repaired_wheel.name in existing_output_files: + message = f'Created wheel ({repaired_wheel}) already exists in output directory.' + if 'abi3' in repaired_wheel.name: + message += ('\n' + "It looks like you are building wheels against Python's limited API/stable ABI;\n" + 'please limit your Python selection to a single version when building limited API\n' + 'wheels, with CIBW_BUILD.') + log.error(message) + sys.exit(1) + # move repaired wheels to output docker.call(['mkdir', '-p', container_output_dir]) docker.call(['mv', *repaired_wheels, container_output_dir]) diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index e6395449f..60b9bceb0 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -456,6 +456,16 @@ def call_with_arch(args: Sequence[PathOrStr], **kwargs: Any) -> int: # clean up shutil.rmtree(venv_dir) + if (options.output_dir / repaired_wheel.name).exists(): + message = f'Created wheel ({repaired_wheel}) already exists in output directory.' + if 'abi3' in repaired_wheel.name: + message += ('\n' + "It looks like you are building wheels against Python's limited API/stable ABI;\n" + 'please limit your Python selection to a single version when building limited API\n' + 'wheels, with CIBW_BUILD.') + log.error(message) + sys.exit(1) + # we're all done here; move it to output (overwrite existing) shutil.move(str(repaired_wheel), options.output_dir) log.build_end() diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 45f0141ab..afd759bad 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -325,6 +325,16 @@ def build(options: BuildOptions) -> None: # clean up shutil.rmtree(venv_dir) + if (options.output_dir / repaired_wheel.name).exists(): + message = f'Created wheel ({repaired_wheel}) already exists in output directory.' + if 'abi3' in repaired_wheel.name: + message += ('\n' + "It looks like you are building wheels against Python's limited API/stable ABI;\n" + 'please limit your Python selection to a single version when building limited API\n' + 'wheels, with CIBW_BUILD.') + log.error(message) + sys.exit(1) + # we're all done here; move it to output (remove if already exists) shutil.move(str(repaired_wheel), options.output_dir) log.build_end() diff --git a/test/test_limited_api.py b/test/test_limited_api.py new file mode 100644 index 000000000..0e9626d23 --- /dev/null +++ b/test/test_limited_api.py @@ -0,0 +1,60 @@ +import subprocess +import textwrap + +import pytest + +from . import test_projects, utils + +limited_api_project = test_projects.new_c_project( + setup_cfg_add=textwrap.dedent(r''' + [bdist_wheel] + py_limited_api=cp36 + ''') +) + + +def test_setup_cfg(tmp_path): + project_dir = tmp_path / 'project' + limited_api_project.generate(project_dir) + + # build the wheels + actual_wheels = utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_BUILD': 'cp27-* cp36-*', # PyPy does not have a Py_LIMITED_API equivalent + }) + + # check that the expected wheels are produced + expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0', limited_api='cp36') + if '-pp' not in w] + assert set(actual_wheels) == set(expected_wheels) + + +def test_build_option_env(tmp_path, capfd): + project_dir = tmp_path / 'project' + test_projects.new_c_project().generate(project_dir) + + # build the wheels + actual_wheels = utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_ENVIRONMENT': 'PIP_BUILD_OPTION="--py-limited-api=cp36"', + 'CIBW_BUILD': 'cp27-* cp36-*', # PyPy does not have a Py_LIMITED_API equivalent + }) + + # check that the expected wheels are produced + expected_wheels = [w for w in utils.expected_wheels('spam', '0.1.0', limited_api='cp36') + if '-pp' not in w] + assert set(actual_wheels) == set(expected_wheels) + + +def test_duplicate_wheel_error(tmp_path, capfd): + project_dir = tmp_path / 'project' + limited_api_project.generate(project_dir) + + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run(project_dir, add_env={ + 'CIBW_BUILD': 'cp36-* cp37-*', + }) + + captured = capfd.readouterr() + print('out', captured.out) + print('err', captured.err) + assert "already exists in output directory" in captured.err + assert "It looks like you are building wheels against Python's limited API" in captured.err diff --git a/test/utils.py b/test/utils.py index 8dcb2f604..ef96d5069 100644 --- a/test/utils.py +++ b/test/utils.py @@ -88,7 +88,8 @@ def _get_arm64_macosx_deployment_target(macosx_deployment_target: str) -> str: def expected_wheels(package_name, package_version, manylinux_versions=None, macosx_deployment_target='10.9', machine_arch=None, *, - exclude_27=IS_WINDOWS_RUNNING_ON_TRAVIS): + exclude_27=IS_WINDOWS_RUNNING_ON_TRAVIS, + limited_api=None): ''' Returns a list of expected wheels from a run of cibuildwheel. ''' @@ -106,7 +107,10 @@ def expected_wheels(package_name, package_version, manylinux_versions=None, else: manylinux_versions = ['manylinux2014'] - python_abi_tags = ['cp35-cp35m', 'cp36-cp36m', 'cp37-cp37m', 'cp38-cp38', 'cp39-cp39'] + if limited_api: + python_abi_tags = [f'{limited_api}-abi3'] + else: + python_abi_tags = ['cp35-cp35m', 'cp36-cp36m', 'cp37-cp37m', 'cp38-cp38', 'cp39-cp39'] if machine_arch in ['x86_64', 'AMD64', 'x86']: python_abi_tags += ['cp27-cp27m', 'pp27-pypy_73', 'pp36-pypy36_pp73', 'pp37-pypy37_pp73']