diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 3b759c08..57a7deee 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -11,6 +11,7 @@ from .oci_container import OCIContainer from .options import Options from .typing import OrderedDict, PathOrStr, assert_never from .util import ( + AlreadyBuiltWheelError, BuildSelector, NonPlatformWheelError, find_compatible_wheel, @@ -261,6 +262,10 @@ def build_in_container( repaired_wheels = container.glob(repaired_wheel_dir, "*.whl") + for repaired_wheel in repaired_wheels: + if repaired_wheel.name in {wheel.name for wheel in built_wheels}: + raise AlreadyBuiltWheelError(repaired_wheel.name) + if build_options.test_command and build_options.test_selector(config.identifier): log.step("Testing wheel...") diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 22213de8..8cc2e80d 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -18,6 +18,7 @@ from .options import Options from .typing import Literal, PathOrStr, assert_never from .util import ( CIBW_CACHE_PATH, + AlreadyBuiltWheelError, BuildFrontend, BuildSelector, NonPlatformWheelError, @@ -410,6 +411,9 @@ def build(options: Options, tmp_path: Path) -> None: repaired_wheel = next(repaired_wheel_dir.glob("*.whl")) + if repaired_wheel.name in {wheel.name for wheel in built_wheels}: + raise AlreadyBuiltWheelError(repaired_wheel.name) + log.step_end() if build_options.test_command and build_options.test_selector(config.identifier): diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index d3f42bda..f164b91a 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -372,6 +372,20 @@ class NonPlatformWheelError(Exception): super().__init__(message) +class AlreadyBuiltWheelError(Exception): + def __init__(self, wheel_name: str) -> None: + message = textwrap.dedent( + f""" + cibuildwheel: Build failed because a wheel named {wheel_name} was already generated in the current run. + + If you expected another wheel to be generated, check your project configuration, or run + cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs. + """ + ) + + super().__init__(message) + + def strtobool(val: str) -> bool: return val.lower() in {"y", "yes", "t", "true", "on", "1"} diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 46b8bf90..82309a23 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -18,6 +18,7 @@ from .options import Options from .typing import PathOrStr, assert_never from .util import ( CIBW_CACHE_PATH, + AlreadyBuiltWheelError, BuildFrontend, BuildSelector, NonPlatformWheelError, @@ -367,6 +368,9 @@ def build(options: Options, tmp_path: Path) -> None: repaired_wheel = next(repaired_wheel_dir.glob("*.whl")) + if repaired_wheel.name in {wheel.name for wheel in built_wheels}: + raise AlreadyBuiltWheelError(repaired_wheel.name) + if build_options.test_command and options.globals.test_selector(config.identifier): log.step("Testing wheel...") # set up a virtual environment to install and test from, to make sure diff --git a/test/test_same_wheel.py b/test/test_same_wheel.py new file mode 100644 index 00000000..d89f28cb --- /dev/null +++ b/test/test_same_wheel.py @@ -0,0 +1,42 @@ +import subprocess +from test import test_projects + +import pytest + +from . import utils + +basic_project = test_projects.new_c_project() +basic_project.files[ + "repair.py" +] = """ +import shutil +import sys +from pathlib import Path + +wheel = Path(sys.argv[1]) +dest_dir = Path(sys.argv[2]) +platform = wheel.stem.split("-")[-1] +name = f"spam-0.1.0-py2-none-{platform}.whl" +dest = dest_dir / name +dest_dir.mkdir(parents=True, exist_ok=True) +if dest.exists(): + dest.unlink() +shutil.copy(wheel, dest) +""" + + +def test(tmp_path, capfd): + # this test checks that a generated wheel name shall be unique in a given cibuildwheel run + project_dir = tmp_path / "project" + basic_project.generate(project_dir) + + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run( + project_dir, + add_env={ + "CIBW_REPAIR_WHEEL_COMMAND": "python repair.py {wheel} {dest_dir}", + }, + ) + + captured = capfd.readouterr() + assert "Build failed because a wheel named" in captured.err