Add static binary build infrastructure

FreeBSD User <Jaime Fournier>

2f08031aefad29cadd21fddd72d3286a78a9d25e

diff --git a/.gitignore b/.gitignore
index edcc946..42ca438 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,11 @@
 
 # CDB data files
 *.cdb
+
+# Static build artifacts
+static/*.boot
+static/*.wp.so
+static/*.h
+static/jdns
+static/jdns-data
+static/jdns-debug
diff --git a/static/build-boot.ss b/static/build-boot.ss
new file mode 100644
index 0000000..273d839
--- /dev/null
+++ b/static/build-boot.ss
@@ -0,0 +1,44 @@
+#!chezscheme
+;;; Build compiled program for static embedding.
+;;;
+;;; Usage: scheme --libdirs "lib:$JERBOA" --script static/build-boot.ss <entry.ss> <output.so>
+;;;
+;;; This compiles the entry point with whole-program optimization,
+;;; producing a single .so file that can be loaded via Sscheme_program.
+
+(import (chezscheme))
+
+(unless (>= (length (command-line)) 3)
+  (display "Usage: build-boot.ss <entry.ss> <output.so>\n" (current-error-port))
+  (exit 1))
+
+(define (string-suffix? str suffix)
+  (let ([slen (string-length str)]
+        [xlen (string-length suffix)])
+    (and (>= slen xlen)
+         (string=? (substring str (- slen xlen) slen) suffix))))
+
+(let ([entry-file (list-ref (command-line) 1)]
+      [output-so (list-ref (command-line) 2)])
+
+  ;; Step 1: Compile with WPO enabled
+  (compile-imported-libraries #t)
+  (generate-wpo-files #t)
+
+  ;; Chez strips the .ss extension for output: bin/jdns.ss -> bin/jdns.wpo
+  (let* ([base (if (or (string-suffix? entry-file ".ss")
+                       (string-suffix? entry-file ".scm"))
+                 (substring entry-file 0
+                   (- (string-length entry-file)
+                      (if (string-suffix? entry-file ".ss") 3 4)))
+                 entry-file)]
+         [wpo-file (string-append base ".wpo")])
+
+    (display (format "Compiling ~a...\n" entry-file) (current-error-port))
+    (compile-program entry-file)
+
+    ;; Step 2: Whole-program optimization — bundle all libraries into one .so
+    (display (format "Whole-program optimization -> ~a\n" output-so) (current-error-port))
+    (compile-whole-program wpo-file output-so)
+
+    (display "Done.\n" (current-error-port))))
diff --git a/static/build-static.sh b/static/build-static.sh
new file mode 100755
index 0000000..54cb073
--- /dev/null
+++ b/static/build-static.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+#
+# Build static jdns binaries.
+#
+# Produces:
+#   static/jdns       — DNS server (single static binary)
+#   static/jdns-data  — Zone compiler (single static binary)
+#
+# Each binary embeds the Chez Scheme runtime + boot files + compiled program.
+# No external .boot files or shared libraries needed.
+
+set -e
+
+SCHEME="${SCHEME:-scheme}"
+JERBOA="${JERBOA:-$HOME/jerboa/lib}"
+LIBDIRS="lib:${JERBOA}"
+# Find Chez Scheme installation directory
+CSVDIR=""
+for d in /usr/local/lib/csv*/"$($SCHEME -q <<'EOF'
+(display (machine-type)) (exit)
+EOF
+)" /usr/lib/csv*/"$($SCHEME -q <<'EOF'
+(display (machine-type)) (exit)
+EOF
+)"; do
+    if [ -f "$d/libkernel.a" ] 2>/dev/null; then
+        CSVDIR="$d"
+        break
+    fi
+done
+
+if [ ! -f "$CSVDIR/libkernel.a" ]; then
+    echo "Error: Cannot find Chez Scheme installation (libkernel.a)" >&2
+    echo "  Searched: $CSVDIR" >&2
+    exit 1
+fi
+
+echo "Chez Scheme: $CSVDIR"
+echo "Jerboa:      $JERBOA"
+
+STATIC_DIR="$(dirname "$0")"
+cd "$(dirname "$0")/.."
+
+# Step 1: Build libraries (ensure .wpo files exist)
+echo "==> Compiling libraries..."
+$SCHEME --libdirs "$LIBDIRS" --compile-imported-libraries --script build.ss
+
+# Step 2: Build each entry point
+build_binary() {
+    local name="$1"
+    local entry="$2"
+    local prog_so="static/${name}.wp.so"
+
+    echo "==> Building $name..."
+
+    # Compile with WPO into a single .so
+    $SCHEME --libdirs "$LIBDIRS" --script static/build-boot.ss "$entry" "$prog_so"
+
+    # Convert boot files and program to C headers
+    echo "  Converting to C headers..."
+    convert_to_header "$CSVDIR/petite.boot" "static/petite_boot"
+    convert_to_header "$CSVDIR/scheme.boot" "static/scheme_boot"
+    convert_to_header "$prog_so"            "static/program_boot"
+
+    # Compile and link static binary
+    echo "  Linking static binary..."
+    cc -DSCHEME_STATIC \
+        -I"$CSVDIR" \
+        -o "static/${name}" \
+        static/main.c \
+        "$CSVDIR/libkernel.a" \
+        "$CSVDIR/liblz4.a" \
+        "$CSVDIR/libz.a" \
+        -L/usr/local/lib \
+        -lm -lthr -liconv -lncursesw \
+        -static
+
+    strip "static/${name}"
+
+    echo "  static/${name}: $(ls -lh "static/${name}" | awk '{print $5}')"
+}
+
+convert_to_header() {
+    local input="$1"
+    local stem="$2"
+    local varname="$(basename "$stem")_data"
+    local sizename="$(basename "$stem")_size"
+    local header="${stem}.h"
+
+    # Convert binary file to C unsigned char array using od
+    printf "static const unsigned char %s[] = {\n" "$varname" > "$header"
+    od -An -tx1 -v "$input" | sed 's/^ *//;s/ *$//;s/  */ /g;s/ /,0x/g;s/^/0x/;s/$/,/' >> "$header"
+    printf "};\n" >> "$header"
+    printf "static const unsigned int %s = sizeof(%s);\n" "$sizename" "$varname" >> "$header"
+}
+
+build_binary "jdns"      "bin/jdns.ss"
+build_binary "jdns-data" "bin/jdns-data.ss"
+
+echo ""
+echo "Static binaries:"
+ls -lh static/jdns static/jdns-data
+echo ""
+echo "Verify (should show 'statically linked'):"
+file static/jdns static/jdns-data
diff --git a/static/main.c b/static/main.c
new file mode 100644
index 0000000..cc5cda2
--- /dev/null
+++ b/static/main.c
@@ -0,0 +1,134 @@
+/*
+ * jdns static binary entry point
+ *
+ * Embeds Chez Scheme boot files (petite.boot, scheme.boot) and
+ * the compiled program (.so) as binary blobs.
+ * Statically linked — no shared library dependencies.
+ *
+ * The program .so is written to a temp file and loaded via
+ * Sscheme_program, which sets command-line before executing.
+ */
+
+#include "scheme.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <dlfcn.h>
+
+/* Embedded boot file data — generated by build-static.sh */
+#include "petite_boot.h"
+#include "scheme_boot.h"
+#include "program_boot.h"
+
+/*
+ * Override dlopen/dlsym for static binary.
+ * Chez's load-shared-object calls dlopen, which may fail in a fully
+ * static binary. We return a dummy handle so it doesn't error out.
+ * All actual symbol resolution happens via Sforeign_symbol.
+ */
+static int _static_dummy;
+
+void *dlopen(const char *path, int mode) {
+    (void)path;
+    (void)mode;
+    return &_static_dummy;
+}
+
+void *dlsym(void *handle, const char *name) {
+    (void)handle;
+    (void)name;
+    return NULL;
+}
+
+int dlclose(void *handle) {
+    (void)handle;
+    return 0;
+}
+
+char *dlerror(void) {
+    return NULL;
+}
+
+/* Wrapper functions for htons/ntohs which may be macros */
+static unsigned short wrap_htons(unsigned short x) { return htons(x); }
+static unsigned short wrap_ntohs(unsigned short x) { return ntohs(x); }
+
+/*
+ * Called by Sbuild_heap after the Scheme heap is initialized
+ * but before boot files execute. Register FFI symbols here.
+ */
+static void custom_init(void) {
+    Sforeign_symbol("socket", (void *)socket);
+    Sforeign_symbol("bind", (void *)bind);
+    Sforeign_symbol("close", (void *)close);
+    Sforeign_symbol("recvfrom", (void *)recvfrom);
+    Sforeign_symbol("sendto", (void *)sendto);
+    Sforeign_symbol("setsockopt", (void *)setsockopt);
+    Sforeign_symbol("htons", (void *)wrap_htons);
+    Sforeign_symbol("ntohs", (void *)wrap_ntohs);
+    Sforeign_symbol("inet_pton", (void *)inet_pton);
+    Sforeign_symbol("inet_ntop", (void *)inet_ntop);
+    Sforeign_symbol("setuid", (void *)setuid);
+    Sforeign_symbol("setgid", (void *)setgid);
+    Sforeign_symbol("chdir", (void *)chdir);
+    Sforeign_symbol("chroot", (void *)chroot);
+#ifdef __FreeBSD__
+    {
+        extern int cap_rights_limit(int, const void *);
+        Sforeign_symbol("cap_rights_limit", (void *)cap_rights_limit);
+    }
+#endif
+}
+
+/*
+ * Write embedded program data to a temp file so Sscheme_program
+ * can load it. This lets Chez set command-line before execution.
+ */
+static const char *write_program_tempfile(void) {
+    static char path[] = "/tmp/jdns-prog-XXXXXX";
+    int fd;
+    ssize_t written;
+
+    fd = mkstemp(path);
+    if (fd < 0) {
+        perror("mkstemp");
+        exit(1);
+    }
+
+    written = write(fd, program_boot_data, program_boot_size);
+    if (written != (ssize_t)program_boot_size) {
+        perror("write");
+        close(fd);
+        unlink(path);
+        exit(1);
+    }
+
+    close(fd);
+    return path;
+}
+
+int main(int argc, const char *argv[]) {
+    const char *prog_path;
+
+    Sscheme_init(NULL);
+
+    /* Register boot files (petite + scheme only — not the program) */
+    Sregister_boot_file_bytes("petite", (void *)petite_boot_data, petite_boot_size);
+    Sregister_boot_file_bytes("scheme", (void *)scheme_boot_data, scheme_boot_size);
+
+    /* Build heap with FFI symbols registered in custom_init */
+    Sbuild_heap(NULL, custom_init);
+
+    /* Write compiled program to temp file, load via Sscheme_program
+     * which sets (command-line) before executing */
+    prog_path = write_program_tempfile();
+    Sscheme_program(prog_path, argc, argv);
+    unlink(prog_path);
+
+    Sscheme_deinit();
+    return 0;
+}