feature: add support for ABI3 wheels

This commit is contained in:
mayeut
2022-04-26 22:38:17 +02:00
parent de07370930
commit e3ff027131
7 changed files with 339 additions and 201 deletions
+79 -63
View File
@@ -12,6 +12,7 @@ from .typing import OrderedDict, PathOrStr, assert_never
from .util import (
BuildSelector,
NonPlatformWheelError,
find_compatible_abi3_wheel,
get_build_verbosity_extra_flags,
prepare_command,
read_python_configs,
@@ -132,6 +133,8 @@ def build_on_docker(
)
docker.call(["sh", "-c", before_all_prepared], env=env)
built_wheels: List[PurePath] = []
for config in platform_configs:
log.build_start(config.identifier)
build_options = options.build_options(config.identifier)
@@ -174,74 +177,83 @@ def build_on_docker(
)
sys.exit(1)
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build,
project=container_project_path,
package=container_package_dir,
)
docker.call(["sh", "-c", before_build_prepared], env=env)
log.step("Building wheel...")
temp_dir = PurePath("/tmp/cibuildwheel")
built_wheel_dir = temp_dir / "built_wheel"
docker.call(["rm", "-rf", built_wheel_dir])
docker.call(["mkdir", "-p", built_wheel_dir])
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if build_options.build_frontend == "pip":
docker.call(
[
"python",
"-m",
"pip",
"wheel",
container_package_dir,
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*verbosity_flags,
],
env=env,
)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
docker.call(
[
"python",
"-m",
"build",
container_package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
],
env=env,
abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
if abi3_wheel:
log.step_end()
print(
f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
repaired_wheels = [abi3_wheel]
else:
assert_never(build_options.build_frontend)
built_wheel = docker.glob(built_wheel_dir, "*.whl")[0]
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build,
project=container_project_path,
package=container_package_dir,
)
docker.call(["sh", "-c", before_build_prepared], env=env)
repaired_wheel_dir = temp_dir / "repaired_wheel"
docker.call(["rm", "-rf", repaired_wheel_dir])
docker.call(["mkdir", "-p", repaired_wheel_dir])
log.step("Building wheel...")
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
temp_dir = PurePath("/tmp/cibuildwheel")
built_wheel_dir = temp_dir / "built_wheel"
docker.call(["rm", "-rf", built_wheel_dir])
docker.call(["mkdir", "-p", built_wheel_dir])
if build_options.repair_command:
log.step("Repairing wheel...")
repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
docker.call(["sh", "-c", repair_command_prepared], env=env)
else:
docker.call(["mv", built_wheel, repaired_wheel_dir])
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
repaired_wheels = docker.glob(repaired_wheel_dir, "*.whl")
if build_options.build_frontend == "pip":
docker.call(
[
"python",
"-m",
"pip",
"wheel",
container_package_dir,
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*verbosity_flags,
],
env=env,
)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
docker.call(
[
"python",
"-m",
"build",
container_package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
],
env=env,
)
else:
assert_never(build_options.build_frontend)
built_wheel = docker.glob(built_wheel_dir, "*.whl")[0]
repaired_wheel_dir = temp_dir / "repaired_wheel"
docker.call(["rm", "-rf", repaired_wheel_dir])
docker.call(["mkdir", "-p", repaired_wheel_dir])
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
if build_options.repair_command:
log.step("Repairing wheel...")
repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
docker.call(["sh", "-c", repair_command_prepared], env=env)
else:
docker.call(["mv", built_wheel, repaired_wheel_dir])
repaired_wheels = docker.glob(repaired_wheel_dir, "*.whl")
if build_options.test_command and build_options.test_selector(config.identifier):
log.step("Testing wheel...")
@@ -292,8 +304,12 @@ def build_on_docker(
docker.call(["rm", "-rf", venv_dir])
# move repaired wheels to output
docker.call(["mkdir", "-p", container_output_dir])
docker.call(["mv", *repaired_wheels, container_output_dir])
if abi3_wheel is None:
docker.call(["mkdir", "-p", container_output_dir])
docker.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.extend(
container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels
)
log.build_end()
+85 -70
View File
@@ -23,6 +23,7 @@ from .util import (
call,
detect_ci_provider,
download,
find_compatible_abi3_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
install_certifi_script,
@@ -291,6 +292,8 @@ def build(options: Options, tmp_path: Path) -> None:
)
shell(before_all_prepared, env=env)
built_wheels: List[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
log.build_start(config.identifier)
@@ -318,84 +321,94 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend,
)
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=build_options.package_dir
abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
if abi3_wheel:
log.step_end()
print(
f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
shell(before_build_prepared, env=env)
log.step("Building wheel...")
built_wheel_dir.mkdir()
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if build_options.build_frontend == "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*verbosity_flags,
env=env,
)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
build_env = env.copy()
if build_options.dependency_constraints:
constraint_path = build_options.dependency_constraints.get_for_python_version(
config.version
repaired_wheel = abi3_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=build_options.package_dir
)
build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
env=build_env,
)
else:
assert_never(build_options.build_frontend)
shell(before_build_prepared, env=env)
built_wheel = next(built_wheel_dir.glob("*.whl"))
log.step("Building wheel...")
built_wheel_dir.mkdir()
repaired_wheel_dir.mkdir()
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
if build_options.repair_command:
log.step("Repairing wheel...")
if config_is_universal2:
delocate_archs = "x86_64,arm64"
elif config_is_arm64:
delocate_archs = "arm64"
if build_options.build_frontend == "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*verbosity_flags,
env=env,
)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
build_env = env.copy()
if build_options.dependency_constraints:
constraint_path = (
build_options.dependency_constraints.get_for_python_version(
config.version
)
)
build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
env=build_env,
)
else:
delocate_archs = "x86_64"
assert_never(build_options.build_frontend)
repair_command_prepared = prepare_command(
build_options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
delocate_archs=delocate_archs,
)
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
built_wheel = next(built_wheel_dir.glob("*.whl"))
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
repaired_wheel_dir.mkdir()
log.step_end()
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
if build_options.repair_command:
log.step("Repairing wheel...")
if config_is_universal2:
delocate_archs = "x86_64,arm64"
elif config_is_arm64:
delocate_archs = "arm64"
else:
delocate_archs = "x86_64"
repair_command_prepared = prepare_command(
build_options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
delocate_archs=delocate_archs,
)
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
log.step_end()
if build_options.test_command and build_options.test_selector(config.identifier):
machine_arch = platform.machine()
@@ -521,7 +534,9 @@ def build(options: Options, tmp_path: Path) -> None:
)
# we're all done here; move it to output (overwrite existing)
shutil.move(str(repaired_wheel), build_options.output_dir)
if abi3_wheel is None:
shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
# clean up
shutil.rmtree(identifier_tmp_dir)
+40 -1
View File
@@ -13,7 +13,7 @@ import time
import urllib.request
from enum import Enum
from functools import lru_cache
from pathlib import Path
from pathlib import Path, PurePath
from time import sleep
from typing import (
Any,
@@ -26,6 +26,7 @@ from typing import (
Optional,
Sequence,
TextIO,
TypeVar,
cast,
overload,
)
@@ -41,6 +42,7 @@ else:
from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet
from packaging.utils import parse_wheel_filename
from packaging.version import Version
from platformdirs import user_cache_path
@@ -51,6 +53,7 @@ __all__ = [
"MANYLINUX_ARCHS",
"call",
"shell",
"find_compatible_abi3_wheel",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
@@ -566,6 +569,42 @@ def virtualenv(
return env
T = TypeVar("T", bound=PurePath)
def find_compatible_abi3_wheel(wheels: Sequence[T], identifier: str) -> Optional[T]:
"""
Finds an ABI3 wheel 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"):
continue
if int(tag.interpreter[3:]) > int(interpreter[3:]):
continue
if platform.startswith(("manylinux", "musllinux", "macosx")):
# Linux, macOS
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
if not tag.platform.endswith("_" + arch):
continue
else:
# Windows
if not tag.platform == platform:
continue
return wheel
return None
if sys.version_info >= (3, 8):
from functools import cached_property
else:
+81 -66
View File
@@ -22,6 +22,7 @@ from .util import (
NonPlatformWheelError,
call,
download,
find_compatible_abi3_wheel,
get_build_verbosity_extra_flags,
get_pip_version,
prepare_command,
@@ -249,6 +250,8 @@ def build(options: Options, tmp_path: Path) -> None:
)
shell(before_all_prepared, env=env)
built_wheels: List[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
log.build_start(config.identifier)
@@ -274,83 +277,93 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend,
)
# run the before_build command
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=options.globals.package_dir
abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier)
if abi3_wheel:
log.step_end()
print(
f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
shell(before_build_prepared, env=env)
log.step("Building wheel...")
built_wheel_dir.mkdir()
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if build_options.build_frontend == "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
options.globals.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*get_build_verbosity_extra_flags(build_options.build_verbosity),
env=env,
)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
build_env = env.copy()
if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version(
config.version
repaired_wheel = abi3_wheel
else:
# run the before_build command
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=options.globals.package_dir
)
# Bug in pip <= 21.1.3 - we can't have a space in the
# constraints file, and pip doesn't support drive letters
# in uhi. After probably pip 21.2, we can use uri. For
# now, use a temporary file.
if " " in str(constraints_path):
assert " " not in str(identifier_tmp_dir)
tmp_file = identifier_tmp_dir / "constraints.txt"
tmp_file.write_bytes(constraints_path.read_bytes())
constraints_path = tmp_file
shell(before_build_prepared, env=env)
build_env["PIP_CONSTRAINT"] = str(constraints_path)
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
log.step("Building wheel...")
built_wheel_dir.mkdir()
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if build_options.build_frontend == "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
env=build_env,
"pip",
"wheel",
options.globals.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*get_build_verbosity_extra_flags(build_options.build_verbosity),
env=env,
)
else:
assert_never(build_options.build_frontend)
elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags)
build_env = env.copy()
if build_options.dependency_constraints:
constraints_path = (
build_options.dependency_constraints.get_for_python_version(
config.version
)
)
# Bug in pip <= 21.1.3 - we can't have a space in the
# constraints file, and pip doesn't support drive letters
# in uhi. After probably pip 21.2, we can use uri. For
# now, use a temporary file.
if " " in str(constraints_path):
assert " " not in str(identifier_tmp_dir)
tmp_file = identifier_tmp_dir / "constraints.txt"
tmp_file.write_bytes(constraints_path.read_bytes())
constraints_path = tmp_file
built_wheel = next(built_wheel_dir.glob("*.whl"))
build_env["PIP_CONSTRAINT"] = str(constraints_path)
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
env=build_env,
)
else:
assert_never(build_options.build_frontend)
# repair the wheel
repaired_wheel_dir.mkdir()
built_wheel = next(built_wheel_dir.glob("*.whl"))
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
# repair the wheel
repaired_wheel_dir.mkdir()
if build_options.repair_command:
log.step("Repairing wheel...")
repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError()
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
if build_options.repair_command:
log.step("Repairing wheel...")
repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
if build_options.test_command and options.globals.test_selector(config.identifier):
log.step("Testing wheel...")
@@ -405,7 +418,9 @@ 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)
shutil.move(str(repaired_wheel), build_options.output_dir)
if abi3_wheel is None:
shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
# clean up
# (we ignore errors because occasionally Windows fails to unlink a file and we
+1 -1
View File
@@ -35,7 +35,7 @@ install_requires =
bracex
certifi
filelock
packaging
packaging>=20.9
platformdirs
dataclasses;python_version < '3.7'
tomli;python_version < '3.11'
+50
View File
@@ -0,0 +1,50 @@
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(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
]
assert set(actual_wheels) == set(expected_wheels)
+3
View File
@@ -54,6 +54,7 @@ setup(
'spam',
sources=['spam.c'],
libraries=libraries,
{{ setup_py_extension_args_add | indent(8) }}
)],
{{ setup_py_setup_args_add | indent(4) }}
)
@@ -73,6 +74,7 @@ def new_c_project(
spam_c_top_level_add="",
spam_c_function_add="",
setup_py_add="",
setup_py_extension_args_add="",
setup_py_setup_args_add="",
setup_cfg_add="",
):
@@ -91,6 +93,7 @@ def new_c_project(
"spam_c_top_level_add": spam_c_top_level_add,
"spam_c_function_add": spam_c_function_add,
"setup_py_add": setup_py_add,
"setup_py_extension_args_add": setup_py_extension_args_add,
"setup_py_setup_args_add": setup_py_setup_args_add,
"setup_cfg_add": setup_cfg_add,
}