-
Notifications
You must be signed in to change notification settings - Fork 298
Add support for abi3audit for Stable ABI wheels and run it automatically
#2745
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
base: main
Are you sure you want to change the base?
Changes from 10 commits
322f2c4
f917a24
f2fdc93
1054219
03d1708
97cc3c2
97b6d51
87ac303
babfb13
8d6988d
6e9c281
597f061
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -36,6 +36,7 @@ classifiers = [ | |||||
| "Topic :: Software Development :: Build Tools", | ||||||
| ] | ||||||
| dependencies = [ | ||||||
| "abi3audit", | ||||||
|
||||||
| "abi3audit", | |
| "abi3audit>=0.0.8", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/pypa/abi3audit/releases/tag/v0.0.26 lists Python 3.10 as the minimum supported version. There's no proper CHANGELOG for older versions (such as v0.0.8, as suggested here), so I'm not sure when the --strict flag was added.
But this begs the question: what's the best approach for us here? We wouldn't want to run different versions of abi3audit without the user knowing, right?
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| import subprocess | ||
| import textwrap | ||
|
|
||
| import pytest | ||
|
|
||
| from . import test_projects, utils | ||
|
|
||
| pyproject_toml = r""" | ||
| [build-system] | ||
| requires = ["setuptools", "wheel"] | ||
| build-backend = "setuptools.build_meta" | ||
| """ | ||
|
|
||
| limited_api_project = test_projects.new_c_project( | ||
| setup_py_add=textwrap.dedent( | ||
| r""" | ||
| import sysconfig | ||
|
|
||
| IS_CPYTHON = sys.implementation.name == "cpython" | ||
| Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED") | ||
| CAN_USE_ABI3 = IS_CPYTHON and not Py_GIL_DISABLED | ||
| setup_options = {} | ||
| extension_kwargs = {} | ||
| if CAN_USE_ABI3 and sys.version_info[:2] >= (3, 10): | ||
| extension_kwargs["define_macros"] = [("Py_LIMITED_API", "0x030A0000")] | ||
| extension_kwargs["py_limited_api"] = True | ||
| setup_options = {"bdist_wheel": {"py_limited_api": "cp310"}} | ||
| """ | ||
| ), | ||
| setup_py_extension_args_add="**extension_kwargs", | ||
| setup_py_setup_args_add="options=setup_options", | ||
| ) | ||
|
|
||
| limited_api_project.files["pyproject.toml"] = pyproject_toml | ||
|
|
||
| # Project that claims abi3 but violates the stable ABI by calling | ||
| # PyUnicode_AsUTF8 (not in stable ABI until 3.13) without defining | ||
| # Py_LIMITED_API in the C code. | ||
| violating_abi3_project = test_projects.new_c_project( | ||
| setup_py_add=textwrap.dedent( | ||
| r""" | ||
| import sysconfig | ||
|
|
||
| IS_CPYTHON = sys.implementation.name == "cpython" | ||
| Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED") | ||
| CAN_USE_ABI3 = IS_CPYTHON and not Py_GIL_DISABLED | ||
| setup_options = {} | ||
| extension_kwargs = {} | ||
| if CAN_USE_ABI3 and sys.version_info[:2] >= (3, 10): | ||
| # Intentionally NOT defining Py_LIMITED_API as a C macro, | ||
| # but still tagging the wheel as abi3. | ||
| extension_kwargs["py_limited_api"] = True | ||
| setup_options = {"bdist_wheel": {"py_limited_api": "cp310"}} | ||
| """ | ||
| ), | ||
| spam_c_function_add=textwrap.dedent( | ||
| r""" | ||
| // Call a function not in the stable ABI until Python 3.13. | ||
| // Without Py_LIMITED_API defined, the compiler allows it. | ||
| PyObject *str_obj = PyUnicode_FromString(content); | ||
| const char *utf8 = PyUnicode_AsUTF8(str_obj); | ||
| (void)utf8; | ||
| Py_DECREF(str_obj); | ||
| """ | ||
| ), | ||
| setup_py_extension_args_add="**extension_kwargs", | ||
| setup_py_setup_args_add="options=setup_options", | ||
| ) | ||
|
|
||
| violating_abi3_project.files["pyproject.toml"] = pyproject_toml | ||
|
|
||
|
|
||
| @utils.skip_if_pyodide("Pyodide doesn't build abi3 wheels, so abi3audit is not relevant") | ||
| def test_abi3audit_runs_on_abi3_wheel(tmp_path, capfd): | ||
| """Test that abi3audit runs automatically on abi3 wheels.""" | ||
| project_dir = tmp_path / "project" | ||
| limited_api_project.generate(project_dir) | ||
|
|
||
| actual_wheels = utils.cibuildwheel_run( | ||
| project_dir, | ||
| add_env={ | ||
| # Let's only build one cpython version to keep the test fast. | ||
| "CIBW_BUILD": "cp310-*", | ||
| "CIBW_ARCHS": "native", | ||
| }, | ||
| ) | ||
|
|
||
| assert len(actual_wheels) >= 1 | ||
|
|
||
| captured = capfd.readouterr() | ||
| assert "Running abi3audit" in captured.out | ||
|
|
||
|
|
||
| @utils.skip_if_pyodide("Pyodide doesn't build abi3 wheels, so abi3audit is not relevant") | ||
| def test_abi3audit_skipped_for_non_abi3_wheel(tmp_path, capfd): | ||
| """Test that abi3audit does not run for non-abi3 wheels.""" | ||
| project_dir = tmp_path / "project" | ||
| basic_project = test_projects.new_c_project() | ||
| basic_project.generate(project_dir) | ||
|
|
||
| actual_wheels = utils.cibuildwheel_run( | ||
| project_dir, | ||
| add_env={ | ||
| "CIBW_ARCHS": "native", | ||
| }, | ||
| single_python=True, | ||
| ) | ||
|
|
||
| assert len(actual_wheels) >= 1 | ||
|
|
||
| captured = capfd.readouterr() | ||
| assert "Running abi3audit" not in captured.out | ||
|
|
||
|
|
||
| @utils.skip_if_pyodide("Pyodide doesn't build abi3 wheels, so abi3audit is not relevant") | ||
| def test_abi3audit_detects_violation(tmp_path, capfd): | ||
| """Test that abi3audit catches stable ABI violations and fails the build. | ||
|
|
||
| This project tags the wheel as cp310-abi3 but uses PyUnicode_AsUTF8, | ||
| which was not part of the stable ABI until Python 3.13. | ||
| """ | ||
| project_dir = tmp_path / "project" | ||
| violating_abi3_project.generate(project_dir) | ||
|
|
||
| with pytest.raises(subprocess.CalledProcessError): | ||
| utils.cibuildwheel_run( | ||
| project_dir, | ||
| add_env={ | ||
| "CIBW_BUILD": "cp310-*", | ||
| "CIBW_ARCHS": "native", | ||
| }, | ||
| ) | ||
|
|
||
| captured = capfd.readouterr() | ||
| assert "Running abi3audit" in captured.out |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
|
|
||
| from cibuildwheel.util.packaging import is_abi3_wheel, run_abi3audit | ||
|
|
||
|
|
||
| class TestIsAbi3Wheel: | ||
| def test_abi3_wheel(self): | ||
| assert is_abi3_wheel("foo-1.0-cp310-abi3-manylinux_2_28_x86_64.whl") is True | ||
|
|
||
| def test_abi3_wheel_macos(self): | ||
| assert is_abi3_wheel("foo-1.0-cp311-abi3-macosx_11_0_arm64.whl") is True | ||
|
|
||
| def test_abi3_wheel_windows(self): | ||
| assert is_abi3_wheel("foo-1.0-cp310-abi3-win_amd64.whl") is True | ||
|
|
||
| def test_cpython_wheel(self): | ||
| assert is_abi3_wheel("foo-1.0-cp310-cp310-manylinux_2_28_x86_64.whl") is False | ||
|
|
||
| def test_none_any_wheel(self): | ||
| assert is_abi3_wheel("foo-1.0-py3-none-any.whl") is False | ||
|
|
||
| def test_none_platform_wheel(self): | ||
| assert is_abi3_wheel("foo-1.0-cp310-none-win_amd64.whl") is False | ||
|
|
||
|
|
||
| class TestRunAbi3audit: | ||
| def test_skips_non_abi3_wheel(self): | ||
| wheel = Path("/tmp/foo-1.0-cp310-cp310-manylinux_2_28_x86_64.whl") | ||
| with patch("subprocess.run") as mock_run: | ||
agriyakhetarpal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| run_abi3audit(wheel) | ||
| mock_run.assert_not_called() | ||
|
|
||
| def test_runs_on_abi3_wheel(self): | ||
| wheel = Path("/tmp/foo-1.0-cp310-abi3-manylinux_2_28_x86_64.whl") | ||
| with patch("cibuildwheel.util.packaging.subprocess.run") as mock_run: | ||
| run_abi3audit(wheel) | ||
| mock_run.assert_called_once_with( | ||
| [sys.executable, "-m", "abi3audit", "--strict", "--report", str(wheel)], | ||
| check=True, | ||
| ) | ||
|
|
||
| def test_raises_on_failure(self): | ||
| wheel = Path("/tmp/foo-1.0-cp310-abi3-manylinux_2_28_x86_64.whl") | ||
| with ( | ||
| patch( | ||
| "cibuildwheel.util.packaging.subprocess.run", | ||
| side_effect=subprocess.CalledProcessError(1, "abi3audit"), | ||
| ), | ||
| pytest.raises(subprocess.CalledProcessError), | ||
| ): | ||
| run_abi3audit(wheel) | ||
Uh oh!
There was an error while loading. Please reload this page.