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

feature: add support for py3-none-{platform} wheels #1151

Merged
merged 5 commits into from Jul 3, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions cibuildwheel/linux.py
Expand Up @@ -12,7 +12,7 @@
from .util import (
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 +177,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 @@ -304,7 +304,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
12 changes: 6 additions & 6 deletions cibuildwheel/macos.py
Expand Up @@ -23,7 +23,7 @@
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 +321,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 @@ -534,7 +534,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
37 changes: 24 additions & 13 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 @@ -574,36 +574,47 @@ 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
specified by `identifier`.
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
specified by `identifier` that is previously built.
"""

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":
# ABI3 wheels must start with cp3 for impl and tag
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
continue
elif tag.abi == "none":
# CPythonless wheels must include py3 tag
if tag.interpreter[:3] != "py3":
continue
else:
# Other types of wheels are not detected, this is looking for previously built wheels.
continue
if int(tag.interpreter[3:]) > int(interpreter[3:]):

if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
# If a minor version number is given, it has to be lower than the current one.
continue

if platform.startswith(("manylinux", "musllinux", "macosx")):
# Linux, macOS
# Linux, macOS require the beginning and ending match (macos/manylinux version doesn't need to)
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
if not tag.platform.endswith("_" + arch):
if not tag.platform.endswith(f"_{arch}"):
continue
else:
# Windows
# Windows should exactly match
if not tag.platform == platform:
continue

# If all the filters above pass, then the wheel is a previously built compatible wheel.
return wheel

return None


Expand Down
12 changes: 6 additions & 6 deletions cibuildwheel/windows.py
Expand Up @@ -22,7 +22,7 @@
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 +277,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 @@ -418,7 +418,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
4 changes: 2 additions & 2 deletions setup.cfg
Expand Up @@ -56,8 +56,8 @@ console_scripts =
cibuildwheel = resources/*

[flake8]
extend-ignore = E203,E501,B950
extend-select = B,B9
extend-ignore = E203,E501,B950,B023
extend-select = B9
application-import-names = cibuildwheel
exclude =
cibuildwheel/resources/,
Expand Down
179 changes: 179 additions & 0 deletions test/test_abi_variants.py
@@ -0,0 +1,179 @@
import textwrap

from . import test_projects, utils

limited_api_project = test_projects.new_c_project(
setup_py_add=textwrap.dedent(
r"""
cmdclass = {}
extension_kwargs = {}
if sys.version_info[:2] >= (3, 8):
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel

class bdist_wheel_abi3(_bdist_wheel):
def finalize_options(self):
_bdist_wheel.finalize_options(self)
self.root_is_pure = False

def get_tag(self):
python, abi, plat = _bdist_wheel.get_tag(self)
return python, "abi3", plat

cmdclass["bdist_wheel"] = bdist_wheel_abi3
extension_kwargs["define_macros"] = [("Py_LIMITED_API", "0x03080000")]
extension_kwargs["py_limited_api"] = True
"""
),
setup_py_extension_args_add="**extension_kwargs",
setup_py_setup_args_add="cmdclass=cmdclass",
)


def test_abi3(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_SKIP": "pp* ", # PyPy does not have a Py_LIMITED_API equivalent
},
)

# check that the expected wheels are produced
expected_wheels = [
w.replace("cp38-cp38", "cp38-abi3")
for w in utils.expected_wheels("spam", "0.1.0")
if "-pp" not in w and "-cp39" not in w and "-cp310" not in w and "-cp311" not in w
]
assert set(actual_wheels) == set(expected_wheels)


ctypes_project = test_projects.TestProject()
ctypes_project.files["setup.py"] = textwrap.dedent(
"""
from setuptools import setup, Extension

from distutils.command.build_ext import build_ext as _build_ext
class CTypesExtension(Extension): pass
class build_ext(_build_ext):
def build_extension(self, ext):
self._ctypes = isinstance(ext, CTypesExtension)
return super().build_extension(ext)

def get_export_symbols(self, ext):
if self._ctypes:
return ext.export_symbols
return super().get_export_symbols(ext)

def get_ext_filename(self, ext_name):
if self._ctypes:
return ext_name + '.so'
return super().get_ext_filename(ext_name)

from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
class bdist_wheel_abi_none(_bdist_wheel):
def finalize_options(self):
_bdist_wheel.finalize_options(self)
self.root_is_pure = False

def get_tag(self):
python, abi, plat = _bdist_wheel.get_tag(self)
return "py3", "none", plat

setup(
name="ctypesexample",
version="1.0.0",
py_modules = ["ctypesexample.summing"],
ext_modules=[
CTypesExtension(
"ctypesexample.csumlib",
["ctypesexample/csumlib.c"],
),
],
cmdclass={'build_ext': build_ext, 'bdist_wheel': bdist_wheel_abi_none},
)
"""
)
ctypes_project.files["ctypesexample/csumlib.c"] = textwrap.dedent(
"""
#ifdef _WIN32
#define LIBRARY_API __declspec(dllexport)
#else
#define LIBRARY_API
#endif

#include <stdlib.h>


LIBRARY_API double *add_vec3(double *a, double *b)
{
double *res = malloc(sizeof(double) * 3);

for (int i = 0; i < 3; ++i)
{
res[i] = a[i] + b[i];
}

return res;
}
"""
)
ctypes_project.files["ctypesexample/summing.py"] = textwrap.dedent(
"""
import ctypes
import pathlib

# path of the shared library
libfile = pathlib.Path(__file__).parent / "csumlib.so"
csumlib = ctypes.CDLL(str(libfile))

type_vec3 = ctypes.POINTER(ctypes.c_double * 3)

csumlib.add_vec3.restype = type_vec3
csumlib.add_vec3.argtypes = [type_vec3, type_vec3]
def add(a: list, b: list) -> list:
a_p = (ctypes.c_double * 3)(*a)
b_p = (ctypes.c_double * 3)(*b)
r_p = csumlib.add_vec3(a_p,b_p)

return [l for l in r_p.contents]
"""
)

ctypes_project.files["test/add_test.py"] = textwrap.dedent(
"""
import ctypesexample.summing

def test():
a = [1, 2, 3]
b = [4, 5, 6]
assert ctypesexample.summing.add(a, b) == [5, 7, 9]
"""
)


def test_abi_none(tmp_path, capfd):
project_dir = tmp_path / "project"
ctypes_project.generate(project_dir)

# build the wheels
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "pytest {project}/test",
# limit the number of builds for test performance reasons
"CIBW_BUILD": "cp38-* cp310-* pp39-*",
},
)

# check that the expected wheels are produced
expected_wheels = utils.expected_wheels("ctypesexample", "1.0.0", python_abi_tags=["py3-none"])
assert set(actual_wheels) == set(expected_wheels)

# check that each wheel was built once, and reused
captured = capfd.readouterr()
assert "Building wheel..." in captured.out
assert "Found previously built wheel" in captured.out