Rename module to pyaegis, implement build with zig.

This commit is contained in:
Leo Vasanko
2025-11-06 10:47:54 -06:00
parent 8898cec50f
commit 42ddaac6bc
16 changed files with 354 additions and 494 deletions
+70
View File
@@ -0,0 +1,70 @@
"""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."""
def initialize(self, version: str, build_data: dict) -> None:
"""Build library with Zig and add it to the wheel."""
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"
original_build_zig = libaegis_dir / "build.zig"
if not original_build_zig.exists():
raise RuntimeError(f"libaegis source not found at {libaegis_dir}")
# Prepare a temporary build directory (avoid touching original files)
build_dir = Path.cwd() / "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}")
if "force_include" not in build_data:
build_data["force_include"] = {}
dest_rel = str(Path("build") / dynamic_lib.name)
build_data["force_include"][str(dynamic_lib)] = dest_rel
self.app.display_info(f"[aegis] Added dynamic library to wheel: {dest_rel}")
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Generate CFFI cdef string from libaegis headers.
This script parses the C header files and extracts function declarations,
typedefs, and struct definitions to generate the cdef() string needed by CFFI.
"""
import pathlib
import re
import sys
def preprocess_content(content: str) -> str:
"""Remove comments, preprocessor directives, and extern "C" blocks."""
# Remove multi-line comments
content = re.sub(r"/\*.*?\*/", " ", content, flags=re.DOTALL)
# Remove line comments
content = re.sub(r"//.*$", "", content, flags=re.MULTILINE)
# Remove preprocessor directives
content = re.sub(r"^\s*#.*$", "", content, flags=re.MULTILINE)
# Remove extern "C" blocks
content = re.sub(r'extern\s+"C"\s*\{', "", content)
content = re.sub(r"(?:^|\n)\s*\}\s*(?:\n|$)", "\n", content, flags=re.MULTILINE)
return content
def clean_declaration(text: str) -> str:
"""Clean up a C declaration for CFFI consumption."""
# Remove __attribute__(...) with proper nesting
while "__attribute__" in text:
old = text
text = re.sub(r"__attribute__\s*\(\([^()]*\)\)", "", text)
if text == old:
break
# Remove CRYPTO_ALIGN(...)
text = re.sub(r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)", "", text)
# Normalize whitespace but preserve structure
lines = []
for line in text.split("\n"):
line = re.sub(r"\s+", " ", line).strip()
if line:
lines.append(line)
return " ".join(lines)
def extract_declarations(header_path: pathlib.Path) -> list[str]:
"""Extract function declarations and typedefs from a header file."""
content = header_path.read_text(encoding="utf-8")
content = preprocess_content(content)
declarations = []
# Extract typedefs (including structs)
typedef_pattern = r"typedef\s+struct\s+\w+\s*\{[^}]+\}\s*\w+\s*;"
for match in re.finditer(typedef_pattern, content, re.DOTALL):
decl = clean_declaration(match.group(0))
if decl:
declarations.append(decl)
# Extract function declarations - more permissive pattern
func_pattern = r"((?:const\s+)?(?:int|void|size_t)\s+\w+\s*\([^;]+?\)\s*;)"
for match in re.finditer(func_pattern, content, re.DOTALL):
decl = clean_declaration(match.group(0))
if decl and "aegis" in decl.lower():
declarations.append(decl)
return declarations
def format_declaration(decl: str, max_width: int = 100) -> str:
"""Format a declaration for readability, with intelligent line breaking."""
# If it's short enough, return as-is
if len(decl) <= max_width:
return decl
# For function declarations, try to break at parameter boundaries
if "(" in decl and ")" in decl:
# Find the function name and opening paren
match = re.match(r"(.*?\s+\w+\s*)\((.*)\)(.*)", decl)
if match:
prefix, params, suffix = match.groups()
# Break parameters if they're too long
if len(prefix) + len(params) + 2 > max_width:
# Split parameters
param_list = [p.strip() for p in params.split(",")]
if len(param_list) > 1:
formatted_params = (",\n" + " " * (len(prefix) + 1)).join(
param_list
)
return f"{prefix}({formatted_params}){suffix}"
return decl
def generate_cdef(include_dir: pathlib.Path) -> str:
"""Generate the complete CFFI cdef string from all aegis headers."""
lines = [
"typedef unsigned char uint8_t;",
"typedef unsigned long size_t;",
"",
]
# Header files in order, skipping aegis.h as it might be included elsewhere
headers = [
"aegis.h",
"aegis128l.h",
"aegis128x2.h",
"aegis128x4.h",
"aegis256.h",
"aegis256x2.h",
"aegis256x4.h",
]
for header_name in headers:
header_path = include_dir / header_name
if not header_path.exists():
print(f"Warning: {header_name} not found", file=sys.stderr)
continue
lines.append(f"/* {header_name} */")
declarations = extract_declarations(header_path)
for decl in declarations:
formatted = format_declaration(decl)
lines.append(formatted)
lines.append("")
# Add libc bits for aligned allocation
lines.extend(
[
"/* libc bits for aligned allocation on POSIX */",
"int posix_memalign(void **memptr, size_t alignment, size_t size);",
"void free(void *ptr);",
]
)
return "\n".join(lines)
def main() -> int:
# Find the include directory
root = pathlib.Path(__file__).resolve().parents[2]
include_dir = root / "src" / "include"
if not include_dir.exists():
print(f"Include directory not found: {include_dir}", file=sys.stderr)
return 1
cdef_string = generate_cdef(include_dir)
# Write to a file in the pyaegis/build subdirectory
output_dir = root / "python" / "pyaegis" / "build"
output_dir.mkdir(exist_ok=True)
output_path = output_dir / "aegis_cdef.h"
output_path.write_text(cdef_string, encoding="utf-8")
print(f"Generated: {output_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())