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
+16
View File
@@ -12,6 +12,7 @@ from .typing import OrderedDict, PathOrStr, assert_never
from .util import ( from .util import (
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
find_compatible_abi3_wheel,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
prepare_command, prepare_command,
read_python_configs, read_python_configs,
@@ -132,6 +133,8 @@ def build_on_docker(
) )
docker.call(["sh", "-c", before_all_prepared], env=env) docker.call(["sh", "-c", before_all_prepared], env=env)
built_wheels: List[PurePath] = []
for config in platform_configs: for config in platform_configs:
log.build_start(config.identifier) log.build_start(config.identifier)
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
@@ -174,6 +177,15 @@ def build_on_docker(
) )
sys.exit(1) sys.exit(1)
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:
if build_options.before_build: if build_options.before_build:
log.step("Running before_build...") log.step("Running before_build...")
before_build_prepared = prepare_command( before_build_prepared = prepare_command(
@@ -292,8 +304,12 @@ def build_on_docker(
docker.call(["rm", "-rf", venv_dir]) docker.call(["rm", "-rf", venv_dir])
# move repaired wheels to output # move repaired wheels to output
if abi3_wheel is None:
docker.call(["mkdir", "-p", container_output_dir]) docker.call(["mkdir", "-p", container_output_dir])
docker.call(["mv", *repaired_wheels, 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() log.build_end()
+16 -1
View File
@@ -23,6 +23,7 @@ from .util import (
call, call,
detect_ci_provider, detect_ci_provider,
download, download,
find_compatible_abi3_wheel,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
get_pip_version, get_pip_version,
install_certifi_script, install_certifi_script,
@@ -291,6 +292,8 @@ def build(options: Options, tmp_path: Path) -> None:
) )
shell(before_all_prepared, env=env) shell(before_all_prepared, env=env)
built_wheels: List[Path] = []
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
log.build_start(config.identifier) log.build_start(config.identifier)
@@ -318,6 +321,14 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend, build_options.build_frontend,
) )
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_wheel = abi3_wheel
else:
if build_options.before_build: if build_options.before_build:
log.step("Running before_build...") log.step("Running before_build...")
before_build_prepared = prepare_command( before_build_prepared = prepare_command(
@@ -348,9 +359,11 @@ def build(options: Options, tmp_path: Path) -> None:
config_setting = " ".join(verbosity_flags) config_setting = " ".join(verbosity_flags)
build_env = env.copy() build_env = env.copy()
if build_options.dependency_constraints: if build_options.dependency_constraints:
constraint_path = build_options.dependency_constraints.get_for_python_version( constraint_path = (
build_options.dependency_constraints.get_for_python_version(
config.version config.version
) )
)
build_env["PIP_CONSTRAINT"] = constraint_path.as_uri() build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
build_env["VIRTUALENV_PIP"] = get_pip_version(env) build_env["VIRTUALENV_PIP"] = get_pip_version(env)
call( call(
@@ -521,7 +534,9 @@ def build(options: Options, tmp_path: Path) -> None:
) )
# we're all done here; move it to output (overwrite existing) # we're all done here; move it to output (overwrite existing)
if abi3_wheel is None:
shutil.move(str(repaired_wheel), build_options.output_dir) shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
# clean up # clean up
shutil.rmtree(identifier_tmp_dir) shutil.rmtree(identifier_tmp_dir)
+40 -1
View File
@@ -13,7 +13,7 @@ import time
import urllib.request import urllib.request
from enum import Enum from enum import Enum
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path, PurePath
from time import sleep from time import sleep
from typing import ( from typing import (
Any, Any,
@@ -26,6 +26,7 @@ from typing import (
Optional, Optional,
Sequence, Sequence,
TextIO, TextIO,
TypeVar,
cast, cast,
overload, overload,
) )
@@ -41,6 +42,7 @@ else:
from filelock import FileLock from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet from packaging.specifiers import SpecifierSet
from packaging.utils import parse_wheel_filename
from packaging.version import Version from packaging.version import Version
from platformdirs import user_cache_path from platformdirs import user_cache_path
@@ -51,6 +53,7 @@ __all__ = [
"MANYLINUX_ARCHS", "MANYLINUX_ARCHS",
"call", "call",
"shell", "shell",
"find_compatible_abi3_wheel",
"format_safe", "format_safe",
"prepare_command", "prepare_command",
"get_build_verbosity_extra_flags", "get_build_verbosity_extra_flags",
@@ -566,6 +569,42 @@ def virtualenv(
return env 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): if sys.version_info >= (3, 8):
from functools import cached_property from functools import cached_property
else: else:
+16 -1
View File
@@ -22,6 +22,7 @@ from .util import (
NonPlatformWheelError, NonPlatformWheelError,
call, call,
download, download,
find_compatible_abi3_wheel,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
get_pip_version, get_pip_version,
prepare_command, prepare_command,
@@ -249,6 +250,8 @@ def build(options: Options, tmp_path: Path) -> None:
) )
shell(before_all_prepared, env=env) shell(before_all_prepared, env=env)
built_wheels: List[Path] = []
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
log.build_start(config.identifier) log.build_start(config.identifier)
@@ -274,6 +277,14 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.build_frontend, build_options.build_frontend,
) )
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_wheel = abi3_wheel
else:
# run the before_build command # run the before_build command
if build_options.before_build: if build_options.before_build:
log.step("Running before_build...") log.step("Running before_build...")
@@ -305,9 +316,11 @@ def build(options: Options, tmp_path: Path) -> None:
config_setting = " ".join(verbosity_flags) config_setting = " ".join(verbosity_flags)
build_env = env.copy() build_env = env.copy()
if build_options.dependency_constraints: if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version( constraints_path = (
build_options.dependency_constraints.get_for_python_version(
config.version config.version
) )
)
# Bug in pip <= 21.1.3 - we can't have a space in the # Bug in pip <= 21.1.3 - we can't have a space in the
# constraints file, and pip doesn't support drive letters # constraints file, and pip doesn't support drive letters
# in uhi. After probably pip 21.2, we can use uri. For # in uhi. After probably pip 21.2, we can use uri. For
@@ -405,7 +418,9 @@ def build(options: Options, tmp_path: Path) -> None:
shell(test_command_prepared, cwd="c:\\", env=virtualenv_env) shell(test_command_prepared, cwd="c:\\", env=virtualenv_env)
# we're all done here; move it to output (remove if already exists) # we're all done here; move it to output (remove if already exists)
if abi3_wheel is None:
shutil.move(str(repaired_wheel), build_options.output_dir) shutil.move(str(repaired_wheel), build_options.output_dir)
built_wheels.append(build_options.output_dir / repaired_wheel.name)
# clean up # clean up
# (we ignore errors because occasionally Windows fails to unlink a file and we # (we ignore errors because occasionally Windows fails to unlink a file and we
+1 -1
View File
@@ -35,7 +35,7 @@ install_requires =
bracex bracex
certifi certifi
filelock filelock
packaging packaging>=20.9
platformdirs platformdirs
dataclasses;python_version < '3.7' dataclasses;python_version < '3.7'
tomli;python_version < '3.11' 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', 'spam',
sources=['spam.c'], sources=['spam.c'],
libraries=libraries, libraries=libraries,
{{ setup_py_extension_args_add | indent(8) }}
)], )],
{{ setup_py_setup_args_add | indent(4) }} {{ setup_py_setup_args_add | indent(4) }}
) )
@@ -73,6 +74,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_setup_args_add="", setup_py_setup_args_add="",
setup_cfg_add="", setup_cfg_add="",
): ):
@@ -91,6 +93,7 @@ def new_c_project(
"spam_c_top_level_add": spam_c_top_level_add, "spam_c_top_level_add": spam_c_top_level_add,
"spam_c_function_add": spam_c_function_add, "spam_c_function_add": spam_c_function_add,
"setup_py_add": setup_py_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_py_setup_args_add": setup_py_setup_args_add,
"setup_cfg_add": setup_cfg_add, "setup_cfg_add": setup_cfg_add,
} }