Switch from hatch to setuptools/CFFI build to produce wheels correctly.

This commit is contained in:
Leo Vasanko
2025-11-06 16:34:32 -06:00
parent fd24bb02f8
commit f8cc02eb41
8 changed files with 108 additions and 155 deletions

3
.gitignore vendored
View File

@@ -3,6 +3,9 @@
*.egg-info
/dist
/pyaegis/build
/pyaegis/_aegis.*.so
/pyaegis/_aegis.*.pyd
/pyaegis/_aegis.c
__pycache__
!.gitignore
!.gitmodules

6
MANIFEST.in Normal file
View File

@@ -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

View File

@@ -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()

View File

@@ -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);

View File

@@ -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"]

65
setup.py Normal file
View File

@@ -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"],
)

View File

@@ -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}")

View File

@@ -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 = []