diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 58fe4dc9d..d6b39e748 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -10,6 +10,7 @@ from .options import Options from .typing import OrderedDict, PathOrStr, assert_never from .util import ( + AlreadyBuiltWheelError, BuildSelector, NonPlatformWheelError, find_compatible_wheel, @@ -255,6 +256,10 @@ def build_on_docker( repaired_wheels = docker.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 56942cfb4..d5fff5ed8 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -17,6 +17,7 @@ from .typing import Literal, PathOrStr, assert_never from .util import ( CIBW_CACHE_PATH, + AlreadyBuiltWheelError, BuildFrontend, BuildSelector, NonPlatformWheelError, @@ -408,6 +409,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 18cf6b7d8..d92cb3df9 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -373,6 +373,20 @@ def __init__(self) -> None: 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 5f0d2344e..b4ae6e07e 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -17,6 +17,7 @@ from .typing import PathOrStr, assert_never from .util import ( CIBW_CACHE_PATH, + AlreadyBuiltWheelError, BuildFrontend, BuildSelector, NonPlatformWheelError, @@ -365,6 +366,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 000000000..d89f28cb7 --- /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