build: portable `make binary` target for FreeBSD/Linux/macOS

ober

6d8b4d12cfa27e9ecab0c82ea7cc6fcacff1959f

diff --git a/.gitignore b/.gitignore
index 27db667..28e238e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,11 @@
 \#*
 .#*
 
+# `make binary` outputs
+/jerboa-bin
+/jerboa-bin-main.c
+/jerboa-bin.wp.so
+
 # Rust build artifacts
 jerboa-native-rs/target/
 
diff --git a/Makefile b/Makefile
index 822dc0b..a39bf7c 100644
--- a/Makefile
+++ b/Makefile
@@ -7,13 +7,14 @@ CHEZ_EXT_LIBDIRS = $(CHEZ_EXT_DIR)/chez-https/src:$(CHEZ_EXT_DIR)/chez-ssl/src:$
 # Shared object paths for legacy FFI-based chez-* libraries
 CHEZ_EXT_LDPATH = $(CHEZ_EXT_DIR)/chez-ssl:$(CHEZ_EXT_DIR)/chez-zlib:$(CHEZ_EXT_DIR)/chez-pcre2:$(CHEZ_EXT_DIR)/chez-leveldb:$(CHEZ_EXT_DIR)/chez-epoll:$(CHEZ_EXT_DIR)/chez-inotify:$(CHEZ_EXT_DIR)/chez-crypto:$(CHEZ_EXT_DIR)/chez-sqlite:$(CHEZ_EXT_DIR)/chez-postgresql
 
-.PHONY: help build test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-native test-gaps native clean-native audit-native clean fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-websocket-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push
+.PHONY: help build binary test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-native test-gaps native clean-native audit-native clean fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-websocket-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push
 
 help:
 	@echo "Usage: make <target>"
 	@echo ""
 	@echo "Build:"
 	@echo "  build            Compile all Jerboa libraries"
+	@echo "  binary           Build a self-contained jerboa-bin binary (FreeBSD/Linux/macOS)"
 	@echo "  native           Build Rust native library"
 	@echo "  clean            Remove compiled .so and .wpo artifacts"
 	@echo "  clean-native     Remove Rust build artifacts"
@@ -71,6 +72,14 @@ help:
 build:
 	$(SCHEME) --libdirs $(LIBDIRS) --script support/build.ss
 
+# Build a self-contained Jerboa binary that bundles petite.boot, scheme.boot,
+# and a WPO-compiled entry program. Output: ./jerboa-bin
+# Override entry script with BINARY_ENTRY=path/to/script.ss
+BINARY_ENTRY ?= support/binary-entry.ss
+BINARY_OUTPUT ?= jerboa-bin
+binary: build
+	SCHEME=$(SCHEME) support/build-binary.sh $(BINARY_ENTRY) $(BINARY_OUTPUT)
+
 test: test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-regex-all
 
 check-docs:
diff --git a/support/binary-entry.ss b/support/binary-entry.ss
new file mode 100644
index 0000000..700fd87
--- /dev/null
+++ b/support/binary-entry.ss
@@ -0,0 +1,43 @@
+#!chezscheme
+;;; binary-entry.ss — Default entry point compiled into `make binary` output.
+;;;
+;;; With no args: starts the Jerboa REPL.
+;;; With one arg ending in .ss: loads that script.
+;;; --version / -v: prints version.
+
+(import (except (chezscheme) make-hash-table hash-table? sort sort! printf fprintf
+                 path-extension path-absolute? with-input-from-string
+                 with-output-to-string iota 1+ 1- partition make-date make-time
+                 meta atom?)
+        (jerboa prelude))
+
+(let ([args (cdr (command-line))])
+  (cond
+    [(null? args)
+     (displayln "Jerboa Scheme — type (exit) to quit")
+     (let loop ()
+       (display "jerboa> ")
+       (flush-output-port (current-output-port))
+       (let ([form (read)])
+         (cond
+           [(eof-object? form) (newline)]
+           [else
+            (guard (exn [#t (display "Error: ")
+                            (display-condition exn)
+                            (newline)])
+              (let ([result (eval form (interaction-environment))])
+                (unless (eq? result (void))
+                  (write result) (newline))))
+            (loop)])))]
+    [(or (string=? (car args) "--version")
+         (string=? (car args) "-v"))
+     (displayln "jerboa-bin 0.1.0")]
+    [(or (string=? (car args) "--help")
+         (string=? (car args) "-h"))
+     (displayln "Usage: jerboa-bin [<script.ss> | --version | --help]")
+     (displayln "  no args         start REPL")
+     (displayln "  <script.ss>     load and run a Scheme script")
+     (displayln "  --version, -v   print version")
+     (displayln "  --help, -h      print this help")]
+    [else
+     (load (car args))]))
diff --git a/support/build-binary.sh b/support/build-binary.sh
new file mode 100755
index 0000000..8a175b8
--- /dev/null
+++ b/support/build-binary.sh
@@ -0,0 +1,181 @@
+#!/bin/sh
+# build-binary.sh — Build a self-contained Jerboa binary.
+#
+# Usage:
+#   support/build-binary.sh <entry.ss> <output-name>
+#
+# Portable across FreeBSD, Linux, and macOS — uses the system Chez install
+# (libkernel.a + petite.boot + scheme.boot) discovered by globbing standard
+# prefixes for csv*/<machine-type>/.
+#
+# Output: ./<output-name>  (a real ELF/Mach-O binary that runs Jerboa)
+
+set -eu
+
+if [ $# -lt 2 ]; then
+    echo "Usage: $0 <entry.ss> <output-name>" >&2
+    exit 1
+fi
+
+SCRIPT="$1"
+OUTPUT="$2"
+
+JERBOA_HOME="${JERBOA_HOME:-$(cd "$(dirname "$0")/.." && pwd)}"
+SCHEME="${SCHEME:-scheme}"
+LIBDIRS="${JERBOA_HOME}/lib"
+
+# ── Detect OS and pick compiler + base link flags ────────────────────────────
+OS=$(uname -s)
+case "$OS" in
+    FreeBSD)
+        CC_DEFAULT=cc
+        OS_LIBS="-lm -lpthread -lncurses -L/usr/local/lib -liconv"
+        ;;
+    Darwin)
+        CC_DEFAULT=cc
+        OS_LIBS="-lm -lpthread -lncurses -liconv"
+        ;;
+    Linux)
+        CC_DEFAULT=gcc
+        # Try both -lncurses and -ltinfo; -ltinfo is split out on some distros.
+        if [ -f /usr/lib/x86_64-linux-gnu/libtinfo.so ] || [ -f /usr/lib64/libtinfo.so ]; then
+            OS_LIBS="-lm -ldl -lpthread -ltinfo"
+        else
+            OS_LIBS="-lm -ldl -lpthread -lncurses"
+        fi
+        ;;
+    *)
+        CC_DEFAULT=cc
+        OS_LIBS="-lm -lpthread -lncurses"
+        ;;
+esac
+CC="${CC:-$CC_DEFAULT}"
+
+# ── Find the Chez install dir (libkernel.a + scheme.h + boot files) ──────────
+MACHINE_TYPE=$("$SCHEME" -q <<'EOF'
+(display (machine-type)) (exit)
+EOF
+)
+
+CSV_DIR=""
+for prefix in /usr/local/lib /usr/lib /usr/lib64 /opt/homebrew/lib /opt/local/lib; do
+    for d in "$prefix"/csv*/"$MACHINE_TYPE"; do
+        if [ -f "$d/libkernel.a" ] && [ -f "$d/scheme.h" ] && [ -f "$d/petite.boot" ]; then
+            CSV_DIR="$d"
+            break 2
+        fi
+    done
+done
+
+if [ -z "$CSV_DIR" ]; then
+    echo "ERROR: cannot find Chez install for machine-type '$MACHINE_TYPE'." >&2
+    echo "       Searched csv*/$MACHINE_TYPE under:" >&2
+    echo "         /usr/local/lib /usr/lib /usr/lib64 /opt/homebrew/lib /opt/local/lib" >&2
+    exit 1
+fi
+
+echo "=== Jerboa binary build: $SCRIPT -> $OUTPUT ==="
+echo "    OS:    $OS"
+echo "    CC:    $CC"
+echo "    Chez:  $CSV_DIR"
+echo ""
+
+# ── Step 1: WPO-compile entry script -> program.so ───────────────────────────
+WPO_SO="${OUTPUT}.wp.so"
+OBJ_DIR=$(mktemp -d "/tmp/jerboa-bin-obj.XXXXXX")
+trap 'rm -rf "$OBJ_DIR" "$WPO_SO" petite_boot.h scheme_boot.h program_boot.h "${OUTPUT}-main.c"' EXIT
+
+echo "==> [1/4] WPO compile"
+"$SCHEME" --libdirs "$LIBDIRS" \
+    --script "$JERBOA_HOME/support/build-boot.ss" "$SCRIPT" "$WPO_SO" "$OBJ_DIR"
+echo ""
+
+# ── Step 2: Embed boot files + program as C byte arrays ──────────────────────
+echo "==> [2/4] Embedding boot files as C arrays"
+
+embed() {
+    in="$1"; stem="$2"
+    var="${stem}_data"; sz="${stem}_size"; out="$stem.h"
+    printf 'static const unsigned char %s[] = {\n' "$var" > "$out"
+    # macOS `od` emits a trailing blank line; drop it before the substitution
+    # so we don't generate a stray "0x," at the end.
+    od -An -tx1 -v "$in" \
+        | sed -e '/^[[:space:]]*$/d' \
+              -e 's/^ *//;s/ *$//;s/  */ /g;s/ /,0x/g;s/^/0x/;s/$/,/' >> "$out"
+    printf '};\nstatic const unsigned int %s = sizeof(%s);\n' "$sz" "$var" >> "$out"
+}
+
+embed "$CSV_DIR/petite.boot"  petite_boot
+embed "$CSV_DIR/scheme.boot"  scheme_boot
+embed "$WPO_SO"               program_boot
+echo ""
+
+# ── Step 3: Generate main.c ──────────────────────────────────────────────────
+echo "==> [3/4] Generate ${OUTPUT}-main.c"
+
+cat > "${OUTPUT}-main.c" <<'CMAIN'
+/* Jerboa binary entry point — generated by support/build-binary.sh */
+#include "scheme.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+
+#include "petite_boot.h"
+#include "scheme_boot.h"
+#include "program_boot.h"
+
+static const char *write_program_tmpfile(void) {
+    static char path[] = "/tmp/jerboa-prog-XXXXXX";
+    int fd = mkstemp(path);
+    if (fd < 0) { perror("mkstemp"); exit(1); }
+    ssize_t n = write(fd, program_boot_data, program_boot_size);
+    if (n != (ssize_t)program_boot_size) {
+        perror("write"); close(fd); unlink(path); exit(1);
+    }
+    close(fd);
+    return path;
+}
+
+int main(int argc, const char *argv[]) {
+    Sscheme_init(NULL);
+    Sregister_boot_file_bytes("petite",
+        (void *)petite_boot_data, petite_boot_size);
+    Sregister_boot_file_bytes("scheme",
+        (void *)scheme_boot_data, scheme_boot_size);
+    Sbuild_heap(NULL, NULL);
+
+    const char *prog_path = write_program_tmpfile();
+    Sscheme_program(prog_path, argc, argv);
+    unlink(prog_path);
+
+    Sscheme_deinit();
+    return 0;
+}
+CMAIN
+echo ""
+
+# ── Step 4: Compile + link ───────────────────────────────────────────────────
+echo "==> [4/4] Compile + link -> $OUTPUT"
+
+EXTRA_ARCHIVES=""
+for a in liblz4.a libz.a; do
+    if [ -f "$CSV_DIR/$a" ]; then
+        EXTRA_ARCHIVES="$EXTRA_ARCHIVES $CSV_DIR/$a"
+    fi
+done
+
+# shellcheck disable=SC2086
+$CC -I"$CSV_DIR" -O2 \
+    -o "$OUTPUT" \
+    "${OUTPUT}-main.c" \
+    "$CSV_DIR/libkernel.a" \
+    $EXTRA_ARCHIVES \
+    $OS_LIBS
+
+echo ""
+echo "=== Build complete ==="
+ls -lh "$OUTPUT"
+file "$OUTPUT" 2>/dev/null || true