Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error out if multiple wheels with the same name are produced #1152

Merged
merged 3 commits into from Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 11 additions & 6 deletions cibuildwheel/linux.py
Expand Up @@ -10,9 +10,10 @@
from .options import Options
from .typing import OrderedDict, PathOrStr, assert_never
from .util import (
AlreadyBuiltWheelError,
BuildSelector,
NonPlatformWheelError,
find_compatible_abi3_wheel,
find_compatible_wheel,
get_build_verbosity_extra_flags,
prepare_command,
read_python_configs,
Expand Down Expand Up @@ -177,13 +178,13 @@ def build_on_docker(
)
sys.exit(1)

abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
if abi3_wheel:
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
log.step_end()
print(
f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
repaired_wheels = [abi3_wheel]
repaired_wheels = [compatible_wheel]
else:

if build_options.before_build:
Expand Down Expand Up @@ -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...")

Expand Down Expand Up @@ -304,7 +309,7 @@ def build_on_docker(
docker.call(["rm", "-rf", venv_dir])

# move repaired wheels to output
if abi3_wheel is None:
if compatible_wheel is None:
docker.call(["mkdir", "-p", container_output_dir])
docker.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.extend(
Expand Down
16 changes: 10 additions & 6 deletions cibuildwheel/macos.py
Expand Up @@ -17,13 +17,14 @@
from .typing import Literal, PathOrStr, assert_never
from .util import (
CIBW_CACHE_PATH,
AlreadyBuiltWheelError,
BuildFrontend,
BuildSelector,
NonPlatformWheelError,
call,
detect_ci_provider,
download,
find_compatible_abi3_wheel,
find_compatible_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
install_certifi_script,
Expand Down Expand Up @@ -321,13 +322,13 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend,
)

abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
if abi3_wheel:
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
log.step_end()
print(
f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
repaired_wheel = abi3_wheel
repaired_wheel = compatible_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -534,7 +538,7 @@ def build(options: Options, tmp_path: Path) -> None:
)

# we're all done here; move it to output (overwrite existing)
if abi3_wheel is None:
if compatible_wheel is None:
try:
(build_options.output_dir / repaired_wheel.name).unlink()
except FileNotFoundError:
Expand Down
34 changes: 25 additions & 9 deletions cibuildwheel/util.py
Expand Up @@ -54,7 +54,7 @@
"MANYLINUX_ARCHS",
"call",
"shell",
"find_compatible_abi3_wheel",
"find_compatible_wheel",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
Expand Down Expand Up @@ -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"}

Expand Down Expand Up @@ -574,23 +588,25 @@ def virtualenv(
T = TypeVar("T", bound=PurePath)


def find_compatible_abi3_wheel(wheels: Sequence[T], identifier: str) -> Optional[T]:
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> Optional[T]:
"""
Finds an ABI3 wheel in `wheels` compatible with the Python interpreter
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
specified by `identifier`.
"""

interpreter, platform = identifier.split("-")
if not interpreter.startswith("cp3"):
return None
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
if tag.abi != "abi3":
continue
if not tag.interpreter.startswith("cp3"):
if tag.abi == "abi3":
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
continue
elif tag.abi == "none":
if tag.interpreter[:3] != "py3":
continue
else:
continue
if int(tag.interpreter[3:]) > int(interpreter[3:]):
if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
continue
if platform.startswith(("manylinux", "musllinux", "macosx")):
# Linux, macOS
Expand Down
16 changes: 10 additions & 6 deletions cibuildwheel/windows.py
Expand Up @@ -17,12 +17,13 @@
from .typing import PathOrStr, assert_never
from .util import (
CIBW_CACHE_PATH,
AlreadyBuiltWheelError,
BuildFrontend,
BuildSelector,
NonPlatformWheelError,
call,
download,
find_compatible_abi3_wheel,
find_compatible_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
prepare_command,
Expand Down Expand Up @@ -277,13 +278,13 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend,
)

abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
if abi3_wheel:
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
log.step_end()
print(
f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
repaired_wheel = abi3_wheel
repaired_wheel = compatible_wheel
else:
# run the before_build command
if build_options.before_build:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -418,7 +422,7 @@ def build(options: Options, tmp_path: Path) -> None:
shell(test_command_prepared, cwd="c:\\", env=virtualenv_env)

# we're all done here; move it to output (remove if already exists)
if abi3_wheel is None:
if compatible_wheel is None:
shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)

Expand Down
42 changes: 42 additions & 0 deletions 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
44 changes: 43 additions & 1 deletion unit_test/utils_test.py
@@ -1,4 +1,8 @@
from cibuildwheel.util import format_safe, prepare_command
from pathlib import PurePath

import pytest

from cibuildwheel.util import find_compatible_wheel, format_safe, prepare_command


def test_format_safe():
Expand Down Expand Up @@ -46,3 +50,41 @@ def test_prepare_command():
prepare_command("{a}{a,b}{b:.2e}{c}{d%s}{e:3}{f[0]}", a="42", b="3.14159")
== "42{a,b}{b:.2e}{c}{d%s}{e:3}{f[0]}"
)


@pytest.mark.parametrize(
"wheel,identifier",
(
("foo-0.1-cp38-abi3-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-cp38-abi3-macosx_11_0_x86_64.whl", "cp310-macosx_x86_64"),
("foo-0.1-cp38-abi3-manylinux2014_x86_64.whl", "cp310-manylinux_x86_64"),
("foo-0.1-cp38-abi3-musllinux_1_1_x86_64.whl", "cp310-musllinux_x86_64"),
("foo-0.1-py2.py3-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py2.py3-none-win_amd64.whl", "pp310-win_amd64"),
("foo-0.1-py3-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "pp310-win_amd64"),
),
)
def test_find_compatible_wheel_found(wheel: str, identifier: str):
wheel_ = PurePath(wheel)
found = find_compatible_wheel([wheel_], identifier)
assert found is wheel_


@pytest.mark.parametrize(
"wheel,identifier",
(
("foo-0.1-cp38-abi3-win_amd64.whl", "cp310-win32"),
("foo-0.1-cp38-abi3-win_amd64.whl", "cp37-win_amd64"),
("foo-0.1-cp38-abi3-macosx_11_0_x86_64.whl", "cp310-macosx_universal2"),
("foo-0.1-cp38-abi3-manylinux2014_x86_64.whl", "cp310-musllinux_x86_64"),
("foo-0.1-cp38-abi3-musllinux_1_1_x86_64.whl", "cp310-manylinux_x86_64"),
("foo-0.1-py2-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "cp37-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "pp37-win_amd64"),
("foo-0.1-cp38-cp38-win_amd64.whl", "cp310-win_amd64"),
),
)
def test_find_compatible_wheel_not_found(wheel: str, identifier: str):
assert find_compatible_wheel([PurePath(wheel)], identifier) is None