* 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>
220 lines
6.6 KiB
Python
220 lines
6.6 KiB
Python
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
|
|
|
|
|
|
def test_abi3(tmp_path: Path) -> None:
|
|
project_dir = tmp_path / "project"
|
|
limited_api_project.generate(project_dir)
|
|
|
|
# build the wheels
|
|
actual_wheels = utils.cibuildwheel_run(
|
|
project_dir,
|
|
add_env={
|
|
# free_threaded, GraalPy, and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
|
|
# pyodide uses cp313 (the stable version) which supports limited API / abi3
|
|
# also limit the number of builds for test performance reasons
|
|
"CIBW_BUILD": (
|
|
"cp313-*"
|
|
if utils.get_platform() == "pyodide"
|
|
else "cp39-* cp310-* pp310-* gp312_250-* cp312-* cp314t-*"
|
|
),
|
|
"CIBW_ENABLE": "all",
|
|
},
|
|
)
|
|
|
|
# check that the expected wheels are produced
|
|
if utils.get_platform() == "pyodide":
|
|
# there's only 1 possible configuration for pyodide, cp313. It builds
|
|
# a wheel that is tagged abi3, compatible back to 3.10
|
|
expected_wheels = utils.expected_wheels(
|
|
"spam",
|
|
"0.1.0",
|
|
python_abi_tags=["cp310-abi3"],
|
|
)
|
|
else:
|
|
expected_wheels = utils.expected_wheels(
|
|
"spam",
|
|
"0.1.0",
|
|
python_abi_tags=[
|
|
"cp39-cp39",
|
|
"cp310-abi3", # <-- ABI3, works with 3.10 and 3.12
|
|
"cp314-cp314t",
|
|
"pp310-pypy310_pp73",
|
|
"graalpy312-graalpy250_312_native",
|
|
],
|
|
)
|
|
|
|
assert set(actual_wheels) == set(expected_wheels)
|
|
|
|
|
|
ctypes_project = test_projects.TestProject()
|
|
ctypes_project.files["setup.py"] = textwrap.dedent(
|
|
"""
|
|
from setuptools import setup, Extension
|
|
|
|
from distutils.command.build_ext import build_ext as _build_ext
|
|
class CTypesExtension(Extension): pass
|
|
class build_ext(_build_ext):
|
|
def build_extension(self, ext):
|
|
self._ctypes = isinstance(ext, CTypesExtension)
|
|
return super().build_extension(ext)
|
|
|
|
def get_export_symbols(self, ext):
|
|
if self._ctypes:
|
|
return ext.export_symbols
|
|
return super().get_export_symbols(ext)
|
|
|
|
def get_ext_filename(self, ext_name):
|
|
if self._ctypes:
|
|
return ext_name + '.so'
|
|
return super().get_ext_filename(ext_name)
|
|
|
|
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
|
|
class bdist_wheel_abi_none(_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 "py3", "none", plat
|
|
|
|
setup(
|
|
name="ctypesexample",
|
|
version="1.0.0",
|
|
package_dir = {"": "src"},
|
|
py_modules = ["ctypesexample.summing"],
|
|
ext_modules=[
|
|
CTypesExtension(
|
|
"ctypesexample.csumlib",
|
|
["src/ctypesexample/csumlib.c"],
|
|
),
|
|
],
|
|
cmdclass={'build_ext': build_ext, 'bdist_wheel': bdist_wheel_abi_none},
|
|
)
|
|
"""
|
|
)
|
|
ctypes_project.files["src/ctypesexample/csumlib.c"] = textwrap.dedent(
|
|
"""
|
|
#ifdef _WIN32
|
|
#define LIBRARY_API __declspec(dllexport)
|
|
#else
|
|
#define LIBRARY_API
|
|
#endif
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
LIBRARY_API double *add_vec3(double *a, double *b)
|
|
{
|
|
double *res = malloc(sizeof(double) * 3);
|
|
|
|
for (int i = 0; i < 3; ++i)
|
|
{
|
|
res[i] = a[i] + b[i];
|
|
}
|
|
|
|
return res;
|
|
}
|
|
"""
|
|
)
|
|
ctypes_project.files["src/ctypesexample/summing.py"] = textwrap.dedent(
|
|
"""
|
|
import ctypes
|
|
import pathlib
|
|
|
|
# path of the shared library
|
|
libfile = pathlib.Path(__file__).parent / "csumlib.so"
|
|
csumlib = ctypes.CDLL(str(libfile))
|
|
|
|
type_vec3 = ctypes.POINTER(ctypes.c_double * 3)
|
|
|
|
csumlib.add_vec3.restype = type_vec3
|
|
csumlib.add_vec3.argtypes = [type_vec3, type_vec3]
|
|
def add(a: list, b: list) -> list:
|
|
a_p = (ctypes.c_double * 3)(*a)
|
|
b_p = (ctypes.c_double * 3)(*b)
|
|
r_p = csumlib.add_vec3(a_p,b_p)
|
|
|
|
return [l for l in r_p.contents]
|
|
"""
|
|
)
|
|
|
|
ctypes_project.files["test/add_test.py"] = textwrap.dedent(
|
|
"""
|
|
import ctypesexample.summing
|
|
|
|
def test():
|
|
a = [1, 2, 3]
|
|
b = [4, 5, 6]
|
|
assert ctypesexample.summing.add(a, b) == [5, 7, 9]
|
|
"""
|
|
)
|
|
|
|
ctypes_project.files["pyproject.toml"] = pyproject_toml
|
|
|
|
|
|
def test_abi_none(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
project_dir = tmp_path / "project"
|
|
ctypes_project.generate(project_dir)
|
|
|
|
# build the wheels
|
|
actual_wheels = utils.cibuildwheel_run(
|
|
project_dir,
|
|
add_env={
|
|
"CIBW_TEST_REQUIRES": "pytest",
|
|
"CIBW_TEST_COMMAND": f"{utils.invoke_pytest()} {{project}}/test",
|
|
# limit the number of builds for test performance reasons
|
|
"CIBW_BUILD": "cp39-* cp{}{}-* cp314t-* pp310-*".format(*utils.SINGLE_PYTHON_VERSION),
|
|
"CIBW_ENABLE": "all",
|
|
},
|
|
)
|
|
|
|
expected_wheels = utils.expected_wheels("ctypesexample", "1.0.0", python_abi_tags=["py3-none"])
|
|
# check that the expected wheels are produced
|
|
assert set(actual_wheels) == set(expected_wheels)
|
|
|
|
captured = capfd.readouterr()
|
|
|
|
if utils.get_platform() == "pyodide":
|
|
# pyodide builds a different platform tag for each python version, so
|
|
# wheels are not reused
|
|
assert "Found previously built wheel" not in captured.out
|
|
else:
|
|
# check that each wheel was built once, and reused
|
|
assert "Building wheel..." in captured.out
|
|
assert "Found previously built wheel" in captured.out
|