128 lines
3.6 KiB
Bash
Executable File
128 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# TODO this is a complete vibe-coded hack, but the header patching at least is crucial
|
|
|
|
# Generate Haskell FFI bindings from Rust source code.
|
|
#
|
|
# Pipeline: cargo build -> cbindgen -> patch header -> hs-bindgen
|
|
#
|
|
# Prerequisites: run inside the Nix dev shell (provides gcc, cabal, cbindgen, hs-bindgen-cli).
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
RUST_DIR="$SCRIPT_DIR/rust"
|
|
HASKELL_DIR="$SCRIPT_DIR/haskell"
|
|
HEADER_NAME="garnet_rs.h"
|
|
HEADER="$RUST_DIR/$HEADER_NAME"
|
|
|
|
# --- Step 1: Build Rust static library ---
|
|
echo "=== Building Rust library ==="
|
|
cargo build --manifest-path "$RUST_DIR/Cargo.toml"
|
|
|
|
# --- Step 2: Generate C header with cbindgen ---
|
|
echo "=== Running cbindgen ==="
|
|
cbindgen \
|
|
--lang c \
|
|
--crate garnet-rs \
|
|
--output "$HEADER" \
|
|
"$RUST_DIR"
|
|
|
|
echo " Raw header written to $HEADER"
|
|
|
|
# --- Step 3: Patch the header for hs-bindgen compatibility ---
|
|
echo "=== Patching header ==="
|
|
|
|
# Two patches are needed:
|
|
#
|
|
# Patch 1: Replace cbindgen's enum pattern with typedef + #defines.
|
|
# cbindgen emits: enum Shape_Tag { Circle, Rectangle, }; typedef uint8_t Shape_Tag;
|
|
# hs-bindgen needs: typedef uint8_t Shape_Tag; #define Circle 0 ...
|
|
#
|
|
# Patch 2: Name anonymous unions inside structs.
|
|
# cbindgen emits: union { ... };
|
|
# hs-bindgen needs: union { ... } body;
|
|
|
|
awk '
|
|
# State machine for enum->typedef+define transformation
|
|
/^enum [A-Za-z_][A-Za-z0-9_]* \{$/ {
|
|
in_enum = 1
|
|
enum_name = $2
|
|
variant_count = 0
|
|
delete variants
|
|
next
|
|
}
|
|
in_enum && /^\};$/ {
|
|
# Next line should be: typedef <type> <enum_name>;
|
|
in_enum = 0
|
|
pending_enum = 1
|
|
next
|
|
}
|
|
in_enum {
|
|
# Collect variant names (strip trailing comma and whitespace)
|
|
v = $0
|
|
gsub(/^[[:space:]]+/, "", v)
|
|
gsub(/,[[:space:]]*$/, "", v)
|
|
if (v != "") {
|
|
variants[variant_count] = v
|
|
variant_count++
|
|
}
|
|
next
|
|
}
|
|
pending_enum && /^typedef [A-Za-z0-9_]+ / {
|
|
# Emit: typedef <type> <name>; then #define for each variant
|
|
print $0
|
|
for (i = 0; i < variant_count; i++) {
|
|
printf "#define %s %d\n", variants[i], i
|
|
}
|
|
pending_enum = 0
|
|
next
|
|
}
|
|
|
|
# Name anonymous unions: }; at end of union block inside struct -> } body;
|
|
/^ \};$/ && saw_union {
|
|
print " } body;"
|
|
saw_union = 0
|
|
next
|
|
}
|
|
/^ union \{$/ {
|
|
saw_union = 1
|
|
}
|
|
|
|
{ print }
|
|
' "$HEADER" > "${HEADER}.tmp" && mv "${HEADER}.tmp" "$HEADER"
|
|
|
|
echo " Patched header at $HEADER"
|
|
|
|
# --- Step 4: Derive system include paths for hs-bindgen's libclang ---
|
|
echo "=== Detecting system include paths ==="
|
|
|
|
# hs-bindgen uses libclang directly, which doesn't know about NixOS's
|
|
# non-standard include locations. We extract them from cpp -v and pass
|
|
# all of them — extra paths are harmless.
|
|
CLANG_OPTIONS=()
|
|
while IFS= read -r dir; do
|
|
CLANG_OPTIONS+=("--clang-option" "-isystem$dir")
|
|
done < <(echo | cpp -v 2>&1 | awk '/#include <\.\.\.> search starts here:/{f=1;next}/End of search list/{f=0}f{gsub(/^ +/,"");print}')
|
|
|
|
if [ ${#CLANG_OPTIONS[@]} -eq 0 ]; then
|
|
echo " WARNING: No system include paths detected. hs-bindgen may fail."
|
|
else
|
|
echo " Found ${#CLANG_OPTIONS[@]} clang options:"
|
|
for ((i=0; i<${#CLANG_OPTIONS[@]}; i+=2)); do
|
|
echo " ${CLANG_OPTIONS[i+1]}"
|
|
done
|
|
fi
|
|
|
|
# --- Step 5: Run hs-bindgen ---
|
|
echo "=== Running hs-bindgen ==="
|
|
hs-bindgen-cli preprocess \
|
|
--overwrite-files --create-output-dirs \
|
|
--unique-id com.garnet --enable-record-dot \
|
|
--hs-output-dir "$HASKELL_DIR/generated" --module GarnetRs \
|
|
"${CLANG_OPTIONS[@]}" \
|
|
-I "$RUST_DIR" "$HEADER_NAME"
|
|
|
|
echo "=== Done ==="
|
|
echo "Generated Haskell bindings in $HASKELL_DIR/generated/"
|
|
echo "Run 'cabal run garnet' to test."
|