feature: add support for ABI3 wheels
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user