#!/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" 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 ; 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 ; 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' to test."