diff --git a/libaegis b/libaegis index 4a23400..7b667dd 160000 --- a/libaegis +++ b/libaegis @@ -1 +1 @@ -Subproject commit 4a234009c94454fe818ed1b19091c0c41a1c63d7 +Subproject commit 7b667dd88318648da1714997b1de9d6656db69cc diff --git a/pyproject.toml b/pyproject.toml index a2b9171..1e1b124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,6 @@ package-dir = {"" = "src"} packages = ["aeg"] [tool.setuptools.package-data] -aeg = ["*.h", "*.so", "*.pyd"] +aeg = ["*.h"] [tool.setuptools_scm] diff --git a/setup.py b/setup.py index 3708ace..2f4840f 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,24 @@ """Setup script for aeg - builds CFFI extension with libaegis C library.""" import sys +import sysconfig from pathlib import Path from cffi import FFI from setuptools import setup -# Locate the static library (built by build_backend.py before this runs) -lib_name = "aegis.lib" if sys.platform == "win32" else "libaegis.a" -libaegis_static = Path("libaegis/zig-out/lib") / lib_name -if not libaegis_static.exists(): - raise RuntimeError(f"libaegis static library not found at {libaegis_static}") -libaegis_static = str(libaegis_static.resolve()) +libaegis_static = Path("libaegis/zig-out/lib") / ( + "aegis.lib" if sys.platform == "win32" else "libaegis.a" +) -# Include directory for headers -libaegis_include = Path("libaegis/src/include") -if not libaegis_include.exists(): - raise RuntimeError(f"libaegis include directory not found at {libaegis_include}") -include_dirs = [str(libaegis_include)] - -# Read the CDEF header -cdef_path = Path(__file__).parent / "src" / "aeg" / "aegis_cdef.h" -cdef_content = cdef_path.read_text(encoding="utf-8") - -# Create CFFI builder ffibuilder = FFI() -ffibuilder.cdef(cdef_content) +ffibuilder.cdef((Path(__file__).parent / "src/aeg/aegis_cdef.h").read_text()) + +# Free-threaded Python does not support Limited API (abi3) +is_free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") -# Set the source ffibuilder.set_source( - "aeg._aegis", # module name + "aeg._aegis", """ #include "aegis.h" #include "aegis128l.h" @@ -39,11 +28,15 @@ ffibuilder.set_source( #include "aegis256x2.h" #include "aegis256x4.h" """, - include_dirs=include_dirs, - extra_objects=[libaegis_static], + include_dirs=["libaegis/src/include"], + extra_objects=[str(libaegis_static.resolve())], + py_limited_api=not is_free_threaded, ) if __name__ == "__main__": setup( cffi_modules=["setup.py:ffibuilder"], + options=( + {"bdist_wheel": {"py_limited_api": "cp310"}} if not is_free_threaded else {} + ), ) diff --git a/src/aeg/aegis128l.py b/src/aeg/aegis128l.py index d76f940..4e3638c 100644 --- a/src/aeg/aegis128l.py +++ b/src/aeg/aegis128l.py @@ -708,24 +708,17 @@ class Encryptor: raise TypeError( "into length must be >= expected output size for this update" ) - written = ffi.new("size_t *") rc = _lib.aegis128l_state_encrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(message), message.nbytes, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") - raise RuntimeError( - f"state encrypt update failed: {err_name} written {written[0]}" - ) - w = int(written[0]) - assert w == expected_out - return out if into is None else memoryview(out)[:w] # type: ignore + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, into: Buffer | None = None) -> bytearray | memoryview: """Finalize encryption and return the authentication tag. @@ -746,24 +739,17 @@ class Encryptor: if into is not None: into = memoryview(into) out = into if into is not None else bytearray(maclen) - written = ffi.new("size_t *") rc = _lib.aegis128l_state_encrypt_final( self._state.ptr, ffi.from_buffer(out), - memoryview(out).nbytes, - written, maclen, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state encrypt final failed: {err_name}") - w = int(written[0]) - if into is None: - # Only the tag bytes are returned when we allocate the buffer - assert w == maclen self._state = None - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:maclen] # type: ignore class Decryptor: @@ -837,12 +823,9 @@ class Decryptor: out_mv = memoryview(out) if out_mv.nbytes < expected_out: raise TypeError("into length must be >= required capacity for this update") - written = ffi.new("size_t *") - rc = _lib.aegis128l_state_decrypt_detached_update( + rc = _lib.aegis128l_state_decrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(ct), ct.nbytes, ) @@ -850,11 +833,7 @@ class Decryptor: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state decrypt update failed: {err_name}") - w = int(written[0]) - assert w == expected_out, ( - f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}" - ) - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, mac: Buffer) -> None: """Finalize decryption by verifying the MAC tag. @@ -873,9 +852,7 @@ class Decryptor: mac = memoryview(mac) if mac.nbytes != maclen: raise TypeError(f"mac length must be {maclen}") - rc = _lib.aegis128l_state_decrypt_detached_final( - self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen - ) + rc = _lib.aegis128l_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) if rc != 0: raise ValueError("authentication failed") self._state = None diff --git a/src/aeg/aegis128x2.py b/src/aeg/aegis128x2.py index 52f8afa..83a29d8 100644 --- a/src/aeg/aegis128x2.py +++ b/src/aeg/aegis128x2.py @@ -708,24 +708,17 @@ class Encryptor: raise TypeError( "into length must be >= expected output size for this update" ) - written = ffi.new("size_t *") rc = _lib.aegis128x2_state_encrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(message), message.nbytes, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") - raise RuntimeError( - f"state encrypt update failed: {err_name} written {written[0]}" - ) - w = int(written[0]) - assert w == expected_out - return out if into is None else memoryview(out)[:w] # type: ignore + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, into: Buffer | None = None) -> bytearray | memoryview: """Finalize encryption and return the authentication tag. @@ -746,24 +739,17 @@ class Encryptor: if into is not None: into = memoryview(into) out = into if into is not None else bytearray(maclen) - written = ffi.new("size_t *") rc = _lib.aegis128x2_state_encrypt_final( self._state.ptr, ffi.from_buffer(out), - memoryview(out).nbytes, - written, maclen, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state encrypt final failed: {err_name}") - w = int(written[0]) - if into is None: - # Only the tag bytes are returned when we allocate the buffer - assert w == maclen self._state = None - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:maclen] # type: ignore class Decryptor: @@ -837,12 +823,9 @@ class Decryptor: out_mv = memoryview(out) if out_mv.nbytes < expected_out: raise TypeError("into length must be >= required capacity for this update") - written = ffi.new("size_t *") - rc = _lib.aegis128x2_state_decrypt_detached_update( + rc = _lib.aegis128x2_state_decrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(ct), ct.nbytes, ) @@ -850,11 +833,7 @@ class Decryptor: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state decrypt update failed: {err_name}") - w = int(written[0]) - assert w == expected_out, ( - f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}" - ) - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, mac: Buffer) -> None: """Finalize decryption by verifying the MAC tag. @@ -873,9 +852,7 @@ class Decryptor: mac = memoryview(mac) if mac.nbytes != maclen: raise TypeError(f"mac length must be {maclen}") - rc = _lib.aegis128x2_state_decrypt_detached_final( - self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen - ) + rc = _lib.aegis128x2_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) if rc != 0: raise ValueError("authentication failed") self._state = None diff --git a/src/aeg/aegis128x4.py b/src/aeg/aegis128x4.py index d5defc8..03f42e7 100644 --- a/src/aeg/aegis128x4.py +++ b/src/aeg/aegis128x4.py @@ -708,24 +708,17 @@ class Encryptor: raise TypeError( "into length must be >= expected output size for this update" ) - written = ffi.new("size_t *") rc = _lib.aegis128x4_state_encrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(message), message.nbytes, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") - raise RuntimeError( - f"state encrypt update failed: {err_name} written {written[0]}" - ) - w = int(written[0]) - assert w == expected_out - return out if into is None else memoryview(out)[:w] # type: ignore + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, into: Buffer | None = None) -> bytearray | memoryview: """Finalize encryption and return the authentication tag. @@ -746,24 +739,17 @@ class Encryptor: if into is not None: into = memoryview(into) out = into if into is not None else bytearray(maclen) - written = ffi.new("size_t *") rc = _lib.aegis128x4_state_encrypt_final( self._state.ptr, ffi.from_buffer(out), - memoryview(out).nbytes, - written, maclen, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state encrypt final failed: {err_name}") - w = int(written[0]) - if into is None: - # Only the tag bytes are returned when we allocate the buffer - assert w == maclen self._state = None - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:maclen] # type: ignore class Decryptor: @@ -837,12 +823,9 @@ class Decryptor: out_mv = memoryview(out) if out_mv.nbytes < expected_out: raise TypeError("into length must be >= required capacity for this update") - written = ffi.new("size_t *") - rc = _lib.aegis128x4_state_decrypt_detached_update( + rc = _lib.aegis128x4_state_decrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(ct), ct.nbytes, ) @@ -850,11 +833,7 @@ class Decryptor: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state decrypt update failed: {err_name}") - w = int(written[0]) - assert w == expected_out, ( - f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}" - ) - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, mac: Buffer) -> None: """Finalize decryption by verifying the MAC tag. @@ -873,9 +852,7 @@ class Decryptor: mac = memoryview(mac) if mac.nbytes != maclen: raise TypeError(f"mac length must be {maclen}") - rc = _lib.aegis128x4_state_decrypt_detached_final( - self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen - ) + rc = _lib.aegis128x4_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) if rc != 0: raise ValueError("authentication failed") self._state = None diff --git a/src/aeg/aegis256.py b/src/aeg/aegis256.py index e1b9719..3041a13 100644 --- a/src/aeg/aegis256.py +++ b/src/aeg/aegis256.py @@ -708,24 +708,17 @@ class Encryptor: raise TypeError( "into length must be >= expected output size for this update" ) - written = ffi.new("size_t *") rc = _lib.aegis256_state_encrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(message), message.nbytes, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") - raise RuntimeError( - f"state encrypt update failed: {err_name} written {written[0]}" - ) - w = int(written[0]) - assert w == expected_out - return out if into is None else memoryview(out)[:w] # type: ignore + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, into: Buffer | None = None) -> bytearray | memoryview: """Finalize encryption and return the authentication tag. @@ -746,24 +739,17 @@ class Encryptor: if into is not None: into = memoryview(into) out = into if into is not None else bytearray(maclen) - written = ffi.new("size_t *") rc = _lib.aegis256_state_encrypt_final( self._state.ptr, ffi.from_buffer(out), - memoryview(out).nbytes, - written, maclen, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state encrypt final failed: {err_name}") - w = int(written[0]) - if into is None: - # Only the tag bytes are returned when we allocate the buffer - assert w == maclen self._state = None - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:maclen] # type: ignore class Decryptor: @@ -837,12 +823,9 @@ class Decryptor: out_mv = memoryview(out) if out_mv.nbytes < expected_out: raise TypeError("into length must be >= required capacity for this update") - written = ffi.new("size_t *") - rc = _lib.aegis256_state_decrypt_detached_update( + rc = _lib.aegis256_state_decrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(ct), ct.nbytes, ) @@ -850,11 +833,7 @@ class Decryptor: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state decrypt update failed: {err_name}") - w = int(written[0]) - assert w == expected_out, ( - f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}" - ) - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, mac: Buffer) -> None: """Finalize decryption by verifying the MAC tag. @@ -873,9 +852,7 @@ class Decryptor: mac = memoryview(mac) if mac.nbytes != maclen: raise TypeError(f"mac length must be {maclen}") - rc = _lib.aegis256_state_decrypt_detached_final( - self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen - ) + rc = _lib.aegis256_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) if rc != 0: raise ValueError("authentication failed") self._state = None diff --git a/src/aeg/aegis256x2.py b/src/aeg/aegis256x2.py index 3a750a7..3304f8b 100644 --- a/src/aeg/aegis256x2.py +++ b/src/aeg/aegis256x2.py @@ -708,24 +708,17 @@ class Encryptor: raise TypeError( "into length must be >= expected output size for this update" ) - written = ffi.new("size_t *") rc = _lib.aegis256x2_state_encrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(message), message.nbytes, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") - raise RuntimeError( - f"state encrypt update failed: {err_name} written {written[0]}" - ) - w = int(written[0]) - assert w == expected_out - return out if into is None else memoryview(out)[:w] # type: ignore + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, into: Buffer | None = None) -> bytearray | memoryview: """Finalize encryption and return the authentication tag. @@ -746,24 +739,17 @@ class Encryptor: if into is not None: into = memoryview(into) out = into if into is not None else bytearray(maclen) - written = ffi.new("size_t *") rc = _lib.aegis256x2_state_encrypt_final( self._state.ptr, ffi.from_buffer(out), - memoryview(out).nbytes, - written, maclen, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state encrypt final failed: {err_name}") - w = int(written[0]) - if into is None: - # Only the tag bytes are returned when we allocate the buffer - assert w == maclen self._state = None - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:maclen] # type: ignore class Decryptor: @@ -837,12 +823,9 @@ class Decryptor: out_mv = memoryview(out) if out_mv.nbytes < expected_out: raise TypeError("into length must be >= required capacity for this update") - written = ffi.new("size_t *") - rc = _lib.aegis256x2_state_decrypt_detached_update( + rc = _lib.aegis256x2_state_decrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(ct), ct.nbytes, ) @@ -850,11 +833,7 @@ class Decryptor: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state decrypt update failed: {err_name}") - w = int(written[0]) - assert w == expected_out, ( - f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}" - ) - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, mac: Buffer) -> None: """Finalize decryption by verifying the MAC tag. @@ -873,9 +852,7 @@ class Decryptor: mac = memoryview(mac) if mac.nbytes != maclen: raise TypeError(f"mac length must be {maclen}") - rc = _lib.aegis256x2_state_decrypt_detached_final( - self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen - ) + rc = _lib.aegis256x2_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) if rc != 0: raise ValueError("authentication failed") self._state = None diff --git a/src/aeg/aegis256x4.py b/src/aeg/aegis256x4.py index 9f0a640..5677b58 100644 --- a/src/aeg/aegis256x4.py +++ b/src/aeg/aegis256x4.py @@ -708,24 +708,17 @@ class Encryptor: raise TypeError( "into length must be >= expected output size for this update" ) - written = ffi.new("size_t *") rc = _lib.aegis256x4_state_encrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(message), message.nbytes, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") - raise RuntimeError( - f"state encrypt update failed: {err_name} written {written[0]}" - ) - w = int(written[0]) - assert w == expected_out - return out if into is None else memoryview(out)[:w] # type: ignore + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, into: Buffer | None = None) -> bytearray | memoryview: """Finalize encryption and return the authentication tag. @@ -746,24 +739,17 @@ class Encryptor: if into is not None: into = memoryview(into) out = into if into is not None else bytearray(maclen) - written = ffi.new("size_t *") rc = _lib.aegis256x4_state_encrypt_final( self._state.ptr, ffi.from_buffer(out), - memoryview(out).nbytes, - written, maclen, ) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state encrypt final failed: {err_name}") - w = int(written[0]) - if into is None: - # Only the tag bytes are returned when we allocate the buffer - assert w == maclen self._state = None - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:maclen] # type: ignore class Decryptor: @@ -837,12 +823,9 @@ class Decryptor: out_mv = memoryview(out) if out_mv.nbytes < expected_out: raise TypeError("into length must be >= required capacity for this update") - written = ffi.new("size_t *") - rc = _lib.aegis256x4_state_decrypt_detached_update( + rc = _lib.aegis256x4_state_decrypt_update( self._state.ptr, ffi.from_buffer(out_mv), - out_mv.nbytes, - written, _ptr(ct), ct.nbytes, ) @@ -850,11 +833,7 @@ class Decryptor: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"state decrypt update failed: {err_name}") - w = int(written[0]) - assert w == expected_out, ( - f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}" - ) - return out if into is None else memoryview(out)[:w] # type: ignore + return out if into is None else memoryview(out)[:expected_out] # type: ignore def final(self, mac: Buffer) -> None: """Finalize decryption by verifying the MAC tag. @@ -873,9 +852,7 @@ class Decryptor: mac = memoryview(mac) if mac.nbytes != maclen: raise TypeError(f"mac length must be {maclen}") - rc = _lib.aegis256x4_state_decrypt_detached_final( - self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen - ) + rc = _lib.aegis256x4_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) if rc != 0: raise ValueError("authentication failed") self._state = None diff --git a/src/aeg/aegis_cdef.h b/src/aeg/aegis_cdef.h index 74e15e8..d07cb59 100644 --- a/src/aeg/aegis_cdef.h +++ b/src/aeg/aegis_cdef.h @@ -55,35 +55,10 @@ void aegis128l_state_init(aegis128l_state *st_, size_t adlen, const uint8_t *npub, const uint8_t *k); -int aegis128l_state_encrypt_update(aegis128l_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - const uint8_t *m, - size_t mlen); -int aegis128l_state_encrypt_detached_final(aegis128l_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - uint8_t *mac, - size_t maclen); -int aegis128l_state_encrypt_final(aegis128l_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - size_t maclen); -int aegis128l_state_decrypt_detached_update(aegis128l_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *c, - size_t clen) ; -int aegis128l_state_decrypt_detached_final(aegis128l_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *mac, - size_t maclen) ; +int aegis128l_state_encrypt_update(aegis128l_state *st_, uint8_t *c, const uint8_t *m, size_t mlen); +int aegis128l_state_encrypt_final(aegis128l_state *st_, uint8_t *mac, size_t maclen); +int aegis128l_state_decrypt_update(aegis128l_state *st_, uint8_t *m, const uint8_t *c, size_t clen) ; +int aegis128l_state_decrypt_final(aegis128l_state *st_, const uint8_t *mac, size_t maclen) ; void aegis128l_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis128l_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, @@ -151,33 +126,14 @@ void aegis128x2_state_init(aegis128x2_state *st_, const uint8_t *k); int aegis128x2_state_encrypt_update(aegis128x2_state *st_, uint8_t *c, - size_t clen_max, - size_t *written, const uint8_t *m, size_t mlen); -int aegis128x2_state_encrypt_detached_final(aegis128x2_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - uint8_t *mac, - size_t maclen); -int aegis128x2_state_encrypt_final(aegis128x2_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - size_t maclen); -int aegis128x2_state_decrypt_detached_update(aegis128x2_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *c, - size_t clen) ; -int aegis128x2_state_decrypt_detached_final(aegis128x2_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *mac, - size_t maclen) ; +int aegis128x2_state_encrypt_final(aegis128x2_state *st_, uint8_t *mac, size_t maclen); +int aegis128x2_state_decrypt_update(aegis128x2_state *st_, + uint8_t *m, + const uint8_t *c, + size_t clen) ; +int aegis128x2_state_decrypt_final(aegis128x2_state *st_, const uint8_t *mac, size_t maclen) ; void aegis128x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis128x2_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, @@ -245,33 +201,14 @@ void aegis128x4_state_init(aegis128x4_state *st_, const uint8_t *k); int aegis128x4_state_encrypt_update(aegis128x4_state *st_, uint8_t *c, - size_t clen_max, - size_t *written, const uint8_t *m, size_t mlen); -int aegis128x4_state_encrypt_detached_final(aegis128x4_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - uint8_t *mac, - size_t maclen); -int aegis128x4_state_encrypt_final(aegis128x4_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - size_t maclen); -int aegis128x4_state_decrypt_detached_update(aegis128x4_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *c, - size_t clen) ; -int aegis128x4_state_decrypt_detached_final(aegis128x4_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *mac, - size_t maclen) ; +int aegis128x4_state_encrypt_final(aegis128x4_state *st_, uint8_t *mac, size_t maclen); +int aegis128x4_state_decrypt_update(aegis128x4_state *st_, + uint8_t *m, + const uint8_t *c, + size_t clen) ; +int aegis128x4_state_decrypt_final(aegis128x4_state *st_, const uint8_t *mac, size_t maclen) ; void aegis128x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis128x4_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, @@ -337,35 +274,10 @@ void aegis256_state_init(aegis256_state *st_, size_t adlen, const uint8_t *npub, const uint8_t *k); -int aegis256_state_encrypt_update(aegis256_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - const uint8_t *m, - size_t mlen); -int aegis256_state_encrypt_detached_final(aegis256_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - uint8_t *mac, - size_t maclen); -int aegis256_state_encrypt_final(aegis256_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - size_t maclen); -int aegis256_state_decrypt_detached_update(aegis256_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *c, - size_t clen) ; -int aegis256_state_decrypt_detached_final(aegis256_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *mac, - size_t maclen) ; +int aegis256_state_encrypt_update(aegis256_state *st_, uint8_t *c, const uint8_t *m, size_t mlen); +int aegis256_state_encrypt_final(aegis256_state *st_, uint8_t *mac, size_t maclen); +int aegis256_state_decrypt_update(aegis256_state *st_, uint8_t *m, const uint8_t *c, size_t clen) ; +int aegis256_state_decrypt_final(aegis256_state *st_, const uint8_t *mac, size_t maclen) ; void aegis256_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis256_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, @@ -433,33 +345,14 @@ void aegis256x2_state_init(aegis256x2_state *st_, const uint8_t *k); int aegis256x2_state_encrypt_update(aegis256x2_state *st_, uint8_t *c, - size_t clen_max, - size_t *written, const uint8_t *m, size_t mlen); -int aegis256x2_state_encrypt_detached_final(aegis256x2_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - uint8_t *mac, - size_t maclen); -int aegis256x2_state_encrypt_final(aegis256x2_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - size_t maclen); -int aegis256x2_state_decrypt_detached_update(aegis256x2_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *c, - size_t clen) ; -int aegis256x2_state_decrypt_detached_final(aegis256x2_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *mac, - size_t maclen) ; +int aegis256x2_state_encrypt_final(aegis256x2_state *st_, uint8_t *mac, size_t maclen); +int aegis256x2_state_decrypt_update(aegis256x2_state *st_, + uint8_t *m, + const uint8_t *c, + size_t clen) ; +int aegis256x2_state_decrypt_final(aegis256x2_state *st_, const uint8_t *mac, size_t maclen) ; void aegis256x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis256x2_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, @@ -527,33 +420,14 @@ void aegis256x4_state_init(aegis256x4_state *st_, const uint8_t *k); int aegis256x4_state_encrypt_update(aegis256x4_state *st_, uint8_t *c, - size_t clen_max, - size_t *written, const uint8_t *m, size_t mlen); -int aegis256x4_state_encrypt_detached_final(aegis256x4_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - uint8_t *mac, - size_t maclen); -int aegis256x4_state_encrypt_final(aegis256x4_state *st_, - uint8_t *c, - size_t clen_max, - size_t *written, - size_t maclen); -int aegis256x4_state_decrypt_detached_update(aegis256x4_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *c, - size_t clen) ; -int aegis256x4_state_decrypt_detached_final(aegis256x4_state *st_, - uint8_t *m, - size_t mlen_max, - size_t *written, - const uint8_t *mac, - size_t maclen) ; +int aegis256x4_state_encrypt_final(aegis256x4_state *st_, uint8_t *mac, size_t maclen); +int aegis256x4_state_decrypt_update(aegis256x4_state *st_, + uint8_t *m, + const uint8_t *c, + size_t clen) ; +int aegis256x4_state_decrypt_final(aegis256x4_state *st_, const uint8_t *mac, size_t maclen) ; void aegis256x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis256x4_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, diff --git a/tools/build_backend.py b/tools/build_backend.py index 08a2928..a8c5b3c 100644 --- a/tools/build_backend.py +++ b/tools/build_backend.py @@ -1,100 +1,68 @@ """Custom build backend that builds libaegis with Zig before building the Python package.""" +import os +import platform import shutil import subprocess import sys from pathlib import Path -from setuptools import build_meta as _orig +from setuptools import build_meta + +__all__ = [ + "build_sdist", + "build_wheel", + "build_editable", + "get_requires_for_build_sdist", + "get_requires_for_build_wheel", + "prepare_metadata_for_build_wheel", +] + +_MACOS_TARGET = "11.0" +_prepared = False -def _check_zig_available(): - """Check if Zig is installed and available.""" +def _prepare(): + """Prepare the build environment and build libaegis.""" + global _prepared + if _prepared: + return + _prepared = True + + # Set macOS deployment target + if sys.platform == "darwin" and "MACOSX_DEPLOYMENT_TARGET" not in os.environ: + os.environ["MACOSX_DEPLOYMENT_TARGET"] = _MACOS_TARGET + + # Check Zig is available if shutil.which("zig") is None: raise RuntimeError( - "\n" + "=" * 70 + "\n" - "ERROR: Zig compiler not found!\n" - "\n" - "Building aeg requires the Zig compiler to build the libaegis\n" - "static library. Please install Zig before building this package.\n" - "\n" - "Installation instructions:\n" - " - Visit: https://ziglang.org/download/\n" - " - Or use a package manager:\n" - " * macOS: brew install zig\n" - " * Linux: See https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager\n" - " * Windows: choco install zig or scoop install zig\n" - "\n" - "After installing Zig, please try building again.\n" + "=" * 70 + "\n" + "Zig compiler not found. Install from https://ziglang.org/download/" ) - -def _build_libaegis(): - """Build libaegis static library with Zig.""" - # Check Zig availability first - _check_zig_available() - + # Build libaegis libaegis_dir = Path(__file__).parent.parent / "libaegis" - if not libaegis_dir.exists(): - raise FileNotFoundError( - f"libaegis directory not found at {libaegis_dir}. " - "Cannot build static library." - ) - - print("Building libaegis static library with Zig...") - try: - subprocess.run( - ["zig", "build", "-Drelease"], - cwd=libaegis_dir, - check=True, - capture_output=False, - ) - print("Successfully built libaegis static library") - except subprocess.CalledProcessError as e: - print( - f"\nError: Zig build failed with exit code {e.returncode}\n" - f"Command: {' '.join(e.cmd)}\n", - file=sys.stderr, - ) - raise + cmd = ["zig", "build", "-Drelease"] + if sys.platform == "darwin": + arch = {"arm64": "aarch64", "x86_64": "x86_64"}.get(platform.machine()) + if arch: + cmd.append(f"-Dtarget={arch}-macos.{_MACOS_TARGET}") + subprocess.run(cmd, cwd=libaegis_dir, check=True) -# Expose all the standard build backend hooks -def get_requires_for_build_wheel(config_settings=None): - """Return build requirements and ensure libaegis is built first.""" - _build_libaegis() - return _orig.get_requires_for_build_wheel(config_settings) - - -def get_requires_for_build_sdist(config_settings=None): - """Return build requirements for sdist and ensure libaegis is built first.""" - _build_libaegis() - return _orig.get_requires_for_build_sdist(config_settings) - - -_orig_prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel -_orig_build_sdist = _orig.build_sdist - - -def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - """Prepare metadata and ensure libaegis is built (some frontends call this early).""" - _build_libaegis() - return _orig_prepare_metadata_for_build_wheel(metadata_directory, config_settings) - - -def build_sdist(sdist_directory, config_settings=None): - """Build sdist, building libaegis first so the sdist can include built artifacts if needed.""" - _build_libaegis() - return _orig_build_sdist(sdist_directory, config_settings) +build_sdist = build_meta.build_sdist +get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist +get_requires_for_build_wheel = build_meta.get_requires_for_build_wheel +prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel +# Wheel build hooks - need libaegis built first def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): - """Build wheel with libaegis built first.""" - _build_libaegis() - return _orig.build_wheel(wheel_directory, config_settings, metadata_directory) + _prepare() + return build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) def build_editable(wheel_directory, config_settings=None, metadata_directory=None): - """Build editable install with libaegis built first.""" - _build_libaegis() - return _orig.build_editable(wheel_directory, config_settings, metadata_directory) + _prepare() + return build_meta.build_editable( + wheel_directory, config_settings, metadata_directory + ) diff --git a/tools/release.py b/tools/release.py index 4e91633..ad076fc 100755 --- a/tools/release.py +++ b/tools/release.py @@ -1,6 +1,7 @@ #!/usr/bin/env -S uv run """Build wheels for all supported Python versions using uv.""" +import os import platform import shutil import subprocess @@ -13,19 +14,34 @@ from packaging.version import Version sys.path.insert(0, str(Path(__file__).parent)) import generate -PYTHON_VERSIONS = [ +# Minimum macOS deployment target for compatibility +MACOS_DEPLOYMENT_TARGET = "11.0" + +# ABI3 wheel: built once, works for all GIL-enabled Python versions +# We use a recent Python to build since it doesn't affect the wheel compatibility +ABI3_BUILD_VERSION = "3.14+gil" + +# All GIL-enabled Python versions covered by the ABI3 wheel +ABI3_COVERED_VERSIONS = [ "3.10", "3.11", "3.12", - "3.13", - "3.14", + "3.13+gil", + "3.14+gil", + "3.15+gil", +] + +# Non-ABI3 wheels: each needs its own build (free-threaded and PyPy) +NON_ABI3_VERSIONS = [ "3.14t", - "3.15", "3.15t", "pypy3.10", "pypy3.11", ] +# All versions for testing and benchmarking +ALL_PYTHON_VERSIONS = ABI3_COVERED_VERSIONS + NON_ABI3_VERSIONS + def get_version_from_scm(): """Get version from setuptools-scm (git tags).""" @@ -94,21 +110,29 @@ def make_release_message(version): return msg -def run_command(cmd, description): - """Run a command and handle errors.""" - print(f"\n{'=' * 70}") - print(f"{description}") - print(f"{'=' * 70}") +def run_command(cmd, description=None, env=None): + """Run a command and handle errors. If description is None, only print the command.""" + if description: + print(f"\n{'=' * 70}") + print(f"{description}") + print(f"{'=' * 70}") print(f">>> {' '.join(cmd)}") try: - subprocess.run(cmd, check=True) - print(f"✓ {description} completed successfully") + subprocess.run(cmd, check=True, env=env) return True except subprocess.CalledProcessError as e: - print(f"✗ {description} failed with exit code {e.returncode}", file=sys.stderr) + print(f"✗ Command failed with exit code {e.returncode}", file=sys.stderr) return False +def get_build_env(): + """Get environment variables for building wheels.""" + env = os.environ.copy() + if platform.system() == "Darwin": + env["MACOSX_DEPLOYMENT_TARGET"] = MACOS_DEPLOYMENT_TARGET + return env + + def normalize_line_endings(repo_root: Path): """Normalize all text files to LF line endings.""" # Patterns for files to normalize @@ -132,6 +156,160 @@ def normalize_line_endings(repo_root: Path): file_path.write_bytes(content) +def get_wheel_pattern(py_version: str, abi3: bool = False) -> str: + """Get the glob pattern for finding a wheel file.""" + if abi3: + # ABI3 wheels always use cp310-abi3 tag (minimum supported version) + # regardless of which Python version was used to build + return "aeg-*-cp310-abi3-*.whl" + elif py_version.startswith("pypy"): + # PyPy wheels use pp3XX format + return f"aeg-*-pp{py_version.replace('pypy', '').replace('.', '')}-*.whl" + elif py_version.endswith("t"): + # Free-threaded Python wheels use cpXXX-cpXXXt format (e.g., cp314-cp314t) + base_version = py_version.replace(".", "").replace("t", "") + return f"aeg-*-cp{base_version}-cp{base_version}t-*.whl" + else: + # Regular CPython wheels use cpXXX-cpXXX format + # Strip +gil suffix used to force non-free-threaded build + base_version = py_version.replace(".", "").replace("+gil", "") + return f"aeg-*-cp{base_version}-cp{base_version}-*.whl" + + +def build_abi3_wheel(dist_dir: Path, py_version: str) -> Path | None: + """Build the ABI3 wheel using the specified Python version.""" + cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"] + + if not run_command(cmd, env=get_build_env()): + return None + + # Find the ABI3 wheel (always tagged cp310-abi3 regardless of build Python version) + wheel_pattern = get_wheel_pattern(py_version, abi3=True) + wheels = list(dist_dir.glob(wheel_pattern)) + if not wheels: + print(f"✗ Could not find ABI3 wheel matching {wheel_pattern}", file=sys.stderr) + return None + + wheel = wheels[0] + + # Repair wheel with auditwheel for manylinux compatibility (Linux only) + if platform.system() == "Linux": + wheel = repair_wheel_linux(dist_dir, wheel, py_version, abi3=True) + if not wheel: + return None + + return wheel + + +def build_wheel_for_version(dist_dir: Path, py_version: str) -> Path | None: + """Build a wheel for a specific Python version (non-ABI3).""" + cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"] + + if not run_command(cmd, env=get_build_env()): + return None + + # Find the wheel for this version + wheel_pattern = get_wheel_pattern(py_version, abi3=False) + wheels = list(dist_dir.glob(wheel_pattern)) + if not wheels: + print(f"✗ Could not find wheel for Python {py_version}", file=sys.stderr) + return None + + wheel = wheels[0] + + # Repair wheel with auditwheel for manylinux compatibility (Linux only) + if platform.system() == "Linux": + wheel = repair_wheel_linux(dist_dir, wheel, py_version, abi3=False) + if not wheel: + return None + + return wheel + + +def repair_wheel_linux( + dist_dir: Path, wheel: Path, py_version: str, abi3: bool +) -> Path | None: + """Repair a wheel with auditwheel for manylinux compatibility (Linux only).""" + repair_cmd = [ + "uv", + "run", + "auditwheel", + "repair", + str(wheel), + "-w", + str(dist_dir), + ] + if not run_command(repair_cmd): + return None + + # Find the repaired wheel (it will have a different name) + wheel_pattern = get_wheel_pattern(py_version, abi3=abi3) + all_wheels = list(dist_dir.glob(wheel_pattern)) + repaired_wheels = [w for w in all_wheels if "linux_x86_64" not in str(w)] + if not repaired_wheels: + print( + f"✗ Could not find repaired (manylinux) wheel for Python {py_version}", + file=sys.stderr, + ) + return None + + repaired_wheel = repaired_wheels[0] + + # Remove the unrepaired linux_x86_64 wheels + for w in all_wheels: + if "linux_x86_64" in str(w): + w.unlink() + + return repaired_wheel + + +def test_wheel(wheel: Path, py_version: str) -> bool: + """Test a wheel with pytest.""" + # --isolated: avoid .venv conflicts + # --no-project: don't build from source in current directory, use the wheel + # --refresh-package: force uv to not use cached old versions + test_cmd = [ + "uv", + "run", + "--isolated", + "--no-project", + "--refresh-package", + "aeg", + "--python", + py_version, + "--with", + str(wheel), + "--with", + "pytest", + "pytest", + "tests/", + ] + return run_command(test_cmd) + + +def run_benchmark(wheel: Path, py_version: str) -> bool: + """Run benchmark for a wheel.""" + # --isolated: avoid .venv conflicts + # --no-project: don't build from source in current directory, use the wheel + # --refresh-package: force uv to not use cached old versions + bench_cmd = [ + "uv", + "run", + "--isolated", + "--no-project", + "--refresh-package", + "aeg", + "--python", + py_version, + "--with", + str(wheel), + "-m", + "aeg.benchmark", + ] + return run_command(bench_cmd) + return True + + def main(): """Build wheels for all supported Python versions.""" repo_root = Path(__file__).parent.parent @@ -146,14 +324,15 @@ def main(): return 1 # Run ruff to check and fix any issues - if not run_command( - ["uv", "run", "ruff", "check", "--fix", "."], "Running ruff check --fix" - ): + print(f"\n{'=' * 70}") + print("Linting and formatting") + print(f"{'=' * 70}") + if not run_command(["uv", "run", "ruff", "check", "--fix", "."]): print("✗ Ruff check failed", file=sys.stderr) return 1 # Run ruff format - if not run_command(["uv", "run", "ruff", "format", "."], "Running ruff format"): + if not run_command(["uv", "run", "ruff", "format", "."]): print("✗ Ruff format failed", file=sys.stderr) return 1 @@ -172,7 +351,11 @@ def main(): f"Packaging aeg-{version}" + (" for release" if is_release else " (not release)") ) - print(f"Building wheels for Python versions: {', '.join(PYTHON_VERSIONS)}") + print(f"Building: 1 ABI3 wheel (for Python {', '.join(ABI3_COVERED_VERSIONS)})") + print( + f" + {len(NON_ABI3_VERSIONS)} non-ABI3 wheels ({', '.join(NON_ABI3_VERSIONS)})" + ) + print(f"Testing/benchmarking: {len(ALL_PYTHON_VERSIONS)} Python versions") print(f"Output directory: {dist_dir}", end=" ") # Clean dist directory @@ -181,149 +364,88 @@ def main(): shutil.rmtree(dist_dir) else: print("(created)") + + # Clean build directory to remove stale CFFI-generated C code and .so files + build_dir = repo_root / "build" + if build_dir.exists(): + print(f"Cleaning build directory: {build_dir}") + shutil.rmtree(build_dir) + + # Build distributions + print(f"\n{'=' * 70}") + print("Building distributions") print(f"{'=' * 70}") # Build source distribution first - if not run_command( - ["uv", "build", "--sdist", "--quiet"], "Building source distribution" - ): + if not run_command(["uv", "build", "--sdist", "--quiet"], env=get_build_env()): print("✗ Source distribution build failed", file=sys.stderr) return 1 failed_builds = [] + failed_tests = [] successful_wheels = [] + wheel_for_version = {} # Map Python version to wheel path - for py_version in PYTHON_VERSIONS: - # Build wheel - description = f"Building wheel for Python {py_version}" - cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"] + # Build ABI3 wheel (once, works for all GIL-enabled versions) + abi3_wheel = build_abi3_wheel(dist_dir, ABI3_BUILD_VERSION) + if abi3_wheel: + successful_wheels.append(abi3_wheel) + # This wheel works for all ABI3-covered versions + for py_version in ABI3_COVERED_VERSIONS: + wheel_for_version[py_version] = abi3_wheel + else: + failed_builds.append(f"abi3 (built with {ABI3_BUILD_VERSION})") - if not run_command(cmd, description): - failed_builds.append(py_version) - continue - - # Find the wheel for this version - if py_version.startswith("pypy"): - # PyPy wheels use pp3XX format - wheel_pattern = ( - f"aeg-*-pp{py_version.replace('pypy', '').replace('.', '')}-*.whl" - ) - elif py_version.endswith("t"): - # Free-threaded Python wheels use cpXXX-cpXXXt format (e.g., cp314-cp314t) - base_version = py_version.replace(".", "").replace("t", "") - wheel_pattern = f"aeg-*-cp{base_version}-cp{base_version}t-*.whl" + # Build non-ABI3 wheels (free-threaded and PyPy) + for py_version in NON_ABI3_VERSIONS: + wheel = build_wheel_for_version(dist_dir, py_version) + if wheel: + successful_wheels.append(wheel) + wheel_for_version[py_version] = wheel else: - # Regular CPython wheels use cpXXX-cpXXX format - base_version = py_version.replace(".", "") - wheel_pattern = f"aeg-*-cp{base_version}-cp{base_version}-*.whl" - wheels = list(dist_dir.glob(wheel_pattern)) - if not wheels: - print(f"✗ Could not find wheel for Python {py_version}", file=sys.stderr) failed_builds.append(py_version) + + # Test and benchmark each Python version with its appropriate wheel + print(f"\n{'=' * 70}") + print("Testing and benchmarking") + print(f"{'=' * 70}") + + for py_version in ALL_PYTHON_VERSIONS: + wheel = wheel_for_version.get(py_version) + if not wheel: + # No wheel available for this version (build failed) continue - wheel = wheels[0] - - # Repair wheel with auditwheel for manylinux compatibility (Linux only) - if platform.system() == "Linux": - repair_cmd = [ - "uv", - "run", - "auditwheel", - "repair", - str(wheel), - "-w", - str(dist_dir), - ] - if not run_command( - repair_cmd, f"Repairing wheel for Python {py_version} with auditwheel" - ): - print( - f"✗ Auditwheel repair failed for Python {py_version}", - file=sys.stderr, - ) - failed_builds.append(py_version) - continue - - # Find the repaired wheel (it will have a different name) - # Use same pattern logic as above for free-threaded vs regular builds - if py_version.startswith("pypy"): - repair_pattern = ( - f"aeg-*-pp{py_version.replace('pypy', '').replace('.', '')}-*.whl" - ) - elif py_version.endswith("t"): - base_version = py_version.replace(".", "").replace("t", "") - repair_pattern = f"aeg-*-cp{base_version}-cp{base_version}t-*.whl" - else: - base_version = py_version.replace(".", "") - repair_pattern = f"aeg-*-cp{base_version}-cp{base_version}-*.whl" - all_wheels = list(dist_dir.glob(repair_pattern)) - repaired_wheels = [w for w in all_wheels if "linux_x86_64" not in str(w)] - if not repaired_wheels: - print( - f"✗ Could not find repaired (manylinux) wheel for Python {py_version}", - file=sys.stderr, - ) - failed_builds.append(py_version) - continue - - wheel = repaired_wheels[0] # Use the repaired wheel for testing - - # Remove the unrepaired linux_x86_64 wheels - for w in all_wheels: - if "linux_x86_64" in str(w): - w.unlink() - - # Test the wheel with pytest (use --isolated to avoid .venv conflicts) - test_cmd = [ - "uv", - "run", - "--isolated", - "--python", - py_version, - "--with", - str(wheel), - "--with", - "pytest", - "pytest", - ] - if not run_command( - test_cmd, f"Testing wheel for Python {py_version} with pytest" - ): - print(f"✗ Tests failed for Python {py_version}", file=sys.stderr) - failed_builds.append(py_version) + # Test the wheel with pytest + if not test_wheel(wheel, py_version): + failed_tests.append(py_version) continue - # Run benchmark (use --isolated to avoid .venv conflicts) - bench_cmd = [ - "uv", - "run", - "--isolated", - "--python", - py_version, - "--with", - str(wheel), - "-m", - "aeg.benchmark", - ] - if not run_command(bench_cmd, f"Running benchmark for Python {py_version}"): - print(f"✗ Benchmark failed for Python {py_version}", file=sys.stderr) - failed_builds.append(py_version) + # Run benchmark + if not run_benchmark(wheel, py_version): + failed_tests.append(py_version) continue - successful_wheels.append(wheel) - # Summary print(f"\n{'=' * 70}") print("BUILD SUMMARY") print(f"{'=' * 70}") print( - f"Successful builds: sdist and {len(successful_wheels)}/{len(PYTHON_VERSIONS)} wheels" + f"Successful builds: sdist and {len(successful_wheels)} wheels " + f"(1 abi3 + {len(NON_ABI3_VERSIONS)} non-abi3)" + ) + print( + f"Tests/benchmarks passed: {len(ALL_PYTHON_VERSIONS) - len(failed_tests) - len(failed_builds)}/{len(ALL_PYTHON_VERSIONS)} Python versions" ) if failed_builds: print(f"\nFailed builds: {len(failed_builds)}") for failed_version in failed_builds: + print(f" ✗ {failed_version}") + + if failed_tests: + print(f"\nFailed tests/benchmarks: {len(failed_tests)}") + for failed_version in failed_tests: print(f" ✗ Python {failed_version}") if not successful_wheels: @@ -356,4 +478,7 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(1)