Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daba717ebb | ||
|
|
5b74bcb683 | ||
|
|
d9a18f43d5 | ||
|
|
98ee4ab7b3 |
No files matched your search
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
/pyaegis/build
|
/pyaegis/build
|
||||||
/pyaegis/_aegis.*.so
|
/pyaegis/_aegis*.so
|
||||||
/pyaegis/_aegis.*.pyd
|
/pyaegis/_aegis*.pyd
|
||||||
__pycache__
|
__pycache__
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!.gitmodules
|
!.gitmodules
|
||||||
@@ -4,10 +4,8 @@ include tools/build_backend.py
|
|||||||
include BUILD.md
|
include BUILD.md
|
||||||
include README.md
|
include README.md
|
||||||
recursive-include libaegis *.c *.h *.zig *.zon
|
recursive-include libaegis *.c *.h *.zig *.zon
|
||||||
include libaegis/CMakeLists.txt
|
|
||||||
include libaegis/LICENSE
|
include libaegis/LICENSE
|
||||||
include libaegis/README.md
|
include libaegis/README.md
|
||||||
recursive-include libaegis/cmake *.cmake *.cmake.in
|
|
||||||
graft libaegis/src
|
graft libaegis/src
|
||||||
include libaegis/build.zig
|
include libaegis/build.zig
|
||||||
include libaegis/build.zig.zon
|
include libaegis/build.zig.zon
|
||||||
@@ -295,9 +295,7 @@ def decrypt(
|
|||||||
out = bytearray(expected_out)
|
out = bytearray(expected_out)
|
||||||
else:
|
else:
|
||||||
if into.nbytes < expected_out:
|
if into.nbytes < expected_out:
|
||||||
raise TypeError(
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
"into length must be at least ct.nbytes - maclen"
|
|
||||||
)
|
|
||||||
out = into
|
out = into
|
||||||
|
|
||||||
rc = _lib.aegis128l_decrypt(
|
rc = _lib.aegis128l_decrypt(
|
||||||
@@ -582,7 +580,9 @@ class Mac:
|
|||||||
out = into
|
out = into
|
||||||
|
|
||||||
clone = self.clone()
|
clone = self.clone()
|
||||||
rc = _lib.aegis128l_mac_final(clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes)
|
rc = _lib.aegis128l_mac_final(
|
||||||
|
clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).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}")
|
||||||
@@ -851,7 +851,9 @@ class Decryptor:
|
|||||||
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}")
|
||||||
w = int(written[0])
|
w = int(written[0])
|
||||||
assert w == expected_out, f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
|
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)[:w] # type: ignore
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
def final(self, mac: Buffer) -> None:
|
||||||
|
|||||||
@@ -295,9 +295,7 @@ def decrypt(
|
|||||||
out = bytearray(expected_out)
|
out = bytearray(expected_out)
|
||||||
else:
|
else:
|
||||||
if into.nbytes < expected_out:
|
if into.nbytes < expected_out:
|
||||||
raise TypeError(
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
"into length must be at least ct.nbytes - maclen"
|
|
||||||
)
|
|
||||||
out = into
|
out = into
|
||||||
|
|
||||||
rc = _lib.aegis128x2_decrypt(
|
rc = _lib.aegis128x2_decrypt(
|
||||||
@@ -582,7 +580,9 @@ class Mac:
|
|||||||
out = into
|
out = into
|
||||||
|
|
||||||
clone = self.clone()
|
clone = self.clone()
|
||||||
rc = _lib.aegis128x2_mac_final(clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes)
|
rc = _lib.aegis128x2_mac_final(
|
||||||
|
clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).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}")
|
||||||
@@ -851,7 +851,9 @@ class Decryptor:
|
|||||||
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}")
|
||||||
w = int(written[0])
|
w = int(written[0])
|
||||||
assert w == expected_out, f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
|
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)[:w] # type: ignore
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
def final(self, mac: Buffer) -> None:
|
||||||
|
|||||||
@@ -295,9 +295,7 @@ def decrypt(
|
|||||||
out = bytearray(expected_out)
|
out = bytearray(expected_out)
|
||||||
else:
|
else:
|
||||||
if into.nbytes < expected_out:
|
if into.nbytes < expected_out:
|
||||||
raise TypeError(
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
"into length must be at least ct.nbytes - maclen"
|
|
||||||
)
|
|
||||||
out = into
|
out = into
|
||||||
|
|
||||||
rc = _lib.aegis128x4_decrypt(
|
rc = _lib.aegis128x4_decrypt(
|
||||||
@@ -582,7 +580,9 @@ class Mac:
|
|||||||
out = into
|
out = into
|
||||||
|
|
||||||
clone = self.clone()
|
clone = self.clone()
|
||||||
rc = _lib.aegis128x4_mac_final(clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes)
|
rc = _lib.aegis128x4_mac_final(
|
||||||
|
clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).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}")
|
||||||
@@ -851,7 +851,9 @@ class Decryptor:
|
|||||||
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}")
|
||||||
w = int(written[0])
|
w = int(written[0])
|
||||||
assert w == expected_out, f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
|
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)[:w] # type: ignore
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
def final(self, mac: Buffer) -> None:
|
||||||
|
|||||||
+7
-5
@@ -295,9 +295,7 @@ def decrypt(
|
|||||||
out = bytearray(expected_out)
|
out = bytearray(expected_out)
|
||||||
else:
|
else:
|
||||||
if into.nbytes < expected_out:
|
if into.nbytes < expected_out:
|
||||||
raise TypeError(
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
"into length must be at least ct.nbytes - maclen"
|
|
||||||
)
|
|
||||||
out = into
|
out = into
|
||||||
|
|
||||||
rc = _lib.aegis256_decrypt(
|
rc = _lib.aegis256_decrypt(
|
||||||
@@ -582,7 +580,9 @@ class Mac:
|
|||||||
out = into
|
out = into
|
||||||
|
|
||||||
clone = self.clone()
|
clone = self.clone()
|
||||||
rc = _lib.aegis256_mac_final(clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes)
|
rc = _lib.aegis256_mac_final(
|
||||||
|
clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).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}")
|
||||||
@@ -851,7 +851,9 @@ class Decryptor:
|
|||||||
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}")
|
||||||
w = int(written[0])
|
w = int(written[0])
|
||||||
assert w == expected_out, f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
|
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)[:w] # type: ignore
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
def final(self, mac: Buffer) -> None:
|
||||||
|
|||||||
@@ -295,9 +295,7 @@ def decrypt(
|
|||||||
out = bytearray(expected_out)
|
out = bytearray(expected_out)
|
||||||
else:
|
else:
|
||||||
if into.nbytes < expected_out:
|
if into.nbytes < expected_out:
|
||||||
raise TypeError(
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
"into length must be at least ct.nbytes - maclen"
|
|
||||||
)
|
|
||||||
out = into
|
out = into
|
||||||
|
|
||||||
rc = _lib.aegis256x2_decrypt(
|
rc = _lib.aegis256x2_decrypt(
|
||||||
@@ -582,7 +580,9 @@ class Mac:
|
|||||||
out = into
|
out = into
|
||||||
|
|
||||||
clone = self.clone()
|
clone = self.clone()
|
||||||
rc = _lib.aegis256x2_mac_final(clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes)
|
rc = _lib.aegis256x2_mac_final(
|
||||||
|
clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).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}")
|
||||||
@@ -851,7 +851,9 @@ class Decryptor:
|
|||||||
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}")
|
||||||
w = int(written[0])
|
w = int(written[0])
|
||||||
assert w == expected_out, f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
|
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)[:w] # type: ignore
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
def final(self, mac: Buffer) -> None:
|
||||||
|
|||||||
+6
-2
@@ -1,11 +1,11 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0", "cffi>=2.0.0"]
|
requires = ["setuptools>=61.0", "cffi>=2.0.0", "setuptools-scm>=8.0"]
|
||||||
build-backend = "build_backend"
|
build-backend = "build_backend"
|
||||||
backend-path = ["tools"]
|
backend-path = ["tools"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pyaegis"
|
name = "pyaegis"
|
||||||
version = "0.3.1"
|
dynamic = ["version"]
|
||||||
description = "Python bindings for libaegis"
|
description = "Python bindings for libaegis"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -25,7 +25,9 @@ Homepage = "https://github.com/LeoVasanko/pyaegis"
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.4.2",
|
"pytest>=8.4.2",
|
||||||
|
"ruff>=0.14.4",
|
||||||
"setuptools>=80.9.0",
|
"setuptools>=80.9.0",
|
||||||
|
"setuptools-scm>=9.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
@@ -33,3 +35,5 @@ packages = ["pyaegis"]
|
|||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
pyaegis = ["*.h", "*.so", "*.pyd"]
|
pyaegis = ["*.h", "*.so", "*.pyd"]
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
@@ -1,28 +1,23 @@
|
|||||||
"""Setup script for pyaegis - builds CFFI extension linking to libaegis.a"""
|
"""Setup script for pyaegis - builds CFFI extension with libaegis C library."""
|
||||||
|
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from cffi import FFI
|
from cffi import FFI
|
||||||
from setuptools import setup
|
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())
|
||||||
|
|
||||||
def find_libaegis():
|
# Include directory for headers
|
||||||
"""Locate libaegis.a - check common locations."""
|
libaegis_include = Path("libaegis/src/include")
|
||||||
libaegis_paths = [
|
if not libaegis_include.exists():
|
||||||
Path("libaegis/zig-out/lib/libaegis.a"), # Zig build output (repo build)
|
raise RuntimeError(f"libaegis include directory not found at {libaegis_include}")
|
||||||
Path("libaegis/build/libaegis.a"), # CMake build output (repo build)
|
include_dirs = [str(libaegis_include)]
|
||||||
Path("/usr/local/lib/libaegis.a"), # System install
|
|
||||||
Path("/usr/lib/libaegis.a"), # System install
|
|
||||||
]
|
|
||||||
|
|
||||||
for path in libaegis_paths:
|
|
||||||
if path.exists():
|
|
||||||
print(f"Found libaegis.a at: {path.resolve()}")
|
|
||||||
return str(path.resolve())
|
|
||||||
|
|
||||||
# Return None instead of raising - will be caught during build
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Read the CDEF header
|
# Read the CDEF header
|
||||||
cdef_path = Path(__file__).parent / "pyaegis" / "aegis_cdef.h"
|
cdef_path = Path(__file__).parent / "pyaegis" / "aegis_cdef.h"
|
||||||
@@ -32,15 +27,6 @@ cdef_content = cdef_path.read_text(encoding="utf-8")
|
|||||||
ffibuilder = FFI()
|
ffibuilder = FFI()
|
||||||
ffibuilder.cdef(cdef_content)
|
ffibuilder.cdef(cdef_content)
|
||||||
|
|
||||||
# Include directory for headers
|
|
||||||
include_dirs = []
|
|
||||||
libaegis_include = Path("libaegis/src/include")
|
|
||||||
if libaegis_include.exists():
|
|
||||||
include_dirs.append(str(libaegis_include.resolve()))
|
|
||||||
|
|
||||||
# Try to find libaegis.a, but don't fail if not found (build backend will build it)
|
|
||||||
libaegis_static = find_libaegis()
|
|
||||||
|
|
||||||
# Set the source
|
# Set the source
|
||||||
ffibuilder.set_source(
|
ffibuilder.set_source(
|
||||||
"pyaegis._aegis", # module name
|
"pyaegis._aegis", # module name
|
||||||
@@ -54,7 +40,7 @@ ffibuilder.set_source(
|
|||||||
#include "aegis256x4.h"
|
#include "aegis256x4.h"
|
||||||
""",
|
""",
|
||||||
include_dirs=include_dirs,
|
include_dirs=include_dirs,
|
||||||
extra_objects=[libaegis_static] if libaegis_static else [],
|
extra_objects=[libaegis_static],
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
#! /usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def tvdump(topic, tvs):
|
|
||||||
with open(filename(topic), "w") as f:
|
|
||||||
f.write(json.dumps(tvs, indent=2))
|
|
||||||
|
|
||||||
print(json.dumps(tvs, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
def filename(topic):
|
|
||||||
return re.sub(r"[^a-z0-9]+", "-", topic.lower()) + ".json"
|
|
||||||
|
|
||||||
|
|
||||||
header = True
|
|
||||||
in_tv = False
|
|
||||||
tv = {}
|
|
||||||
tvs = []
|
|
||||||
must_fail = False
|
|
||||||
with open("../draft-irtf-cfrg-aegis-aead.md") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line == "":
|
|
||||||
continue
|
|
||||||
if line.startswith("# Test Vectors"):
|
|
||||||
header = False
|
|
||||||
continue
|
|
||||||
if header:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith("## "):
|
|
||||||
if len(tvs) > 0:
|
|
||||||
tvdump(topic, tvs)
|
|
||||||
topic = line[3:]
|
|
||||||
tv_name = topic
|
|
||||||
tvs = []
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith("### "):
|
|
||||||
tv_name = line[4:]
|
|
||||||
tv = {"test": tv_name}
|
|
||||||
in_tv = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line == "~~~ test-vectors":
|
|
||||||
in_tv = True
|
|
||||||
tv = {"name": tv_name}
|
|
||||||
if must_fail:
|
|
||||||
tv["error"] = "verification failed"
|
|
||||||
must_fail = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line == "~~~":
|
|
||||||
tvs.append(tv)
|
|
||||||
in_tv = False
|
|
||||||
current_key = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.find("verification failed") != -1:
|
|
||||||
must_fail = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line == "After initialization:":
|
|
||||||
tv_name = tv_name + " (after initialization)"
|
|
||||||
|
|
||||||
if not in_tv:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = line.split(":")
|
|
||||||
if len(parts) == 2:
|
|
||||||
key = parts[0].strip()
|
|
||||||
value = parts[1].strip()
|
|
||||||
if key == "After Update":
|
|
||||||
continue
|
|
||||||
if key in tv:
|
|
||||||
key = key + "_2"
|
|
||||||
tv[key] = value
|
|
||||||
current_key = key
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not current_key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
tv[key] += line.strip()
|
|
||||||
|
|
||||||
tvdump(topic, tvs)
|
|
||||||
+2
-2
@@ -267,7 +267,7 @@ def generate_python_modules(
|
|||||||
if dst.exists() and dst.read_text(encoding="utf-8") == new_content:
|
if dst.exists() and dst.read_text(encoding="utf-8") == new_content:
|
||||||
unchanged.append(dst)
|
unchanged.append(dst)
|
||||||
else:
|
else:
|
||||||
dst.write_text(new_content, encoding="utf-8")
|
dst.write_bytes(new_content.encode())
|
||||||
updated.append(dst)
|
updated.append(dst)
|
||||||
|
|
||||||
return updated, unchanged
|
return updated, unchanged
|
||||||
@@ -301,7 +301,7 @@ def main() -> int:
|
|||||||
if cdef_path.exists() and cdef_path.read_text(encoding="utf-8") == cdef_content:
|
if cdef_path.exists() and cdef_path.read_text(encoding="utf-8") == cdef_content:
|
||||||
print(f" - No changes to {cdef_path}", file=sys.stderr)
|
print(f" - No changes to {cdef_path}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
cdef_path.write_text(cdef_content, encoding="utf-8")
|
cdef_path.write_bytes(cdef_content.encode())
|
||||||
print(f" - Updated {cdef_path}", file=sys.stderr)
|
print(f" - Updated {cdef_path}", file=sys.stderr)
|
||||||
|
|
||||||
print("Step 3: Generating Python modules...", file=sys.stderr)
|
print("Step 3: Generating Python modules...", file=sys.stderr)
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build wheels for all supported Python versions using uv."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
|
# Import generate module from same directory
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
import generate
|
||||||
|
|
||||||
|
|
||||||
|
def get_python_versions():
|
||||||
|
"""Get supported Python versions."""
|
||||||
|
pyproject_toml = Path(__file__).parent.parent / "pyproject.toml"
|
||||||
|
data = tomllib.loads(pyproject_toml.read_text(encoding="utf-8"))
|
||||||
|
spec = SpecifierSet(data["project"]["requires-python"])
|
||||||
|
# Generate versions that match the specifier (up to Python 3.14)
|
||||||
|
return [f"3.{minor}" for minor in range(10, 15) if f"3.{minor}" in spec]
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_from_scm():
|
||||||
|
"""Get version from setuptools-scm (git tags)."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["uv", "run", "-m", "setuptools_scm"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
cwd=Path(__file__).parent.parent,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"✗ Error getting version from setuptools-scm: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_release_version(version):
|
||||||
|
"""Check if version is a clean release (no dev/post/local identifiers)."""
|
||||||
|
# A release version is just x.y.z with optional alpha/beta/rc suffixes
|
||||||
|
# No +local or .devN or .postN
|
||||||
|
if not version:
|
||||||
|
return False
|
||||||
|
return not any(marker in version for marker in ["+", ".dev", ".post"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_version(current_version):
|
||||||
|
"""Get the next release version from a dev version."""
|
||||||
|
# Parse base version (strips dev/local parts)
|
||||||
|
try:
|
||||||
|
v = Version(current_version)
|
||||||
|
return f"{v.major}.{v.minor}.{v.micro}"
|
||||||
|
except Exception:
|
||||||
|
return current_version
|
||||||
|
|
||||||
|
|
||||||
|
def is_working_copy_clean():
|
||||||
|
"""Check if git working copy is clean."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "status", "--porcelain"], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.returncode == 0 and not result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def make_release_message(version):
|
||||||
|
"""Generate message for making a release."""
|
||||||
|
next_version = get_next_version(version)
|
||||||
|
is_clean = is_working_copy_clean()
|
||||||
|
|
||||||
|
msg = "\n⚠️ This is not a clean release version; upload to PyPI skipped.\n\n"
|
||||||
|
msg += f"To create a release (e.g. {next_version}) and upload to PyPI:\n"
|
||||||
|
|
||||||
|
if not is_clean:
|
||||||
|
msg += " 1. Add and commit changes on the working copy\n"
|
||||||
|
msg += f" 2. Tag the commit: git tag v{next_version}\n"
|
||||||
|
msg += " 3. Run this script again\n"
|
||||||
|
msg += f" 4. Push the tag: git push origin v{next_version}\n"
|
||||||
|
else:
|
||||||
|
msg += f" 1. Tag the current commit: git tag v{next_version}\n"
|
||||||
|
msg += " 2. Run this script again\n"
|
||||||
|
msg += f" 3. Push the tag: git push origin v{next_version}\n"
|
||||||
|
|
||||||
|
msg += (
|
||||||
|
f"\nIf the build didn't work, delete the tag with git tag -d v{next_version}\n"
|
||||||
|
)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
PYTHON_VERSIONS = get_python_versions()
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, description):
|
||||||
|
"""Run a command and handle errors."""
|
||||||
|
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")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"✗ {description} failed with exit code {e.returncode}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_line_endings(repo_root: Path):
|
||||||
|
"""Normalize all text files to LF line endings."""
|
||||||
|
# Patterns for files to normalize
|
||||||
|
patterns = [
|
||||||
|
"pyaegis/**/*.py",
|
||||||
|
"pyaegis/**/*.h",
|
||||||
|
"tests/**/*.py",
|
||||||
|
"tools/**/*.py",
|
||||||
|
"*.py",
|
||||||
|
"*.md",
|
||||||
|
"*.txt",
|
||||||
|
"*.toml",
|
||||||
|
"*.in",
|
||||||
|
]
|
||||||
|
for pattern in patterns:
|
||||||
|
for file_path in repo_root.glob(pattern):
|
||||||
|
if file_path.is_file():
|
||||||
|
content = file_path.read_bytes()
|
||||||
|
if b"\r\n" in content:
|
||||||
|
content = content.replace(b"\r\n", b"\n")
|
||||||
|
file_path.write_bytes(content)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Build wheels for all supported Python versions."""
|
||||||
|
repo_root = Path(__file__).parent.parent
|
||||||
|
dist_dir = repo_root / "dist"
|
||||||
|
|
||||||
|
# Generate CFFI definitions and Python modules
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("Code generation from C headers (tools/generate.py)")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
if generate.main() != 0:
|
||||||
|
print("✗ Code generation failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run ruff to check and fix any issues
|
||||||
|
if not run_command(
|
||||||
|
["uv", "run", "ruff", "check", "--fix", "."], "Running 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"):
|
||||||
|
print("✗ Ruff format failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Normalize all line endings to LF (important for consistent builds)
|
||||||
|
normalize_line_endings(repo_root)
|
||||||
|
|
||||||
|
# Get version from git repo
|
||||||
|
version = get_version_from_scm()
|
||||||
|
if not version:
|
||||||
|
return 1
|
||||||
|
is_release = is_release_version(version)
|
||||||
|
|
||||||
|
# Main header for the packaging process
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(
|
||||||
|
f"Packaging pyaegis-{version}"
|
||||||
|
+ (" for release" if is_release else " (not release)")
|
||||||
|
)
|
||||||
|
print(f"Building wheels for Python versions: {', '.join(PYTHON_VERSIONS)}")
|
||||||
|
print(f"Output directory: {dist_dir}", end=" ")
|
||||||
|
|
||||||
|
# Clean dist directory
|
||||||
|
if dist_dir.exists():
|
||||||
|
print("(wiped)")
|
||||||
|
shutil.rmtree(dist_dir)
|
||||||
|
else:
|
||||||
|
print("(created)")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
# Build source distribution first
|
||||||
|
if not run_command(
|
||||||
|
["uv", "build", "--sdist", "--quiet"], "Building source distribution"
|
||||||
|
):
|
||||||
|
print("✗ Source distribution build failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
failed_builds = []
|
||||||
|
successful_wheels = []
|
||||||
|
|
||||||
|
for py_version in PYTHON_VERSIONS:
|
||||||
|
# Build wheel
|
||||||
|
description = f"Building wheel for Python {py_version}"
|
||||||
|
cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"]
|
||||||
|
|
||||||
|
if not run_command(cmd, description):
|
||||||
|
failed_builds.append(py_version)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find the wheel for this version
|
||||||
|
wheel_pattern = f"pyaegis-*-cp{py_version.replace('.', '')}-*.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
|
||||||
|
|
||||||
|
wheel = wheels[0]
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Run benchmark (use --isolated to avoid .venv conflicts)
|
||||||
|
bench_cmd = [
|
||||||
|
"uv",
|
||||||
|
"run",
|
||||||
|
"--isolated",
|
||||||
|
"--python",
|
||||||
|
py_version,
|
||||||
|
"--with",
|
||||||
|
str(wheel),
|
||||||
|
"-m",
|
||||||
|
"pyaegis.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
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("BUILD SUMMARY")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print(
|
||||||
|
f"Successful builds: sdist and {len(successful_wheels)}/{len(PYTHON_VERSIONS)} wheels"
|
||||||
|
)
|
||||||
|
|
||||||
|
if failed_builds:
|
||||||
|
print(f"\nFailed builds: {len(failed_builds)}")
|
||||||
|
for version in failed_builds:
|
||||||
|
print(f" ✗ Python {version}")
|
||||||
|
|
||||||
|
if not successful_wheels:
|
||||||
|
print("\n✗ No successful wheels to upload")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# List files to upload
|
||||||
|
sdist = list(dist_dir.glob("*.tar.gz"))
|
||||||
|
upload_files = sdist + successful_wheels
|
||||||
|
|
||||||
|
for file in upload_files:
|
||||||
|
print(f" - {file.name}")
|
||||||
|
|
||||||
|
# Only upload if this is a clean release version
|
||||||
|
if not is_release:
|
||||||
|
print(make_release_message(version))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Upload with twine
|
||||||
|
upload_cmd = ["uvx", "twine", "upload"] + [str(f) for f in upload_files]
|
||||||
|
if not run_command(upload_cmd, "Uploading to PyPI with twine"):
|
||||||
|
print("\n✗ Upload failed")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("All builds and upload completed successfully!")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in new issue
Block a user