diff --git a/.gitignore b/.gitignore index 5d6be57..03aaeff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ *.egg-info /dist /pyaegis/build +/pyaegis/_aegis.*.so +/pyaegis/_aegis.*.pyd +/pyaegis/_aegis.c __pycache__ !.gitignore !.gitmodules diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a3b35af --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include pyaegis/aegis_cdef.h +include setup.py +include BUILD.md +include README.md +recursive-include libaegis/src/include *.h +include libaegis/zig-out/lib/libaegis.a diff --git a/pyaegis/_loader.py b/pyaegis/_loader.py index 381ac97..edad13c 100644 --- a/pyaegis/_loader.py +++ b/pyaegis/_loader.py @@ -1,36 +1,7 @@ -"""Dynamic loader for libaegis using CFFI (ABI mode).""" +"""Loader for libaegis CFFI extension module.""" -import os -import sys -from pathlib import Path -from typing import Any +from pyaegis._aegis import ffi, lib -from cffi import FFI +__all__ = ["ffi", "lib"] -__ALL__ = ["ffi", "lib"] - - -def _platform_lib_name() -> str: - if sys.platform == "darwin": - return "libaegis.dylib" - if os.name == "nt": - return "aegis.dll" - return "libaegis.so" - - -def _load_libaegis(): - pkg_dir = Path(__file__).parent - candidate = pkg_dir / "build" / _platform_lib_name() - if candidate.exists(): - try: - return ffi.dlopen(str(candidate)) - except Exception as e: - raise OSError(f"Failed to load libaegis from {candidate}: {e}") - else: - raise OSError(f"Could not find libaegis at {candidate}") - - -ffi = FFI() -ffi.cdef(Path(__file__).with_name("aegis_cdef.h").read_text(encoding="utf-8")) -lib: Any = _load_libaegis() lib.aegis_init() diff --git a/pyaegis/aegis_cdef.h b/pyaegis/aegis_cdef.h index 9c5b6c4..88f4760 100644 --- a/pyaegis/aegis_cdef.h +++ b/pyaegis/aegis_cdef.h @@ -9,8 +9,8 @@ int aegis_verify_16(const uint8_t *x, const uint8_t *y) ; int aegis_verify_32(const uint8_t *x, const uint8_t *y) ; /* aegis128l.h */ -typedef struct aegis128l_state { uint8_t opaque[256]; } aegis128l_state; -typedef struct aegis128l_mac_state { uint8_t opaque[384]; } aegis128l_mac_state; +typedef struct aegis128l_state { ...; } aegis128l_state; +typedef struct aegis128l_mac_state { ...; } aegis128l_mac_state; size_t aegis128l_keybytes(void); size_t aegis128l_npubbytes(void); size_t aegis128l_abytes_min(void); @@ -103,8 +103,8 @@ void aegis128l_mac_reset(aegis128l_mac_state *st_); void aegis128l_mac_state_clone(aegis128l_mac_state *dst, const aegis128l_mac_state *src); /* aegis128x2.h */ -typedef struct aegis128x2_state { uint8_t opaque[448]; } aegis128x2_state; -typedef struct aegis128x2_mac_state { uint8_t opaque[704]; } aegis128x2_mac_state; +typedef struct aegis128x2_state { ...; } aegis128x2_state; +typedef struct aegis128x2_mac_state { ...; } aegis128x2_mac_state; size_t aegis128x2_keybytes(void); size_t aegis128x2_npubbytes(void); size_t aegis128x2_abytes_min(void); @@ -197,8 +197,8 @@ void aegis128x2_mac_reset(aegis128x2_mac_state *st_); void aegis128x2_mac_state_clone(aegis128x2_mac_state *dst, const aegis128x2_mac_state *src); /* aegis128x4.h */ -typedef struct aegis128x4_state { uint8_t opaque[832]; } aegis128x4_state; -typedef struct aegis128x4_mac_state { uint8_t opaque[1344]; } aegis128x4_mac_state; +typedef struct aegis128x4_state { ...; } aegis128x4_state; +typedef struct aegis128x4_mac_state { ...; } aegis128x4_mac_state; size_t aegis128x4_keybytes(void); size_t aegis128x4_npubbytes(void); size_t aegis128x4_abytes_min(void); @@ -291,8 +291,8 @@ void aegis128x4_mac_reset(aegis128x4_mac_state *st_); void aegis128x4_mac_state_clone(aegis128x4_mac_state *dst, const aegis128x4_mac_state *src); /* aegis256.h */ -typedef struct aegis256_state { uint8_t opaque[192]; } aegis256_state; -typedef struct aegis256_mac_state { uint8_t opaque[288]; } aegis256_mac_state; +typedef struct aegis256_state { ...; } aegis256_state; +typedef struct aegis256_mac_state { ...; } aegis256_mac_state; size_t aegis256_keybytes(void); size_t aegis256_npubbytes(void); size_t aegis256_abytes_min(void); @@ -385,8 +385,8 @@ void aegis256_mac_reset(aegis256_mac_state *st_); void aegis256_mac_state_clone(aegis256_mac_state *dst, const aegis256_mac_state *src); /* aegis256x2.h */ -typedef struct aegis256x2_state { uint8_t opaque[320]; } aegis256x2_state; -typedef struct aegis256x2_mac_state { uint8_t opaque[512]; } aegis256x2_mac_state; +typedef struct aegis256x2_state { ...; } aegis256x2_state; +typedef struct aegis256x2_mac_state { ...; } aegis256x2_mac_state; size_t aegis256x2_keybytes(void); size_t aegis256x2_npubbytes(void); size_t aegis256x2_abytes_min(void); @@ -479,8 +479,8 @@ void aegis256x2_mac_reset(aegis256x2_mac_state *st_); void aegis256x2_mac_state_clone(aegis256x2_mac_state *dst, const aegis256x2_mac_state *src); /* aegis256x4.h */ -typedef struct aegis256x4_state { uint8_t opaque[576]; } aegis256x4_state; -typedef struct aegis256x4_mac_state { uint8_t opaque[960]; } aegis256x4_mac_state; +typedef struct aegis256x4_state { ...; } aegis256x4_state; +typedef struct aegis256x4_mac_state { ...; } aegis256x4_mac_state; size_t aegis256x4_keybytes(void); size_t aegis256x4_npubbytes(void); size_t aegis256x4_abytes_min(void); diff --git a/pyproject.toml b/pyproject.toml index b0956b4..5329f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] -requires = ["hatchling", "cffi>=2.0.0"] -build-backend = "hatchling.build" +requires = ["setuptools>=61.0", "cffi>=2.0.0"] +build-backend = "setuptools.build_meta" [project] name = "pyaegis" version = "0.1.0" -description = "Python bindings for libaegis (links to system library)" +description = "Python bindings for libaegis (links to static library)" requires-python = ">=3.12" classifiers = [ "Programming Language :: Python :: 3", @@ -27,10 +27,8 @@ dev = [ "pytest>=8.4.2", ] -[tool.hatch.build.hooks.custom] -# Placeholder build hook for compiling libaegis with Zig during wheel builds. -# The actual Zig build is intentionally not executed yet. -path = "tools/build_hook.py" - -[tool.hatch.build.targets.wheel] +[tool.setuptools] packages = ["pyaegis"] + +[tool.setuptools.package-data] +pyaegis = ["*.h", "*.so", "*.pyd"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ce0df56 --- /dev/null +++ b/setup.py @@ -0,0 +1,65 @@ +"""Setup script for pyaegis - builds CFFI extension linking to libaegis.a""" + +from pathlib import Path + +from cffi import FFI +from setuptools import setup + +# Read the CDEF header +cdef_path = Path(__file__).parent / "pyaegis" / "aegis_cdef.h" +cdef_content = cdef_path.read_text(encoding="utf-8") + +# Create CFFI builder +ffibuilder = FFI() +ffibuilder.cdef(cdef_content) + +# Locate libaegis.a - check common locations +libaegis_paths = [ + Path("libaegis/zig-out/lib/libaegis.a"), # Zig build output (repo build) + Path("libaegis/build/libaegis.a"), # CMake build output (repo build) + Path("../libaegis/zig-out/lib/libaegis.a"), # When building from extracted sdist + Path("../libaegis/build/libaegis.a"), # When building from extracted sdist + Path("/usr/local/lib/libaegis.a"), # System install + Path("/usr/lib/libaegis.a"), # System install +] + +libaegis_static = None +for path in libaegis_paths: + if path.exists(): + libaegis_static = path.resolve() + print(f"Found libaegis.a at: {libaegis_static}") + break + +if not libaegis_static: + raise FileNotFoundError( + "libaegis.a not found. Please build libaegis first. " + f"Searched: {[str(p) for p in libaegis_paths]}" + ) + +# Include directory for headers +include_dirs = [] +libaegis_include = Path("libaegis/src/include") +if libaegis_include.exists(): + include_dirs.append(str(libaegis_include.resolve())) + +# Set the source - we don't need any C source files since we're linking to the static library +# CFFI will generate the wrapper code automatically +ffibuilder.set_source( + "pyaegis._aegis", # module name + """ + #include "aegis.h" + #include "aegis128l.h" + #include "aegis128x2.h" + #include "aegis128x4.h" + #include "aegis256.h" + #include "aegis256x2.h" + #include "aegis256x4.h" + """, + include_dirs=include_dirs, + extra_objects=[str(libaegis_static)], # Link against the static library +) + +if __name__ == "__main__": + setup( + cffi_modules=["setup.py:ffibuilder"], + ) diff --git a/tools/build_hook.py b/tools/build_hook.py deleted file mode 100644 index 5818f21..0000000 --- a/tools/build_hook.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Hatch build hook for building dynamic libaegis library using Zig.""" - -import shutil -import subprocess -from pathlib import Path - -from hatchling.builders.hooks.plugin.interface import BuildHookInterface - - -class BuildHook(BuildHookInterface): - """Build dynamic library with Zig and include in wheel.""" - - PLUGIN_NAME = "pyaegis_build_hook" - - def initialize(self, version: str, build_data: dict) -> None: - """Build library with Zig and add it to the wheel.""" - super().initialize(version, build_data) - if self.target_name != "wheel": - return - - if not shutil.which("zig"): - raise RuntimeError("Zig compiler not found in PATH") - - libaegis_dir = Path(self.root) / "libaegis" - self.app.display_info(f"[aegis] Using libaegis source at: {libaegis_dir}") - original_build_zig = libaegis_dir / "build.zig" - if not original_build_zig.exists(): - raise RuntimeError(f"libaegis source not found at {libaegis_dir}") - - try: - build_dir = Path("libaegis-build") - build_dir.mkdir(exist_ok=True) - build_zig = build_dir / "build.zig" - build_zig.write_text( - original_build_zig.read_text(encoding="utf-8").replace( - ".linkage = .static,", ".linkage = .dynamic," - ), - encoding="utf-8", - ) - for res in ("build.zig.zon", "src"): - (build_dir / res).symlink_to(libaegis_dir / res) - self.app.display_info( - "[aegis] Building libaegis dynamic library with Zig..." - ) - try: - subprocess.run( - ["zig", "build", "-Drelease"], - check=True, - cwd=str(build_dir), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - except subprocess.CalledProcessError as e: - output = e.stdout.decode(errors="replace") if e.stdout else "" - raise RuntimeError(f"Zig build failed:\n{output}") from e - - lib_dir = build_dir / "zig-out" / "lib" - - dynamic_lib = None - for lib_file in lib_dir.iterdir(): - if lib_file.name.startswith("libaegis") and lib_file.suffix in ( - ".so", - ".dylib", - ".dll", - ): - dynamic_lib = lib_file - break - - if not dynamic_lib or not dynamic_lib.exists(): - raise RuntimeError(f"Built dynamic library not found in {lib_dir}") - - # Copy the built dynamic library into the Python package tree so that it - # is naturally included as package data. Hatch will pick up anything - # under the listed packages ("pyaegis"), so a direct copy is simpler - # than relying on force_include. We still leave the original artifact - # in place in case other hooks/tools want to inspect it. - package_build_dir = Path(self.root) / "pyaegis" / "build" - package_build_dir.mkdir(parents=True, exist_ok=True) - self.app.display_info( - f"[aegis] Staging dynamic library to package... {package_build_dir} {Path.cwd()}" - ) - dest_path = package_build_dir / dynamic_lib.name - try: - shutil.copy2(dynamic_lib, dest_path) - except Exception as e: # pragma: no cover - defensive - raise RuntimeError( - f"Failed to copy dynamic library to package: {e}" - ) from e - finally: - shutil.rmtree(build_dir, ignore_errors=True) - - # Retain force_include as a fallback for environments where an older - # Hatch might not automatically include non-.py files, or if wheels are - # built with custom exclusion rules. - if "force_include" not in build_data: - build_data["force_include"] = {} - build_data["force_include"][str(dest_path)] = str( - Path("pyaegis") / "build" / dynamic_lib.name - ) - self.app.display_info(f"[aegis] Dynamic library staged at: {dest_path}") diff --git a/tools/gen_cdef.py b/tools/gen_cdef.py index 1373a5a..8c25a6d 100644 --- a/tools/gen_cdef.py +++ b/tools/gen_cdef.py @@ -34,8 +34,18 @@ def clean_declaration(text: str) -> str: if text == old: break - # Remove CRYPTO_ALIGN(...) - text = re.sub(r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)", "", text) + # For structs with CRYPTO_ALIGN, replace the field with "...;" to make it flexible + # This tells CFFI to use the C compiler's alignment instead of calculating it + if "CRYPTO_ALIGN" in text and "typedef struct" in text: + # Replace "CRYPTO_ALIGN(N) uint8_t opaque[SIZE];" with "...;" + text = re.sub( + r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)\s+uint8_t\s+opaque\[\d+\];", + "...;", + text + ) + else: + # For non-struct declarations, just remove CRYPTO_ALIGN + text = re.sub(r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)", "", text) # Normalize whitespace but preserve structure lines = []