64 Commits
Author SHA1 Message Date
Leo Vasanko 5a99a52af9 [packaging] Fix wheel curing for pypy-linux. 2025-12-31 21:59:37 +00:00
Leo Vasanko 1ecf0d306f Make tool scripts executable. 2025-12-31 21:43:25 +00:00
Leo Vasanko 5824df28cb Add aeg.cipher(alg) for loading modules by name in str. Added aeg.CIPHERS mapping of the nominal names to module names and Cipher for typing literals. 2025-12-31 21:42:29 +00:00
Leo Vasanko 938239ef72 Added free-threaded non-GIL Python and PyPy wheels, up to 3.15 now. 2025-12-31 21:24:56 +00:00
Leo Vasanko f3c0b2b85d [release script] Auditwheel only on Linux. 2025-12-23 19:46:13 +00:00
Leo Vasanko dd0c0bc0a0 Add README for PyPI. 2025-12-23 19:33:00 +00:00
Leo Vasanko 40560d2deb Make manylinux wheels. 2025-12-23 19:32:03 +00:00
Leo Vasanko 6fc3098650 Clean up for release. 2025-12-23 19:09:59 +00:00
Leo Vasanko ff97bf42a8 Clean up for release. 2025-12-23 19:08:10 +00:00
Leo Vasanko ba2179e827 Clean up for release. 2025-12-23 18:54:03 +00:00
Leo Vasanko 640e812908 Project renamed to aeg, preparing for PyPI release. 2025-12-23 18:25:03 +00:00
Leo Vasanko a644d27b56 Add tools/release.py script for automatic release process. 2025-11-12 11:47:26 -06:00
Leo Vasanko 4445c256b1 Always generate LF line endings (even on broken OS). 2025-11-12 11:43:25 -06:00
Leo Vasanko 04f1c3067c Ruff formatting. 2025-11-12 11:43:04 -06:00
Leo Vasanko 39e5f00f42 Fixed Windows build, cleaned up messages during build, using SCM version numbering, added ruff. 2025-11-12 10:59:21 -06:00
Leo Vasanko 88efc4cabc Update pyproject, bump version. 2025-11-09 20:41:40 -06:00
Leo Vasanko 04b11e9925 README tuning. New benchmark results (a bit slower than initial versions were). 2025-11-09 20:40:04 -06:00
Leo Vasanko d8a9a7ee9d Use a much faster method to wipe buffers. 2025-11-09 20:38:28 -06:00
Leo Vasanko f5430a6ad4 Cleanup. 2025-11-09 20:00:12 -06:00
Leo Vasanko f84ef727d3 Bump version 2025-11-09 09:47:08 -06:00
Leo Vasanko bb9d11842a Convert all input buffers to memoryview before use and use .nbytes, because len() doesn't work correctly with some buffers. Update docs with a Numpy example. 2025-11-09 09:46:21 -06:00
Leo Vasanko 20e0ed8c5f Cleanup benchmark 2025-11-09 08:59:53 -06:00
Leo Vasanko 62fc8fa855 Make Mac class prevent further updates or final after finalisation. Keep cached values for hashlib API. 2025-11-09 08:53:50 -06:00
Leo Vasanko 67c2958384 Cleanup 2025-11-08 20:39:41 -06:00
Leo Vasanko a6faaf9f62 Simplify implementation: remove bytes_in and bytes_out counters from all classes. 2025-11-08 20:09:26 -06:00
Leo Vasanko 75cbc76845 Wipe state structs automatically after use. Simplified aligned allocator and its use via a single handle. 2025-11-08 20:04:39 -06:00
Leo Vasanko 95563a43d1 API updates:
- Mac class follows hashlib API: digest functions added and finalization no longer modifies state.
- Encryptor and Decryptor now raise RuntimeError if still used after final.

Documentation updated with the changes and  further examples.

Tests updated with the changes, new test module for error cases (test_raises).

Docstrings improved.
2025-11-08 18:53:27 -06:00
Leo Vasanko e58990a1c2 Add human-readable algorithm name as NAME constant. 2025-11-08 16:01:05 -06:00
Leo Vasanko 13445887e9 Constants renamed and values extracted from C code rather than function call at runtime. Documentation update. 2025-11-08 15:43:01 -06:00
Leo Vasanko 751a929836 Combine the two generator scripts into one that also reads ALIGNMENT and RATE from C sources. 2025-11-08 13:20:15 -06:00
Leo Vasanko d4f8be69ed Bump version 2025-11-07 12:51:40 -06:00
Leo Vasanko 77601d7f57 README 2025-11-07 10:30:23 -06:00
Leo Vasanko 4356e57ace Move the build_backend script to tools folder. 2025-11-07 10:26:03 -06:00
Leo Vasanko 5e19bd980e Documentation updates, added BUILD.md 2025-11-07 10:20:27 -06:00
Leo Vasanko 63ccef577d Use uv in the commands. 2025-11-07 10:00:36 -06:00
Leo Vasanko 555bbcf2a5 README formatting 2025-11-07 09:59:20 -06:00
Leo Vasanko 1dcdafa008 util.nonce_increment and util.wipe functions, also exported by each module for convenience. 2025-11-07 09:53:39 -06:00
Leo Vasanko 96ce7867de README updated 2025-11-07 09:52:28 -06:00
Leo Vasanko b15174af8b Make random_key and random_nonce return bytearray, and add a nonce_increment utility function. 2025-11-07 07:43:33 -06:00
Leo Vasanko 02eb4d7718 README 2025-11-06 21:11:38 -06:00
Leo Vasanko 1b4d43a448 Do not return True from MAC verify function on success. 2025-11-06 21:11:24 -06:00
Leo Vasanko a8947c23b3 Add random_key and random_nonce functions to each submodule. Update docstrings to show the correct lengths for keys and nonces. 2025-11-06 20:03:56 -06:00
Leo Vasanko 2677df3bde Remove examples folder, keep benchmark as pyaegis.benchmark that becomes part of the install. 2025-11-06 19:55:51 -06:00
Leo Vasanko 7175654b27 API cleanup for simplified update/final. Returns bytearrays when into is not used. Allows into buffers larger than needed. Misc other changes. 2025-11-06 19:38:18 -06:00
Leo Vasanko 46dff56e28 Cleanup. 2025-11-06 19:36:43 -06:00
Leo Vasanko fc76bc4280 More informative build error message when Zig is not installed. 2025-11-06 17:04:03 -06:00
Leo Vasanko 9f2b931a0b Another approach to use zig build with setuptools. 2025-11-06 16:56:39 -06:00
Leo Vasanko fbf9c944e6 Include zig build in the build process. 2025-11-06 16:43:54 -06:00
Leo Vasanko f8cc02eb41 Switch from hatch to setuptools/CFFI build to produce wheels correctly. 2025-11-06 16:34:32 -06:00
Leo Vasanko fd24bb02f8 Test cleanup, remove debug. 2025-11-06 16:33:51 -06:00
Leo Vasanko 285f11299e Remove Encryptor.final_detached(), as final() handles both cases identically (only returns MAC). 2025-11-06 16:33:04 -06:00
Leo Vasanko 3248fccbac The update functions of libaegis now return the same number of bytes that went in. Update the binding accordingly. 2025-11-06 16:27:48 -06:00
Leo Vasanko 17a5f45394 Add comments to generated files saying how they are generated. 2025-11-06 15:31:29 -06:00
Leo Vasanko d43a22bc6c Build cleanup, pathname reorganisation, cdef to repository (although generated with gen_cdef.py). 2025-11-06 15:23:30 -06:00
Leo Vasanko c8fe16d21f Add .gitignore 2025-11-06 10:52:23 -06:00
Leo Vasanko ea066e101d Add libaegis submodule 2025-11-06 10:51:32 -06:00
Leo Vasanko 42ddaac6bc Rename module to pyaegis, implement build with zig. 2025-11-06 10:47:54 -06:00
Leo Vasanko 8898cec50f Add pytest dev dependency. 2025-11-06 08:20:23 -06:00
Leo Vasanko 3579b94e83 Add tests for all functions of each algorithm against AEGIS test vectors (JSON). Incremental updates are tested with randomized splits to catch a variety of errors. 2025-11-06 07:56:12 -06:00
Leo Vasanko bcf4655f64 Precise update output length calculation to allocate exactly sized buffers. 2025-11-06 07:45:15 -06:00
Leo Vasanko 438627e0db Add helper function for calculating the incremental update output buffer size. Add bytes_in and bytes_out counters on the incremental classes. Avoid ERANGE errors caused by too small output buffer. 2025-11-05 15:13:46 -06:00
Leo Vasanko 02310675b7 Make key, nonce the first arguments (in this order), require kwargs past the initial few positional arguments. Add ALIGNMENT constant to each module. Add a script to generate all other modules from aegis256x4.py. 2025-11-04 21:02:29 -06:00
Leo Vasanko 6ceb2971fa Cleanup 2025-11-04 18:40:22 -06:00
Leo Vasanko 7541d9d837 Initial commit 2025-11-04 18:14:07 -06:00
15 changed files with 565 additions and 420 deletions
+13 -21
View File
@@ -17,6 +17,19 @@ Or add to your project using [UV](https://docs.astral.sh/uv/getting-started/inst
uv add aeg uv add aeg
``` ```
## Variants
All submodules expose the same API; pick one for your needs. The 256 bit variants offer maximal security and use larger key and nonce, while the 128 bit variants run slightly faster and use smaller key and nonce while still providing strong security. The MAC length does not depend on the variant. Note that the x2 and x4 variants are typically the fastest (depending on CPU) by utilizing SIMD multi-lane processing for the highest throughput.
| Variant | Key/Nonce Bytes | Notes |
|----------------|----------------:|-------------------------|
| **aegis128l** | 16 | |
| **aegis128x2** | 16 | Fastest on Intel Core |
| **aegis128x4** | 16 | Fastest on AMD and Xeon |
| **aegis256** | 32 | |
| **aegis256x2** | 32 | Fast on Intel Core |
| **aegis256x4** | 32 | Fast on AMD and Xeon |
## Quick start ## Quick start
Normal authenticated encryption using the AEGIS-128X4 algorithm: Normal authenticated encryption using the AEGIS-128X4 algorithm:
@@ -33,27 +46,6 @@ pt = ciph.decrypt(key, nonce, ct) # Raises ValueError if anything was tampered
assert pt == msg assert pt == msg
``` ```
## Variants
All submodules expose the same API; pick one for your needs. The 256 bit variants offer maximal security and use larger key and nonce, while the 128 bit variants run slightly faster and use smaller key and nonce while still providing strong security. The MAC length does not depend on the variant. Note that the x2 and x4 variants are typically the fastest (depending on CPU) by utilizing SIMD multi-lane processing for the highest throughput.
| Variant | Key/Nonce Bytes | Notes |
|----------------|----------------:|-------------------------|
| **aegis128l** | 16 | |
| **aegis128x2** | 16 | Fastest on Intel Core |
| **aegis128x4** | 16 | Fastest on AMD and Xeon |
| **aegis256** | 32 | |
| **aegis256x2** | 32 | Fast on Intel Core |
| **aegis256x4** | 32 | Fast on AMD and Xeon |
Instead of importing the submodules, you can obtain one by its name string:
```python
import aeg
ciph = aeg.cipher("AEGIS-128X2") # Also accepts "aegis128x2" and other forms
```
## API overview ## API overview
Common parameters and returns (applies to all items below): Common parameters and returns (applies to all items below):
+1 -2
View File
@@ -10,7 +10,6 @@ description = "AEGIS encryption easy to use Python binding. Wheels for major pla
readme = {file = "README.md", content-type = "text/markdown"} readme = {file = "README.md", content-type = "text/markdown"}
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Topic :: Security :: Cryptography", "Topic :: Security :: Cryptography",
@@ -37,6 +36,6 @@ package-dir = {"" = "src"}
packages = ["aeg"] packages = ["aeg"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
aeg = ["*.h"] aeg = ["*.h", "*.so", "*.pyd"]
[tool.setuptools_scm] [tool.setuptools_scm]
+22 -15
View File
@@ -1,24 +1,35 @@
"""Setup script for aeg - builds CFFI extension with libaegis C library.""" """Setup script for aeg - builds CFFI extension with libaegis C library."""
import sys import sys
import sysconfig
from pathlib import Path from pathlib import Path
from cffi import FFI from cffi import FFI
from setuptools import setup from setuptools import setup
libaegis_static = Path("libaegis/zig-out/lib") / ( # Locate the static library (built by build_backend.py before this runs)
"aegis.lib" if sys.platform == "win32" else "libaegis.a" 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())
# 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 = FFI()
ffibuilder.cdef((Path(__file__).parent / "src/aeg/aegis_cdef.h").read_text()) ffibuilder.cdef(cdef_content)
# 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( ffibuilder.set_source(
"aeg._aegis", "aeg._aegis", # module name
""" """
#include "aegis.h" #include "aegis.h"
#include "aegis128l.h" #include "aegis128l.h"
@@ -28,15 +39,11 @@ ffibuilder.set_source(
#include "aegis256x2.h" #include "aegis256x2.h"
#include "aegis256x4.h" #include "aegis256x4.h"
""", """,
include_dirs=["libaegis/src/include"], include_dirs=include_dirs,
extra_objects=[str(libaegis_static.resolve())], extra_objects=[libaegis_static],
py_limited_api=not is_free_threaded,
) )
if __name__ == "__main__": if __name__ == "__main__":
setup( setup(
cffi_modules=["setup.py:ffibuilder"], cffi_modules=["setup.py:ffibuilder"],
options=(
{"bdist_wheel": {"py_limited_api": "cp310"}} if not is_free_threaded else {}
),
) )
-2
View File
@@ -11,8 +11,6 @@ def cipher(alg: CipherName) -> Cipher:
name = alg.lower().replace("-", "") name = alg.lower().replace("-", "")
if name == "aegis128": if name == "aegis128":
name = "aegis128l" # AEGIS-128 is dead, the user meant AEGIS-128L name = "aegis128l" # AEGIS-128 is dead, the user meant AEGIS-128L
if not name.startswith("aegis"):
name = "aegis" + name
if name in CIPHERS.values(): if name in CIPHERS.values():
return importlib.import_module(f".{name}", __package__) # type: ignore[return-value] return importlib.import_module(f".{name}", __package__) # type: ignore[return-value]
raise ValueError(f"Unknown algorithm {alg!r}. Valid options: {', '.join(CIPHERS)}") raise ValueError(f"Unknown algorithm {alg!r}. Valid options: {', '.join(CIPHERS)}")
-4
View File
@@ -120,7 +120,3 @@ class Cipher(Protocol):
maclen: int = ..., maclen: int = ...,
into: "Buffer | None" = None, into: "Buffer | None" = None,
) -> bytearray | memoryview: ... ) -> bytearray | memoryview: ...
@staticmethod
def nonce_increment(nonce: "Buffer") -> None: ...
@staticmethod
def wipe(buffer: "Buffer") -> None: ...
+29 -6
View File
@@ -708,17 +708,24 @@ class Encryptor:
raise TypeError( raise TypeError(
"into length must be >= expected output size for this update" "into length must be >= expected output size for this update"
) )
written = ffi.new("size_t *")
rc = _lib.aegis128l_state_encrypt_update( rc = _lib.aegis128l_state_encrypt_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(message), _ptr(message),
message.nbytes, message.nbytes,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt update failed: {err_name}") raise RuntimeError(
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, into: Buffer | None = None) -> bytearray | memoryview: def final(self, into: Buffer | None = None) -> bytearray | memoryview:
"""Finalize encryption and return the authentication tag. """Finalize encryption and return the authentication tag.
@@ -739,17 +746,24 @@ class Encryptor:
if into is not None: if into is not None:
into = memoryview(into) into = memoryview(into)
out = into if into is not None else bytearray(maclen) out = into if into is not None else bytearray(maclen)
written = ffi.new("size_t *")
rc = _lib.aegis128l_state_encrypt_final( rc = _lib.aegis128l_state_encrypt_final(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out), ffi.from_buffer(out),
memoryview(out).nbytes,
written,
maclen, maclen,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt final failed: {err_name}") 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 self._state = None
return out if into is None else memoryview(out)[:maclen] # type: ignore return out if into is None else memoryview(out)[:w] # type: ignore
class Decryptor: class Decryptor:
@@ -823,9 +837,12 @@ class Decryptor:
out_mv = memoryview(out) out_mv = memoryview(out)
if out_mv.nbytes < expected_out: if out_mv.nbytes < expected_out:
raise TypeError("into length must be >= required capacity for this update") raise TypeError("into length must be >= required capacity for this update")
rc = _lib.aegis128l_state_decrypt_update( written = ffi.new("size_t *")
rc = _lib.aegis128l_state_decrypt_detached_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(ct), _ptr(ct),
ct.nbytes, ct.nbytes,
) )
@@ -833,7 +850,11 @@ class Decryptor:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}") raise RuntimeError(f"state decrypt update failed: {err_name}")
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, mac: Buffer) -> None: def final(self, mac: Buffer) -> None:
"""Finalize decryption by verifying the MAC tag. """Finalize decryption by verifying the MAC tag.
@@ -852,7 +873,9 @@ class Decryptor:
mac = memoryview(mac) mac = memoryview(mac)
if mac.nbytes != maclen: if mac.nbytes != maclen:
raise TypeError(f"mac length must be {maclen}") raise TypeError(f"mac length must be {maclen}")
rc = _lib.aegis128l_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) rc = _lib.aegis128l_state_decrypt_detached_final(
self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
)
if rc != 0: if rc != 0:
raise ValueError("authentication failed") raise ValueError("authentication failed")
self._state = None self._state = None
+29 -6
View File
@@ -708,17 +708,24 @@ class Encryptor:
raise TypeError( raise TypeError(
"into length must be >= expected output size for this update" "into length must be >= expected output size for this update"
) )
written = ffi.new("size_t *")
rc = _lib.aegis128x2_state_encrypt_update( rc = _lib.aegis128x2_state_encrypt_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(message), _ptr(message),
message.nbytes, message.nbytes,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt update failed: {err_name}") raise RuntimeError(
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, into: Buffer | None = None) -> bytearray | memoryview: def final(self, into: Buffer | None = None) -> bytearray | memoryview:
"""Finalize encryption and return the authentication tag. """Finalize encryption and return the authentication tag.
@@ -739,17 +746,24 @@ class Encryptor:
if into is not None: if into is not None:
into = memoryview(into) into = memoryview(into)
out = into if into is not None else bytearray(maclen) out = into if into is not None else bytearray(maclen)
written = ffi.new("size_t *")
rc = _lib.aegis128x2_state_encrypt_final( rc = _lib.aegis128x2_state_encrypt_final(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out), ffi.from_buffer(out),
memoryview(out).nbytes,
written,
maclen, maclen,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt final failed: {err_name}") 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 self._state = None
return out if into is None else memoryview(out)[:maclen] # type: ignore return out if into is None else memoryview(out)[:w] # type: ignore
class Decryptor: class Decryptor:
@@ -823,9 +837,12 @@ class Decryptor:
out_mv = memoryview(out) out_mv = memoryview(out)
if out_mv.nbytes < expected_out: if out_mv.nbytes < expected_out:
raise TypeError("into length must be >= required capacity for this update") raise TypeError("into length must be >= required capacity for this update")
rc = _lib.aegis128x2_state_decrypt_update( written = ffi.new("size_t *")
rc = _lib.aegis128x2_state_decrypt_detached_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(ct), _ptr(ct),
ct.nbytes, ct.nbytes,
) )
@@ -833,7 +850,11 @@ class Decryptor:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}") raise RuntimeError(f"state decrypt update failed: {err_name}")
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, mac: Buffer) -> None: def final(self, mac: Buffer) -> None:
"""Finalize decryption by verifying the MAC tag. """Finalize decryption by verifying the MAC tag.
@@ -852,7 +873,9 @@ class Decryptor:
mac = memoryview(mac) mac = memoryview(mac)
if mac.nbytes != maclen: if mac.nbytes != maclen:
raise TypeError(f"mac length must be {maclen}") raise TypeError(f"mac length must be {maclen}")
rc = _lib.aegis128x2_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) rc = _lib.aegis128x2_state_decrypt_detached_final(
self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
)
if rc != 0: if rc != 0:
raise ValueError("authentication failed") raise ValueError("authentication failed")
self._state = None self._state = None
+29 -6
View File
@@ -708,17 +708,24 @@ class Encryptor:
raise TypeError( raise TypeError(
"into length must be >= expected output size for this update" "into length must be >= expected output size for this update"
) )
written = ffi.new("size_t *")
rc = _lib.aegis128x4_state_encrypt_update( rc = _lib.aegis128x4_state_encrypt_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(message), _ptr(message),
message.nbytes, message.nbytes,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt update failed: {err_name}") raise RuntimeError(
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, into: Buffer | None = None) -> bytearray | memoryview: def final(self, into: Buffer | None = None) -> bytearray | memoryview:
"""Finalize encryption and return the authentication tag. """Finalize encryption and return the authentication tag.
@@ -739,17 +746,24 @@ class Encryptor:
if into is not None: if into is not None:
into = memoryview(into) into = memoryview(into)
out = into if into is not None else bytearray(maclen) out = into if into is not None else bytearray(maclen)
written = ffi.new("size_t *")
rc = _lib.aegis128x4_state_encrypt_final( rc = _lib.aegis128x4_state_encrypt_final(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out), ffi.from_buffer(out),
memoryview(out).nbytes,
written,
maclen, maclen,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt final failed: {err_name}") 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 self._state = None
return out if into is None else memoryview(out)[:maclen] # type: ignore return out if into is None else memoryview(out)[:w] # type: ignore
class Decryptor: class Decryptor:
@@ -823,9 +837,12 @@ class Decryptor:
out_mv = memoryview(out) out_mv = memoryview(out)
if out_mv.nbytes < expected_out: if out_mv.nbytes < expected_out:
raise TypeError("into length must be >= required capacity for this update") raise TypeError("into length must be >= required capacity for this update")
rc = _lib.aegis128x4_state_decrypt_update( written = ffi.new("size_t *")
rc = _lib.aegis128x4_state_decrypt_detached_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(ct), _ptr(ct),
ct.nbytes, ct.nbytes,
) )
@@ -833,7 +850,11 @@ class Decryptor:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}") raise RuntimeError(f"state decrypt update failed: {err_name}")
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, mac: Buffer) -> None: def final(self, mac: Buffer) -> None:
"""Finalize decryption by verifying the MAC tag. """Finalize decryption by verifying the MAC tag.
@@ -852,7 +873,9 @@ class Decryptor:
mac = memoryview(mac) mac = memoryview(mac)
if mac.nbytes != maclen: if mac.nbytes != maclen:
raise TypeError(f"mac length must be {maclen}") raise TypeError(f"mac length must be {maclen}")
rc = _lib.aegis128x4_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) rc = _lib.aegis128x4_state_decrypt_detached_final(
self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
)
if rc != 0: if rc != 0:
raise ValueError("authentication failed") raise ValueError("authentication failed")
self._state = None self._state = None
+29 -6
View File
@@ -708,17 +708,24 @@ class Encryptor:
raise TypeError( raise TypeError(
"into length must be >= expected output size for this update" "into length must be >= expected output size for this update"
) )
written = ffi.new("size_t *")
rc = _lib.aegis256_state_encrypt_update( rc = _lib.aegis256_state_encrypt_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(message), _ptr(message),
message.nbytes, message.nbytes,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt update failed: {err_name}") raise RuntimeError(
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, into: Buffer | None = None) -> bytearray | memoryview: def final(self, into: Buffer | None = None) -> bytearray | memoryview:
"""Finalize encryption and return the authentication tag. """Finalize encryption and return the authentication tag.
@@ -739,17 +746,24 @@ class Encryptor:
if into is not None: if into is not None:
into = memoryview(into) into = memoryview(into)
out = into if into is not None else bytearray(maclen) out = into if into is not None else bytearray(maclen)
written = ffi.new("size_t *")
rc = _lib.aegis256_state_encrypt_final( rc = _lib.aegis256_state_encrypt_final(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out), ffi.from_buffer(out),
memoryview(out).nbytes,
written,
maclen, maclen,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt final failed: {err_name}") 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 self._state = None
return out if into is None else memoryview(out)[:maclen] # type: ignore return out if into is None else memoryview(out)[:w] # type: ignore
class Decryptor: class Decryptor:
@@ -823,9 +837,12 @@ class Decryptor:
out_mv = memoryview(out) out_mv = memoryview(out)
if out_mv.nbytes < expected_out: if out_mv.nbytes < expected_out:
raise TypeError("into length must be >= required capacity for this update") raise TypeError("into length must be >= required capacity for this update")
rc = _lib.aegis256_state_decrypt_update( written = ffi.new("size_t *")
rc = _lib.aegis256_state_decrypt_detached_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(ct), _ptr(ct),
ct.nbytes, ct.nbytes,
) )
@@ -833,7 +850,11 @@ class Decryptor:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}") raise RuntimeError(f"state decrypt update failed: {err_name}")
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, mac: Buffer) -> None: def final(self, mac: Buffer) -> None:
"""Finalize decryption by verifying the MAC tag. """Finalize decryption by verifying the MAC tag.
@@ -852,7 +873,9 @@ class Decryptor:
mac = memoryview(mac) mac = memoryview(mac)
if mac.nbytes != maclen: if mac.nbytes != maclen:
raise TypeError(f"mac length must be {maclen}") raise TypeError(f"mac length must be {maclen}")
rc = _lib.aegis256_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) rc = _lib.aegis256_state_decrypt_detached_final(
self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
)
if rc != 0: if rc != 0:
raise ValueError("authentication failed") raise ValueError("authentication failed")
self._state = None self._state = None
+29 -6
View File
@@ -708,17 +708,24 @@ class Encryptor:
raise TypeError( raise TypeError(
"into length must be >= expected output size for this update" "into length must be >= expected output size for this update"
) )
written = ffi.new("size_t *")
rc = _lib.aegis256x2_state_encrypt_update( rc = _lib.aegis256x2_state_encrypt_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(message), _ptr(message),
message.nbytes, message.nbytes,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt update failed: {err_name}") raise RuntimeError(
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, into: Buffer | None = None) -> bytearray | memoryview: def final(self, into: Buffer | None = None) -> bytearray | memoryview:
"""Finalize encryption and return the authentication tag. """Finalize encryption and return the authentication tag.
@@ -739,17 +746,24 @@ class Encryptor:
if into is not None: if into is not None:
into = memoryview(into) into = memoryview(into)
out = into if into is not None else bytearray(maclen) out = into if into is not None else bytearray(maclen)
written = ffi.new("size_t *")
rc = _lib.aegis256x2_state_encrypt_final( rc = _lib.aegis256x2_state_encrypt_final(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out), ffi.from_buffer(out),
memoryview(out).nbytes,
written,
maclen, maclen,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt final failed: {err_name}") 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 self._state = None
return out if into is None else memoryview(out)[:maclen] # type: ignore return out if into is None else memoryview(out)[:w] # type: ignore
class Decryptor: class Decryptor:
@@ -823,9 +837,12 @@ class Decryptor:
out_mv = memoryview(out) out_mv = memoryview(out)
if out_mv.nbytes < expected_out: if out_mv.nbytes < expected_out:
raise TypeError("into length must be >= required capacity for this update") raise TypeError("into length must be >= required capacity for this update")
rc = _lib.aegis256x2_state_decrypt_update( written = ffi.new("size_t *")
rc = _lib.aegis256x2_state_decrypt_detached_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(ct), _ptr(ct),
ct.nbytes, ct.nbytes,
) )
@@ -833,7 +850,11 @@ class Decryptor:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}") raise RuntimeError(f"state decrypt update failed: {err_name}")
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, mac: Buffer) -> None: def final(self, mac: Buffer) -> None:
"""Finalize decryption by verifying the MAC tag. """Finalize decryption by verifying the MAC tag.
@@ -852,7 +873,9 @@ class Decryptor:
mac = memoryview(mac) mac = memoryview(mac)
if mac.nbytes != maclen: if mac.nbytes != maclen:
raise TypeError(f"mac length must be {maclen}") raise TypeError(f"mac length must be {maclen}")
rc = _lib.aegis256x2_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) rc = _lib.aegis256x2_state_decrypt_detached_final(
self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
)
if rc != 0: if rc != 0:
raise ValueError("authentication failed") raise ValueError("authentication failed")
self._state = None self._state = None
+29 -6
View File
@@ -708,17 +708,24 @@ class Encryptor:
raise TypeError( raise TypeError(
"into length must be >= expected output size for this update" "into length must be >= expected output size for this update"
) )
written = ffi.new("size_t *")
rc = _lib.aegis256x4_state_encrypt_update( rc = _lib.aegis256x4_state_encrypt_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(message), _ptr(message),
message.nbytes, message.nbytes,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt update failed: {err_name}") raise RuntimeError(
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, into: Buffer | None = None) -> bytearray | memoryview: def final(self, into: Buffer | None = None) -> bytearray | memoryview:
"""Finalize encryption and return the authentication tag. """Finalize encryption and return the authentication tag.
@@ -739,17 +746,24 @@ class Encryptor:
if into is not None: if into is not None:
into = memoryview(into) into = memoryview(into)
out = into if into is not None else bytearray(maclen) out = into if into is not None else bytearray(maclen)
written = ffi.new("size_t *")
rc = _lib.aegis256x4_state_encrypt_final( rc = _lib.aegis256x4_state_encrypt_final(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out), ffi.from_buffer(out),
memoryview(out).nbytes,
written,
maclen, maclen,
) )
if rc != 0: if rc != 0:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state encrypt final failed: {err_name}") 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 self._state = None
return out if into is None else memoryview(out)[:maclen] # type: ignore return out if into is None else memoryview(out)[:w] # type: ignore
class Decryptor: class Decryptor:
@@ -823,9 +837,12 @@ class Decryptor:
out_mv = memoryview(out) out_mv = memoryview(out)
if out_mv.nbytes < expected_out: if out_mv.nbytes < expected_out:
raise TypeError("into length must be >= required capacity for this update") raise TypeError("into length must be >= required capacity for this update")
rc = _lib.aegis256x4_state_decrypt_update( written = ffi.new("size_t *")
rc = _lib.aegis256x4_state_decrypt_detached_update(
self._state.ptr, self._state.ptr,
ffi.from_buffer(out_mv), ffi.from_buffer(out_mv),
out_mv.nbytes,
written,
_ptr(ct), _ptr(ct),
ct.nbytes, ct.nbytes,
) )
@@ -833,7 +850,11 @@ class Decryptor:
err_num = ffi.errno err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}") err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}") raise RuntimeError(f"state decrypt update failed: {err_name}")
return out if into is None else memoryview(out)[:expected_out] # type: ignore 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
def final(self, mac: Buffer) -> None: def final(self, mac: Buffer) -> None:
"""Finalize decryption by verifying the MAC tag. """Finalize decryption by verifying the MAC tag.
@@ -852,7 +873,9 @@ class Decryptor:
mac = memoryview(mac) mac = memoryview(mac)
if mac.nbytes != maclen: if mac.nbytes != maclen:
raise TypeError(f"mac length must be {maclen}") raise TypeError(f"mac length must be {maclen}")
rc = _lib.aegis256x4_state_decrypt_final(self._state.ptr, _ptr(mac), maclen) rc = _lib.aegis256x4_state_decrypt_detached_final(
self._state.ptr, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
)
if rc != 0: if rc != 0:
raise ValueError("authentication failed") raise ValueError("authentication failed")
self._state = None self._state = None
+158 -32
View File
@@ -55,10 +55,35 @@ void aegis128l_state_init(aegis128l_state *st_,
size_t adlen, size_t adlen,
const uint8_t *npub, const uint8_t *npub,
const uint8_t *k); const uint8_t *k);
int aegis128l_state_encrypt_update(aegis128l_state *st_, uint8_t *c, const uint8_t *m, size_t mlen); int aegis128l_state_encrypt_update(aegis128l_state *st_,
int aegis128l_state_encrypt_final(aegis128l_state *st_, uint8_t *mac, size_t maclen); uint8_t *c,
int aegis128l_state_decrypt_update(aegis128l_state *st_, uint8_t *m, const uint8_t *c, size_t clen) ; size_t clen_max,
int aegis128l_state_decrypt_final(aegis128l_state *st_, const uint8_t *mac, size_t maclen) ; 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) ;
void aegis128l_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis128l_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
void aegis128l_encrypt_unauthenticated(uint8_t *c, void aegis128l_encrypt_unauthenticated(uint8_t *c,
const uint8_t *m, const uint8_t *m,
@@ -126,14 +151,33 @@ void aegis128x2_state_init(aegis128x2_state *st_,
const uint8_t *k); const uint8_t *k);
int aegis128x2_state_encrypt_update(aegis128x2_state *st_, int aegis128x2_state_encrypt_update(aegis128x2_state *st_,
uint8_t *c, uint8_t *c,
size_t clen_max,
size_t *written,
const uint8_t *m, const uint8_t *m,
size_t mlen); size_t mlen);
int aegis128x2_state_encrypt_final(aegis128x2_state *st_, uint8_t *mac, size_t maclen); int aegis128x2_state_encrypt_detached_final(aegis128x2_state *st_,
int aegis128x2_state_decrypt_update(aegis128x2_state *st_, uint8_t *c,
uint8_t *m, size_t clen_max,
const uint8_t *c, size_t *written,
size_t clen) ; uint8_t *mac,
int aegis128x2_state_decrypt_final(aegis128x2_state *st_, const uint8_t *mac, size_t maclen) ; 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) ;
void aegis128x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis128x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
void aegis128x2_encrypt_unauthenticated(uint8_t *c, void aegis128x2_encrypt_unauthenticated(uint8_t *c,
const uint8_t *m, const uint8_t *m,
@@ -201,14 +245,33 @@ void aegis128x4_state_init(aegis128x4_state *st_,
const uint8_t *k); const uint8_t *k);
int aegis128x4_state_encrypt_update(aegis128x4_state *st_, int aegis128x4_state_encrypt_update(aegis128x4_state *st_,
uint8_t *c, uint8_t *c,
size_t clen_max,
size_t *written,
const uint8_t *m, const uint8_t *m,
size_t mlen); size_t mlen);
int aegis128x4_state_encrypt_final(aegis128x4_state *st_, uint8_t *mac, size_t maclen); int aegis128x4_state_encrypt_detached_final(aegis128x4_state *st_,
int aegis128x4_state_decrypt_update(aegis128x4_state *st_, uint8_t *c,
uint8_t *m, size_t clen_max,
const uint8_t *c, size_t *written,
size_t clen) ; uint8_t *mac,
int aegis128x4_state_decrypt_final(aegis128x4_state *st_, const uint8_t *mac, size_t maclen) ; 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) ;
void aegis128x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis128x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
void aegis128x4_encrypt_unauthenticated(uint8_t *c, void aegis128x4_encrypt_unauthenticated(uint8_t *c,
const uint8_t *m, const uint8_t *m,
@@ -274,10 +337,35 @@ void aegis256_state_init(aegis256_state *st_,
size_t adlen, size_t adlen,
const uint8_t *npub, const uint8_t *npub,
const uint8_t *k); const uint8_t *k);
int aegis256_state_encrypt_update(aegis256_state *st_, uint8_t *c, const uint8_t *m, size_t mlen); int aegis256_state_encrypt_update(aegis256_state *st_,
int aegis256_state_encrypt_final(aegis256_state *st_, uint8_t *mac, size_t maclen); uint8_t *c,
int aegis256_state_decrypt_update(aegis256_state *st_, uint8_t *m, const uint8_t *c, size_t clen) ; size_t clen_max,
int aegis256_state_decrypt_final(aegis256_state *st_, const uint8_t *mac, size_t maclen) ; 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) ;
void aegis256_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis256_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
void aegis256_encrypt_unauthenticated(uint8_t *c, void aegis256_encrypt_unauthenticated(uint8_t *c,
const uint8_t *m, const uint8_t *m,
@@ -345,14 +433,33 @@ void aegis256x2_state_init(aegis256x2_state *st_,
const uint8_t *k); const uint8_t *k);
int aegis256x2_state_encrypt_update(aegis256x2_state *st_, int aegis256x2_state_encrypt_update(aegis256x2_state *st_,
uint8_t *c, uint8_t *c,
size_t clen_max,
size_t *written,
const uint8_t *m, const uint8_t *m,
size_t mlen); size_t mlen);
int aegis256x2_state_encrypt_final(aegis256x2_state *st_, uint8_t *mac, size_t maclen); int aegis256x2_state_encrypt_detached_final(aegis256x2_state *st_,
int aegis256x2_state_decrypt_update(aegis256x2_state *st_, uint8_t *c,
uint8_t *m, size_t clen_max,
const uint8_t *c, size_t *written,
size_t clen) ; uint8_t *mac,
int aegis256x2_state_decrypt_final(aegis256x2_state *st_, const uint8_t *mac, size_t maclen) ; 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) ;
void aegis256x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis256x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
void aegis256x2_encrypt_unauthenticated(uint8_t *c, void aegis256x2_encrypt_unauthenticated(uint8_t *c,
const uint8_t *m, const uint8_t *m,
@@ -420,14 +527,33 @@ void aegis256x4_state_init(aegis256x4_state *st_,
const uint8_t *k); const uint8_t *k);
int aegis256x4_state_encrypt_update(aegis256x4_state *st_, int aegis256x4_state_encrypt_update(aegis256x4_state *st_,
uint8_t *c, uint8_t *c,
size_t clen_max,
size_t *written,
const uint8_t *m, const uint8_t *m,
size_t mlen); size_t mlen);
int aegis256x4_state_encrypt_final(aegis256x4_state *st_, uint8_t *mac, size_t maclen); int aegis256x4_state_encrypt_detached_final(aegis256x4_state *st_,
int aegis256x4_state_decrypt_update(aegis256x4_state *st_, uint8_t *c,
uint8_t *m, size_t clen_max,
const uint8_t *c, size_t *written,
size_t clen) ; uint8_t *mac,
int aegis256x4_state_decrypt_final(aegis256x4_state *st_, const uint8_t *mac, size_t maclen) ; 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) ;
void aegis256x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); void aegis256x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
void aegis256x4_encrypt_unauthenticated(uint8_t *c, void aegis256x4_encrypt_unauthenticated(uint8_t *c,
const uint8_t *m, const uint8_t *m,
+78 -46
View File
@@ -1,68 +1,100 @@
"""Custom build backend that builds libaegis with Zig before building the Python package.""" """Custom build backend that builds libaegis with Zig before building the Python package."""
import os
import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from setuptools import build_meta from setuptools import build_meta as _orig
__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 _prepare(): def _check_zig_available():
"""Prepare the build environment and build libaegis.""" """Check if Zig is installed and available."""
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: if shutil.which("zig") is None:
raise RuntimeError( raise RuntimeError(
"Zig compiler not found. Install from https://ziglang.org/download/" "\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"
) )
# Build libaegis
def _build_libaegis():
"""Build libaegis static library with Zig."""
# Check Zig availability first
_check_zig_available()
libaegis_dir = Path(__file__).parent.parent / "libaegis" libaegis_dir = Path(__file__).parent.parent / "libaegis"
cmd = ["zig", "build", "-Drelease"] if not libaegis_dir.exists():
if sys.platform == "darwin": raise FileNotFoundError(
arch = {"arm64": "aarch64", "x86_64": "x86_64"}.get(platform.machine()) f"libaegis directory not found at {libaegis_dir}. "
if arch: "Cannot build static library."
cmd.append(f"-Dtarget={arch}-macos.{_MACOS_TARGET}") )
subprocess.run(cmd, cwd=libaegis_dir, check=True)
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
build_sdist = build_meta.build_sdist # Expose all the standard build backend hooks
get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist def get_requires_for_build_wheel(config_settings=None):
get_requires_for_build_wheel = build_meta.get_requires_for_build_wheel """Return build requirements and ensure libaegis is built first."""
prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel _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)
# Wheel build hooks - need libaegis built first
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
_prepare() """Build wheel with libaegis built first."""
return build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) _build_libaegis()
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
def build_editable(wheel_directory, config_settings=None, metadata_directory=None): def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
_prepare() """Build editable install with libaegis built first."""
return build_meta.build_editable( _build_libaegis()
wheel_directory, config_settings, metadata_directory return _orig.build_editable(wheel_directory, config_settings, metadata_directory)
)
+118 -261
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env -S uv run #!/usr/bin/env -S uv run
"""Build wheels for all supported Python versions using uv.""" """Build wheels for all supported Python versions using uv."""
import os
import platform import platform
import shutil import shutil
import subprocess import subprocess
@@ -14,34 +13,19 @@ from packaging.version import Version
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
import generate import generate
# Minimum macOS deployment target for compatibility PYTHON_VERSIONS = [
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.10",
"3.11", "3.11",
"3.12", "3.12",
"3.13+gil", "3.13",
"3.14+gil", "3.14",
"3.15+gil",
]
# Non-ABI3 wheels: each needs its own build (free-threaded and PyPy)
NON_ABI3_VERSIONS = [
"3.14t", "3.14t",
"3.15",
"3.15t", "3.15t",
"pypy3.10", "pypy3.10",
"pypy3.11", "pypy3.11",
] ]
# All versions for testing and benchmarking
ALL_PYTHON_VERSIONS = ABI3_COVERED_VERSIONS + NON_ABI3_VERSIONS
def get_version_from_scm(): def get_version_from_scm():
"""Get version from setuptools-scm (git tags).""" """Get version from setuptools-scm (git tags)."""
@@ -110,29 +94,21 @@ def make_release_message(version):
return msg return msg
def run_command(cmd, description=None, env=None): def run_command(cmd, description):
"""Run a command and handle errors. If description is None, only print the command.""" """Run a command and handle errors."""
if description: print(f"\n{'=' * 70}")
print(f"\n{'=' * 70}") print(f"{description}")
print(f"{description}") print(f"{'=' * 70}")
print(f"{'=' * 70}")
print(f">>> {' '.join(cmd)}") print(f">>> {' '.join(cmd)}")
try: try:
subprocess.run(cmd, check=True, env=env) subprocess.run(cmd, check=True)
print(f"{description} completed successfully")
return True return True
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Command failed with exit code {e.returncode}", file=sys.stderr) print(f"{description} failed with exit code {e.returncode}", file=sys.stderr)
return False 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): def normalize_line_endings(repo_root: Path):
"""Normalize all text files to LF line endings.""" """Normalize all text files to LF line endings."""
# Patterns for files to normalize # Patterns for files to normalize
@@ -156,160 +132,6 @@ def normalize_line_endings(repo_root: Path):
file_path.write_bytes(content) 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(): def main():
"""Build wheels for all supported Python versions.""" """Build wheels for all supported Python versions."""
repo_root = Path(__file__).parent.parent repo_root = Path(__file__).parent.parent
@@ -324,15 +146,14 @@ def main():
return 1 return 1
# Run ruff to check and fix any issues # Run ruff to check and fix any issues
print(f"\n{'=' * 70}") if not run_command(
print("Linting and formatting") ["uv", "run", "ruff", "check", "--fix", "."], "Running ruff check --fix"
print(f"{'=' * 70}") ):
if not run_command(["uv", "run", "ruff", "check", "--fix", "."]):
print("✗ Ruff check failed", file=sys.stderr) print("✗ Ruff check failed", file=sys.stderr)
return 1 return 1
# Run ruff format # Run ruff format
if not run_command(["uv", "run", "ruff", "format", "."]): if not run_command(["uv", "run", "ruff", "format", "."], "Running ruff format"):
print("✗ Ruff format failed", file=sys.stderr) print("✗ Ruff format failed", file=sys.stderr)
return 1 return 1
@@ -351,11 +172,7 @@ def main():
f"Packaging aeg-{version}" f"Packaging aeg-{version}"
+ (" for release" if is_release else " (not release)") + (" for release" if is_release else " (not release)")
) )
print(f"Building: 1 ABI3 wheel (for Python {', '.join(ABI3_COVERED_VERSIONS)})") print(f"Building wheels for Python versions: {', '.join(PYTHON_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=" ") print(f"Output directory: {dist_dir}", end=" ")
# Clean dist directory # Clean dist directory
@@ -364,89 +181,132 @@ def main():
shutil.rmtree(dist_dir) shutil.rmtree(dist_dir)
else: else:
print("(created)") 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}") print(f"{'=' * 70}")
# Build source distribution first # Build source distribution first
if not run_command(["uv", "build", "--sdist", "--quiet"], env=get_build_env()): if not run_command(
["uv", "build", "--sdist", "--quiet"], "Building source distribution"
):
print("✗ Source distribution build failed", file=sys.stderr) print("✗ Source distribution build failed", file=sys.stderr)
return 1 return 1
failed_builds = [] failed_builds = []
failed_tests = []
successful_wheels = [] successful_wheels = []
wheel_for_version = {} # Map Python version to wheel path
# Build ABI3 wheel (once, works for all GIL-enabled versions) for py_version in PYTHON_VERSIONS:
abi3_wheel = build_abi3_wheel(dist_dir, ABI3_BUILD_VERSION) # Build wheel
if abi3_wheel: description = f"Building wheel for Python {py_version}"
successful_wheels.append(abi3_wheel) cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"]
# 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})")
# Build non-ABI3 wheels (free-threaded and PyPy) if not run_command(cmd, description):
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:
failed_builds.append(py_version) 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 continue
# Test the wheel with pytest # Find the wheel for this version
if not test_wheel(wheel, py_version): if py_version.startswith("pypy"):
failed_tests.append(py_version) # PyPy wheels use pp3XX format
version_tag = f"pp{py_version.replace('pypy', '').replace('.', '')}"
else:
version_tag = f"cp{py_version.replace('.', '').replace('t', '')}"
wheel_pattern = f"aeg-*-{version_tag}-*.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)
continue continue
# Run benchmark wheel = wheels[0]
if not run_benchmark(wheel, py_version):
failed_tests.append(py_version) # 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)
all_wheels = list(dist_dir.glob(f"aeg-*-{version_tag}-*.whl"))
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)
continue 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)
continue
successful_wheels.append(wheel)
# Summary # Summary
print(f"\n{'=' * 70}") print(f"\n{'=' * 70}")
print("BUILD SUMMARY") print("BUILD SUMMARY")
print(f"{'=' * 70}") print(f"{'=' * 70}")
print( print(
f"Successful builds: sdist and {len(successful_wheels)} wheels " f"Successful builds: sdist and {len(successful_wheels)}/{len(PYTHON_VERSIONS)} 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: if failed_builds:
print(f"\nFailed builds: {len(failed_builds)}") print(f"\nFailed builds: {len(failed_builds)}")
for failed_version in failed_builds: for version in failed_builds:
print(f"{failed_version}") print(f" Python {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: if not successful_wheels:
print("\n✗ No successful wheels to upload") print("\n✗ No successful wheels to upload")
@@ -478,7 +338,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
try: sys.exit(main())
sys.exit(main())
except KeyboardInterrupt:
sys.exit(1)