Switch from hatch to setuptools/CFFI build to produce wheels correctly.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
6
MANIFEST.in
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
65
setup.py
Normal 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"],
|
||||
)
|
||||
@@ -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}")
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user