* WIP - initial punt at audit command * Add `abi3audit` as a dependency * Add helper functions to check stable ABI wheels * Run `abi3audit` for macOS and Windows wheels * Copy out of container for repairing? * Add some notes that `cibuildwheel` runs `abi3audit` * Add basic unit tests * Add a basic C extension with `Py_LIMITED_API` * Add a test project that violates Stable ABI * Fix linux test * Skip abi3 wheel tests for Pyodide * Patch the correct subprocess module * wrap cleanup of abi3audit dir * Write the docs for the new options * Move to above testing in docs * Implement audit-requires and audit-command * Some cleanups after self-review * Add default value * fix type errors * the key is `audit-command`, not `audit` * Add a variety of tests for audit requires options * Add `test_audit_requires` similar to `test_test_requires` * Add some configurability-related audit tests * Fix parsing error with options docs leaving out commands * Better way to extract version (maybe helps Pyodide?) * Fix a case of unbound `use_uv` * Standardise: rename to `abi3_wheel` * Fix audit command run message * Simplify custom audit command a bit * Remove unnecessary skip for Pyodide * Pyodide should have no default audit command * More accurate skip messages for Pyodide skips * Wheels are audited after they are repaired * Regenerate constraints to include `abi3audit` * Fix typos * Some attempts for Windows fixes * Check `pyvenv.cfg` instead of directory existence * Add validation for lack of wheel placeholders * Try yet another Windows `uv` fix * Regenerate diagram and re-trigger Azure CI * Add missing `import sys` for abi3 C extension tests * Remove audit-command at the global level * Clarify `abi3audit` pinning a little bit * Regen constraints * Discard changes to cibuildwheel/resources/constraints-pyodide312.txt * Discard changes to cibuildwheel/resources/constraints-pyodide313.txt * try opt-in uv again * fix issue on windows on Python 3.13 related to nested venvs On win / python 3.13, virtualenv creates a venv where the 'home' points back to the venv that sys.executable was running in, rather than the root install. that seemingly leads to problems with package resolution, where pip.exe couldn't find the pip python package. this appears to fix it! * Update constraints * chore: revert python-discovery bump Assisted-by: OpenCode:glm-5.1 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: restore workaround for graalpy Assisted-by: OpenCode:glm-5.1 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> --------- Signed-off-by: Henry Schreiner <henryfs@princeton.edu> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Co-authored-by: Henry Schreiner <henryfs@princeton.edu>
216 lines
6.8 KiB
Python
216 lines
6.8 KiB
Python
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
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 sys
|
|
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 sys
|
|
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("abi3audit is disabled on Pyodide (wasm shared objects are not supported)")
|
|
def test_abi3audit_runs_on_abi3_wheel(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
"""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 audit command: abi3audit" in captured.out
|
|
|
|
|
|
def test_abi3audit_skipped_for_non_abi3_wheel(
|
|
tmp_path: Path, capfd: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""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 audit command: abi3audit" not in captured.out
|
|
|
|
|
|
@utils.skip_if_pyodide("abi3audit is disabled on Pyodide (wasm shared objects are not supported)")
|
|
def test_abi3audit_detects_violation(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
"""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 audit command: abi3audit" in captured.out
|
|
|
|
|
|
def test_custom_audit_command(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
project_dir = tmp_path / "project"
|
|
test_projects.new_c_project().generate(project_dir)
|
|
|
|
actual_wheels = utils.cibuildwheel_run(
|
|
project_dir,
|
|
add_env={
|
|
"CIBW_AUDIT_COMMAND": "echo custom-audit-ok {wheel}",
|
|
"CIBW_AUDIT_REQUIRES": "",
|
|
"CIBW_ARCHS": "native",
|
|
},
|
|
single_python=True,
|
|
)
|
|
|
|
assert len(actual_wheels) >= 1
|
|
captured = capfd.readouterr()
|
|
assert "Auditing wheel" in captured.out
|
|
assert "custom-audit-ok" in captured.out
|
|
|
|
|
|
def test_custom_audit_requires(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
project_dir = tmp_path / "project"
|
|
test_projects.new_c_project().generate(project_dir)
|
|
|
|
actual_wheels = utils.cibuildwheel_run(
|
|
project_dir,
|
|
add_env={
|
|
"CIBW_AUDIT_REQUIRES": "pycowsay",
|
|
"CIBW_AUDIT_COMMAND": "pycowsay moo {wheel}",
|
|
"CIBW_ARCHS": "native",
|
|
},
|
|
single_python=True,
|
|
)
|
|
|
|
assert len(actual_wheels) >= 1
|
|
captured = capfd.readouterr()
|
|
assert "Installing audit dependencies: pycowsay" in captured.out
|
|
assert "moo" in captured.out
|
|
|
|
|
|
def test_empty_audit_command_disables_audit(
|
|
tmp_path: Path, capfd: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project_dir = tmp_path / "project"
|
|
test_projects.new_c_project().generate(project_dir)
|
|
|
|
actual_wheels = utils.cibuildwheel_run(
|
|
project_dir,
|
|
add_env={
|
|
"CIBW_AUDIT_COMMAND": "",
|
|
"CIBW_ARCHS": "native",
|
|
},
|
|
single_python=True,
|
|
)
|
|
|
|
assert len(actual_wheels) >= 1
|
|
captured = capfd.readouterr()
|
|
assert "Auditing wheel" not in captured.out
|
|
|
|
|
|
def test_custom_audit_command_failure(tmp_path: Path) -> None:
|
|
project_dir = tmp_path / "project"
|
|
test_projects.new_c_project().generate(project_dir)
|
|
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
utils.cibuildwheel_run(
|
|
project_dir,
|
|
add_env={
|
|
"CIBW_AUDIT_COMMAND": 'python -c "import sys; sys.exit(1)" {wheel}',
|
|
"CIBW_AUDIT_REQUIRES": "",
|
|
"CIBW_ARCHS": "native",
|
|
},
|
|
single_python=True,
|
|
)
|