Add cross-build for FreeBSD/Linux amd64+arm64 from macOS arm64

Jaime Fournier <jaimef@linbsd.org>

f4754cd3f9986d354b20bfdde1336289c4500c65

diff --git a/.gitignore b/.gitignore
index 3b309bd..24a5a63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,13 +5,26 @@
 # CDB data files (generated from .data via bench/gen-zone.sh)
 *.cdb
 
-# Static build artifacts
-static/*.boot
-static/*.wp.so
-static/*.h
-static/jdns
-static/jdns-data
-static/jdns-debug
+# Cross-build artifacts (see static/build-jdns-*.ss)
+jdns-macos-arm64
+jdns-data-macos-arm64
+jdns-linux-amd64
+jdns-data-linux-amd64
+jdns-linux-arm64
+jdns-data-linux-arm64
+jdns-freebsd-amd64
+jdns-data-freebsd-amd64
+jdns*.wp.so
+jdns*-main.c
+petite_boot.h
+scheme_boot.h
+program_boot.h
+
+# Per-machine compiled-library caches Chez emits during cross-builds
+lib/**/*.tarm64osx
+lib/**/*.ta6le
+lib/**/*.ta6fb
+lib/**/*.tarm64le
 
 # Bench logs and server-stderr dumps (large, regenerable)
 bench/logs/
diff --git a/Makefile b/Makefile
index 183c050..697ad6e 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,9 @@ WASM_SOURCES = $(WASM_DIR)/Cargo.toml \
                $(WASM_DIR)/dns/Cargo.toml $(WASM_DIR)/dns/src/lib.rs \
                $(WASM_DIR)/cdb/Cargo.toml $(WASM_DIR)/cdb/src/lib.rs
 
-.PHONY: all build test clean jdns jdns-data wasm wasm-clean
+.PHONY: all build test clean jdns jdns-data wasm wasm-clean \
+        static static-macos static-linux static-linux-arm64 static-freebsd \
+        static-clean
 
 all: build
 
@@ -49,6 +51,38 @@ test: build
 	done
 	@echo "All tests passed."
 
+# Cross-builds. JERBOA_HOME must contain the cross-built Chez prefixes
+# (.chez-cross-ta6le for linux-amd64, .chez-cross-ta6fb for freebsd-amd64,
+# .chez-cross-tarm64le for linux-arm64) plus the matching xpatch under
+# build/chez/xc-*/s/xpatch. See ~/mine/jerboa for how those are built.
+#
+# Each target produces both jdns-<plat> and jdns-data-<plat> binaries.
+static: static-macos
+
+static-macos:
+	JERBOA_HOME=$(JERBOA_HOME) $(SCHEME) --libdirs "$(LIBDIRS)" \
+	    --script static/build-jdns-macos.ss
+
+static-linux:
+	JERBOA_HOME=$(JERBOA_HOME) $(SCHEME) --libdirs "$(LIBDIRS)" \
+	    --script static/build-jdns-linux.ss
+
+static-linux-arm64:
+	JERBOA_HOME=$(JERBOA_HOME) $(SCHEME) --libdirs "$(LIBDIRS)" \
+	    --script static/build-jdns-linux-arm64.ss
+
+static-freebsd:
+	JERBOA_HOME=$(JERBOA_HOME) $(SCHEME) --libdirs "$(LIBDIRS)" \
+	    --script static/build-jdns-freebsd.ss
+
+static-clean:
+	rm -f jdns-macos-arm64    jdns-data-macos-arm64
+	rm -f jdns-linux-amd64    jdns-data-linux-amd64
+	rm -f jdns-linux-arm64    jdns-data-linux-arm64
+	rm -f jdns-freebsd-amd64  jdns-data-freebsd-amd64
+	rm -f jdns*.wp.so jdns*-main.c
+	rm -f petite_boot.h scheme_boot.h program_boot.h
+
 clean:
 	find lib -name '*.so' -delete 2>/dev/null || true
 	find lib -name '*.wpo' -delete 2>/dev/null || true
diff --git a/static/build-boot.ss b/static/build-boot.ss
deleted file mode 100644
index 273d839..0000000
--- a/static/build-boot.ss
+++ /dev/null
@@ -1,44 +0,0 @@
-#!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-common.ss b/static/build-common.ss
new file mode 100644
index 0000000..cb3f5f7
--- /dev/null
+++ b/static/build-common.ss
@@ -0,0 +1,234 @@
+;;; build-common.ss — Shared helpers for static/build-jdns-*.ss.
+;;;
+;;; Loaded by each per-target build script via (load "static/build-common.ss").
+;;; Defines: embed-as-c-array, find-cross-csv-dir, jdns-posix-symbols,
+;;; jdns-posix-wrapped-symbols, jdns-wasm-weak-symbols, emit-jdns-main-c.
+;;;
+;;; Loaded BEFORE xpatch in cross builds so the helpers themselves don't
+;;; get compiled for the target machine type — they only run on the host.
+
+;; Embed a binary file as a pair of C symbols:
+;;   static const unsigned char <stem>_data[];
+;;   static const unsigned int  <stem>_size;
+(define (embed-as-c-array in-path stem out-path)
+  (let* ([bv (call-with-port (open-file-input-port in-path) get-bytevector-all)]
+         [n  (bytevector-length bv)]
+         [arr-name (string-append stem "_data")]
+         [sz-name  (string-append stem "_size")])
+    (call-with-port (open-file-output-port out-path
+                       (file-options no-fail)
+                       (buffer-mode block)
+                       (native-transcoder))
+      (lambda (out)
+        (fprintf out "static const unsigned char ~a[] = {\n" arr-name)
+        (let loop ([i 0])
+          (when (< i n)
+            (fprintf out "0x~2,'0x," (bytevector-u8-ref bv i))
+            (when (= (mod (+ i 1) 16) 0) (newline out))
+            (loop (+ i 1))))
+        (when (positive? n) (newline out))
+        (display "};\n" out)
+        (fprintf out "static const unsigned int ~a = sizeof(~a);\n"
+                 sz-name arr-name)))
+    (printf "    embed ~a (~a bytes) -> ~a~n" in-path n out-path)))
+
+;; Resolve <cross-prefix>/lib/csv*/<machine> — Chez's per-machine static lib dir.
+(define (find-cross-csv-dir cross-prefix machine)
+  (let ([lib (format "~a/lib" cross-prefix)])
+    (unless (file-directory? lib)
+      (error 'find-cross-csv-dir "missing lib dir" lib))
+    (let* ([entries (directory-list lib)]
+           [csvs    (filter (lambda (e)
+                              (and (>= (string-length e) 3)
+                                   (string=? (substring e 0 3) "csv")))
+                            entries)])
+      (when (null? csvs)
+        (error 'find-cross-csv-dir "no csv* under" lib))
+      (format "~a/~a/~a" lib (car csvs) machine))))
+
+;; POSIX symbols jdns calls via foreign-procedure. Resolved by the libc
+;; statically/dynamically linked into the final binary.
+(define jdns-posix-symbols
+  '("socket" "bind" "close" "recvfrom" "sendto" "setsockopt"
+    "htons" "ntohs" "inet_pton" "inet_ntop"
+    "setuid" "setgid" "chdir" "chroot"))
+
+;; POSIX symbols that are macros or have varargs in libc — must go through
+;; a thin C wrapper so foreign-procedure's int signature matches.
+;; (htons/ntohs are macros on Linux and some BSDs; provide explicit wrappers.)
+(define jdns-posix-wrapped-symbols '("htons" "ntohs"))
+
+;; Wasm-sandbox symbols pulled from (std wasm sandbox). The Scheme side
+;; guards every call with wasm-sandbox-available?, so weak stubs that
+;; return 0 are enough — jdns falls back to in-Scheme parsing.
+(define jdns-wasm-weak-symbols
+  '("jerboa_wasm_module_new" "jerboa_wasm_module_free"
+    "jerboa_wasm_instance_new" "jerboa_wasm_instance_new_hosted"
+    "jerboa_wasm_instance_free" "jerboa_wasm_call"
+    "jerboa_wasm_memory_read" "jerboa_wasm_memory_write" "jerboa_wasm_memory_size"
+    "jerboa_wasm_add_fuel" "jerboa_wasm_fuel_remaining" "jerboa_wasm_get_log"
+    "jerboa_sm_module_new" "jerboa_sm_module_free"
+    "jerboa_sm_instance_new" "jerboa_sm_instance_new_hosted"
+    "jerboa_sm_instance_free" "jerboa_sm_call"
+    "jerboa_sm_add_fuel" "jerboa_sm_get_log"
+    "jerboa_last_error"))
+
+;; Emit main.c. Parameters:
+;;   out-port            — file output port to write to
+;;   binary-name         — used in messages and tempfile path
+;;   platform-tag        — keyword: 'macos | 'linux-static | 'linux-arm64-static | 'freebsd
+;;   extra-posix-symbols — list of additional POSIX symbols (e.g. cap_rights_limit)
+;;
+;; Generates: includes, dlopen stubs (static targets only), wrappers,
+;; weak stubs, register_ffi_symbols(), main() that loads boot files and
+;; runs the embedded program via Sscheme_program.
+(define (emit-jdns-main-c out binary-name platform-tag extra-posix-symbols)
+  (define static? (memq platform-tag '(linux-static linux-arm64-static)))
+  (define freebsd? (eq? platform-tag 'freebsd))
+  (define macos? (eq? platform-tag 'macos))
+
+  (fprintf out "/* Generated by build-jdns-*.ss for ~a — do not edit. */\n"
+           binary-name)
+  (when static? (display "#define _GNU_SOURCE\n" out))
+  (for-each
+    (lambda (h) (fprintf out "#include ~a\n" h))
+    '("<stdlib.h>" "<string.h>" "<stdio.h>" "<unistd.h>"
+      "<sys/types.h>" "<sys/socket.h>" "<sys/stat.h>"
+      "<netinet/in.h>" "<arpa/inet.h>" "<fcntl.h>" "<errno.h>"))
+  (when freebsd? (display "#include <sys/sysctl.h>\n" out))
+  (when (or static? freebsd?) (display "#include <sys/mman.h>\n" out))
+  (display "#include \"scheme.h\"\n" out)
+  (display "#include \"petite_boot.h\"\n" out)
+  (display "#include \"scheme_boot.h\"\n" out)
+  (display "#include \"program_boot.h\"\n\n" out)
+
+  ;; dlopen stubs for fully-static binaries (musl's dlopen always fails in
+  ;; a -static link, but jerboa-dns calls (load-shared-object "libc.so") at
+  ;; init; the stub returns a non-NULL handle so the call succeeds and
+  ;; foreign-procedure routes through Sforeign_symbol).
+  (when static?
+    (display "/* dlopen stubs for static linking */\n" out)
+    (display "void *dlopen(const char *f, int flags) { (void)flags; return f ? NULL : (void*)1; }\n" out)
+    (display "void *dlsym(void *h, const char *s)    { (void)h; (void)s; return NULL; }\n" out)
+    (display "int   dlclose(void *h)                  { (void)h; return 0; }\n" out)
+    (display "static char dlerror_msg[] = \"static binary: dlopen of named libraries is stubbed\";\n" out)
+    (display "char *dlerror(void)                     { return dlerror_msg; }\n\n" out))
+
+  ;; FreeBSD: libc uses __error, glibc uses __errno_location. Provide a
+  ;; wrapper and register it under both names so Scheme code that uses
+  ;; either spelling resolves.
+  (when freebsd?
+    (display "/* FreeBSD errno compat */\n" out)
+    (display "static int *freebsd_errno_location(void) { return &errno; }\n\n" out))
+
+  ;; Wrappers for POSIX macros / FreeBSD-specific symbols.
+  (display "/* POSIX wrappers (htons/ntohs are macros on some libcs) */\n" out)
+  (display "static unsigned short wrap_htons(unsigned short x) { return htons(x); }\n" out)
+  (display "static unsigned short wrap_ntohs(unsigned short x) { return ntohs(x); }\n" out)
+  (when freebsd?
+    (display "\n/* FreeBSD Capsicum cap_rights_limit — declared in <sys/capsicum.h> on FreeBSD;\n" out)
+    (display " * we forward-declare here to avoid pulling that header into the cross build. */\n" out)
+    (display "extern int cap_rights_limit(int, const void *);\n" out))
+  (newline out)
+
+  ;; Weak stubs for wasm symbols not linked into this build. __attribute__((weak))
+  ;; lets the link succeed; at runtime Scheme guards wasm-sandbox-available? so
+  ;; these stubs are never actually called.
+  (display "/* Weak stubs for wasm sandbox symbols (libjerboa_native not linked) */\n" out)
+  (for-each
+    (lambda (n)
+      (fprintf out
+        "__attribute__((weak)) long ~a() { fprintf(stderr, \"[~a-weak] ~a\\n\"); fflush(stderr); return 0; }\n"
+        n binary-name n))
+    jdns-wasm-weak-symbols)
+  (newline out)
+
+  ;; Sbuild_heap callback — register every FFI symbol Scheme can dlsym at runtime.
+  (display "static void register_ffi_symbols(void) {\n" out)
+  (for-each
+    (lambda (n)
+      (cond
+        [(member n jdns-posix-wrapped-symbols)
+         (fprintf out "    Sforeign_symbol(\"~a\", (void*)wrap_~a);\n" n n)]
+        [else
+         (fprintf out "    Sforeign_symbol(\"~a\", (void*)~a);\n" n n)]))
+    jdns-posix-symbols)
+  (for-each
+    (lambda (n)
+      (fprintf out "    Sforeign_symbol(\"~a\", (void*)~a);\n" n n))
+    extra-posix-symbols)
+  (when freebsd?
+    (display "    Sforeign_symbol(\"__error\",          (void*)freebsd_errno_location);\n" out)
+    (display "    Sforeign_symbol(\"__errno_location\", (void*)freebsd_errno_location);\n" out))
+  (for-each
+    (lambda (n)
+      (fprintf out "    Sforeign_symbol(\"~a\", (void*)~a);\n" n n))
+    jdns-wasm-weak-symbols)
+  (display "}\n\n" out)
+
+  ;; main() — write embedded program to tempfile, then Sscheme_program.
+  (display "static const char *write_program_tempfile(void) {\n" out)
+  (fprintf out "    static char path[] = \"/tmp/~a-prog-XXXXXX\";\n" binary-name)
+  (display "    int fd = mkstemp(path);\n" out)
+  (display "    if (fd < 0) { perror(\"mkstemp\"); exit(1); }\n" out)
+  (display "    if (write(fd, program_boot_data, program_boot_size) != (ssize_t)program_boot_size) {\n" out)
+  (display "        perror(\"write\"); close(fd); unlink(path); exit(1);\n" out)
+  (display "    }\n" out)
+  (display "    close(fd);\n" out)
+  (display "    return path;\n" out)
+  (display "}\n\n" out)
+
+  (display "int main(int argc, const char *argv[]) {\n" out)
+  (display "    Sscheme_init(NULL);\n" out)
+  (display "    Sregister_boot_file_bytes(\"petite\", (void *)petite_boot_data, petite_boot_size);\n" out)
+  (display "    Sregister_boot_file_bytes(\"scheme\", (void *)scheme_boot_data, scheme_boot_size);\n" out)
+  (display "    Sbuild_heap(NULL, register_ffi_symbols);\n" out)
+  (display "    const char *prog_path = write_program_tempfile();\n" out)
+  (display "    int status = Sscheme_program(prog_path, argc, argv);\n" out)
+  (display "    unlink(prog_path);\n" out)
+  (display "    Sscheme_deinit();\n" out)
+  (display "    return status;\n" out)
+  (display "}\n" out))
+
+;; Generic per-binary pipeline: compile-program → compile-whole-program →
+;; embed boot + program → generate main.c → invoke cross-cc.
+;; Each target script calls (build-one-binary ...) twice — once for jdns,
+;; once for jdns-data.
+(define (build-one-binary entry-script
+                          binary-name
+                          csv-dir
+                          platform-tag
+                          extra-posix-symbols
+                          link-cmd-fn)
+  (let* ([wpo-output (string-append binary-name ".wp.so")]
+         ;; entry "bin/jdns.ss" → wpo at "bin/jdns.wpo"
+         [base (let ([n (string-length entry-script)])
+                 (if (and (>= n 3)
+                          (string=? (substring entry-script (- n 3) n) ".ss"))
+                   (substring entry-script 0 (- n 3))
+                   entry-script))]
+         [wpo-input (string-append base ".wpo")]
+         [main-c (string-append binary-name "-main.c")])
+    (printf "==> [~a] compile-program ~a~n" binary-name entry-script)
+    (compile-program entry-script)
+    (printf "==> [~a] compile-whole-program ~a -> ~a~n"
+            binary-name wpo-input wpo-output)
+    (compile-whole-program wpo-input wpo-output #t)
+    (printf "==> [~a] embed boot files + program~n" binary-name)
+    (embed-as-c-array (format "~a/petite.boot" csv-dir) "petite_boot"  "petite_boot.h")
+    (embed-as-c-array (format "~a/scheme.boot" csv-dir) "scheme_boot"  "scheme_boot.h")
+    (embed-as-c-array wpo-output                        "program_boot" "program_boot.h")
+    (printf "==> [~a] generate ~a~n" binary-name main-c)
+    (call-with-port (open-file-output-port main-c
+                       (file-options no-fail) (buffer-mode block) (native-transcoder))
+      (lambda (out) (emit-jdns-main-c out binary-name platform-tag extra-posix-symbols)))
+    (printf "==> [~a] link~n" binary-name)
+    (let ([cmd (link-cmd-fn binary-name main-c csv-dir)])
+      (printf "    ~a~n" cmd)
+      (let ([rc (system cmd)])
+        (unless (zero? rc)
+          (error 'build-one-binary "link failed" binary-name rc))))
+    (printf "=== ~a built ===~n" binary-name)
+    (system (format "ls -lh ~a" binary-name))
+    (system (format "file ~a" binary-name))
+    (newline)))
diff --git a/static/build-jdns-freebsd.ss b/static/build-jdns-freebsd.ss
new file mode 100644
index 0000000..5c6ef4c
--- /dev/null
+++ b/static/build-jdns-freebsd.ss
@@ -0,0 +1,73 @@
+#!chezscheme
+;;; build-jdns-freebsd.ss — Cross-compile jdns + jdns-data from macOS arm64
+;;; to FreeBSD 14 amd64 (dynamic).
+;;;
+;;; Usage:
+;;;   JERBOA_HOME=/Users/user/mine/jerboa \
+;;;     scheme --libdirs "lib:$JERBOA_HOME/lib" \
+;;;            --script static/build-jdns-freebsd.ss
+;;;
+;;; Uses:
+;;;   $JERBOA_HOME/.chez-cross-ta6fb/        — cross-built Chez install
+;;;   $JERBOA_HOME/build/chez/xc-ta6fb/s/xpatch — host compiler → ta6fb mode
+;;;   x86_64-unknown-freebsd14-clang         — wrapper around macOS clang+lld
+;;;
+;;; Dynamic-linked: FreeBSD libc uses symbol versioning that prevents
+;;; useful -static against libc.a; the canonical model is dynamic against
+;;; libc.so.7 / libm / libthr / libutil from the target system.
+;;;
+;;; Produces (in repo root):
+;;;   jdns-freebsd-amd64
+;;;   jdns-data-freebsd-amd64
+
+(import (chezscheme))
+
+(load "static/build-common.ss")
+
+(define jerboa-home  (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa"))
+(define cross-prefix (format "~a/.chez-cross-ta6fb" jerboa-home))
+(define xpatch       (format "~a/build/chez/xc-ta6fb/s/xpatch" jerboa-home))
+(define cross-cc     (or (getenv "CROSS_CC") "x86_64-unknown-freebsd14-clang"))
+(define csv-dir      (find-cross-csv-dir cross-prefix "ta6fb"))
+
+(define (require-file p)
+  (unless (file-exists? p)
+    (error 'build-jdns-freebsd "missing file" p)))
+
+(require-file xpatch)
+(require-file (format "~a/libkernel.a" csv-dir))
+(require-file (format "~a/scheme.h"    csv-dir))
+(require-file (format "~a/petite.boot" csv-dir))
+(require-file (format "~a/scheme.boot" csv-dir))
+(require-file "bin/jdns.ss")
+(require-file "bin/jdns-data.ss")
+
+(printf "==> build-jdns-freebsd (cross to ta6fb, dynamic)~n")
+(printf "    JERBOA_HOME: ~a~n" jerboa-home)
+(printf "    csv-dir:     ~a~n" csv-dir)
+(printf "    xpatch:      ~a~n" xpatch)
+(printf "    cross-cc:    ~a~n" cross-cc)
+(printf "~n")
+
+(define orig-libdirs (library-directories))
+(printf "==> [1] loading xpatch~n")
+(load xpatch)
+(library-directories orig-libdirs)
+
+(compile-imported-libraries #t)
+(generate-wpo-files #t)
+
+;; FreeBSD link: dynamic, no -ldl (dlopen is in libc), -lthr for pthread,
+;; -lutil for libutil — matches jerboa-shell's build-jsh-freebsd-cross.
+;; -Wl,--export-dynamic so RTLD_DEFAULT sees Sforeign_symbol registrations.
+(define (link-cmd binary main-c csv)
+  (format
+   "~a -O2 -Wl,--export-dynamic -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -lpthread -lutil"
+   cross-cc csv binary main-c csv csv csv))
+
+;; cap_rights_limit is a FreeBSD-only Capsicum entry point.
+(build-one-binary "bin/jdns.ss"      "jdns-freebsd-amd64"      csv-dir 'freebsd '("cap_rights_limit") link-cmd)
+(build-one-binary "bin/jdns-data.ss" "jdns-data-freebsd-amd64" csv-dir 'freebsd '("cap_rights_limit") link-cmd)
+
+(printf "=== Build complete ===~n")
+(system "ls -lh jdns-freebsd-amd64 jdns-data-freebsd-amd64")
diff --git a/static/build-jdns-linux-arm64.ss b/static/build-jdns-linux-arm64.ss
new file mode 100644
index 0000000..bbff30a
--- /dev/null
+++ b/static/build-jdns-linux-arm64.ss
@@ -0,0 +1,65 @@
+#!chezscheme
+;;; build-jdns-linux-arm64.ss — Cross-compile jdns + jdns-data from macOS arm64
+;;; to Linux aarch64 musl (fully static).
+;;;
+;;; Usage:
+;;;   JERBOA_HOME=/Users/user/mine/jerboa \
+;;;     scheme --libdirs "lib:$JERBOA_HOME/lib" \
+;;;            --script static/build-jdns-linux-arm64.ss
+;;;
+;;; Uses:
+;;;   $JERBOA_HOME/.chez-cross-tarm64le/        — cross-built Chez install
+;;;   $JERBOA_HOME/build/chez/xc-tarm64le/s/xpatch — host compiler → tarm64le mode
+;;;   aarch64-linux-musl-gcc                    — C compile + static link
+;;;
+;;; Produces (in repo root):
+;;;   jdns-linux-arm64
+;;;   jdns-data-linux-arm64
+
+(import (chezscheme))
+
+(load "static/build-common.ss")
+
+(define jerboa-home  (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa"))
+(define cross-prefix (format "~a/.chez-cross-tarm64le" jerboa-home))
+(define xpatch       (format "~a/build/chez/xc-tarm64le/s/xpatch" jerboa-home))
+(define cross-cc     (or (getenv "CROSS_CC") "aarch64-linux-musl-gcc"))
+(define csv-dir      (find-cross-csv-dir cross-prefix "tarm64le"))
+
+(define (require-file p)
+  (unless (file-exists? p)
+    (error 'build-jdns-linux-arm64 "missing file" p)))
+
+(require-file xpatch)
+(require-file (format "~a/libkernel.a" csv-dir))
+(require-file (format "~a/scheme.h"    csv-dir))
+(require-file (format "~a/petite.boot" csv-dir))
+(require-file (format "~a/scheme.boot" csv-dir))
+(require-file "bin/jdns.ss")
+(require-file "bin/jdns-data.ss")
+
+(printf "==> build-jdns-linux-arm64 (cross to tarm64le, static musl)~n")
+(printf "    JERBOA_HOME: ~a~n" jerboa-home)
+(printf "    csv-dir:     ~a~n" csv-dir)
+(printf "    xpatch:      ~a~n" xpatch)
+(printf "    cross-cc:    ~a~n" cross-cc)
+(printf "~n")
+
+(define orig-libdirs (library-directories))
+(printf "==> [1] loading xpatch~n")
+(load xpatch)
+(library-directories orig-libdirs)
+
+(compile-imported-libraries #t)
+(generate-wpo-files #t)
+
+(define (link-cmd binary main-c csv)
+  (format
+   "~a -O2 -static -Wl,--export-dynamic -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -ldl -lpthread"
+   cross-cc csv binary main-c csv csv csv))
+
+(build-one-binary "bin/jdns.ss"      "jdns-linux-arm64"      csv-dir 'linux-arm64-static '() link-cmd)
+(build-one-binary "bin/jdns-data.ss" "jdns-data-linux-arm64" csv-dir 'linux-arm64-static '() link-cmd)
+
+(printf "=== Build complete ===~n")
+(system "ls -lh jdns-linux-arm64 jdns-data-linux-arm64")
diff --git a/static/build-jdns-linux.ss b/static/build-jdns-linux.ss
new file mode 100644
index 0000000..db99210
--- /dev/null
+++ b/static/build-jdns-linux.ss
@@ -0,0 +1,69 @@
+#!chezscheme
+;;; build-jdns-linux.ss — Cross-compile jdns + jdns-data from macOS arm64
+;;; to Linux x86_64 musl (fully static).
+;;;
+;;; Usage:
+;;;   JERBOA_HOME=/Users/user/mine/jerboa \
+;;;     scheme --libdirs "lib:$JERBOA_HOME/lib" \
+;;;            --script static/build-jdns-linux.ss
+;;;
+;;; Uses:
+;;;   $JERBOA_HOME/.chez-cross-ta6le/        — cross-built Chez install
+;;;   $JERBOA_HOME/build/chez/xc-ta6le/s/xpatch — host compiler → ta6le mode
+;;;   x86_64-linux-musl-gcc                  — C compile + static link
+;;;
+;;; Produces (in repo root):
+;;;   jdns-linux-amd64
+;;;   jdns-data-linux-amd64
+
+(import (chezscheme))
+
+(load "static/build-common.ss")
+
+(define jerboa-home  (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa"))
+(define cross-prefix (format "~a/.chez-cross-ta6le" jerboa-home))
+(define xpatch       (format "~a/build/chez/xc-ta6le/s/xpatch" jerboa-home))
+(define cross-cc     (or (getenv "CROSS_CC") "x86_64-linux-musl-gcc"))
+(define csv-dir      (find-cross-csv-dir cross-prefix "ta6le"))
+
+(define (require-file p)
+  (unless (file-exists? p)
+    (error 'build-jdns-linux "missing file" p)))
+
+(require-file xpatch)
+(require-file (format "~a/libkernel.a" csv-dir))
+(require-file (format "~a/scheme.h"    csv-dir))
+(require-file (format "~a/petite.boot" csv-dir))
+(require-file (format "~a/scheme.boot" csv-dir))
+(require-file "bin/jdns.ss")
+(require-file "bin/jdns-data.ss")
+
+(printf "==> build-jdns-linux (cross to ta6le, static musl)~n")
+(printf "    JERBOA_HOME: ~a~n" jerboa-home)
+(printf "    csv-dir:     ~a~n" csv-dir)
+(printf "    xpatch:      ~a~n" xpatch)
+(printf "    cross-cc:    ~a~n" cross-cc)
+(printf "~n")
+
+;; Stage 1: load xpatch — switches the host compiler into ta6le emit mode.
+;; xpatch's load form clobbers library-directories; restore it after.
+(define orig-libdirs (library-directories))
+(printf "==> [1] loading xpatch~n")
+(load xpatch)
+(library-directories orig-libdirs)
+
+(compile-imported-libraries #t)
+(generate-wpo-files #t)
+
+;; Static link with --export-dynamic so Sforeign_symbol-registered functions
+;; remain visible to Chez's dlsym(RTLD_DEFAULT, ...) lookups.
+(define (link-cmd binary main-c csv)
+  (format
+   "~a -O2 -static -Wl,--export-dynamic -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -ldl -lpthread"
+   cross-cc csv binary main-c csv csv csv))
+
+(build-one-binary "bin/jdns.ss"      "jdns-linux-amd64"      csv-dir 'linux-static '() link-cmd)
+(build-one-binary "bin/jdns-data.ss" "jdns-data-linux-amd64" csv-dir 'linux-static '() link-cmd)
+
+(printf "=== Build complete ===~n")
+(system "ls -lh jdns-linux-amd64 jdns-data-linux-amd64")
diff --git a/static/build-jdns-macos.ss b/static/build-jdns-macos.ss
new file mode 100644
index 0000000..a981537
--- /dev/null
+++ b/static/build-jdns-macos.ss
@@ -0,0 +1,57 @@
+#!chezscheme
+;;; build-jdns-macos.ss — Native macOS arm64 build of jdns + jdns-data.
+;;;
+;;; Usage:
+;;;   JERBOA_HOME=/Users/user/mine/jerboa \
+;;;     scheme --libdirs "lib:$JERBOA_HOME/lib" \
+;;;            --script static/build-jdns-macos.ss
+;;;
+;;; Native build — no xpatch. The current host's $JERBOA_HOME/.chez is a
+;;; tarm64osx install built by jerboa's Makefile; we link against its
+;;; static libkernel.a + lib{z,lz4}.a.
+;;;
+;;; Produces (in repo root):
+;;;   jdns-macos-arm64
+;;;   jdns-data-macos-arm64
+
+(import (chezscheme))
+
+(load "static/build-common.ss")
+
+(define jerboa-home (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa"))
+(define chez-prefix (format "~a/.chez" jerboa-home))
+(define csv-dir     (find-cross-csv-dir chez-prefix "tarm64osx"))
+(define cc          (or (getenv "CC") "cc"))
+
+(define (require-file p)
+  (unless (file-exists? p)
+    (error 'build-jdns-macos "missing file" p)))
+
+(require-file (format "~a/libkernel.a" csv-dir))
+(require-file (format "~a/scheme.h"    csv-dir))
+(require-file (format "~a/petite.boot" csv-dir))
+(require-file (format "~a/scheme.boot" csv-dir))
+(require-file "bin/jdns.ss")
+(require-file "bin/jdns-data.ss")
+
+(printf "==> build-jdns-macos~n")
+(printf "    JERBOA_HOME: ~a~n" jerboa-home)
+(printf "    csv-dir:     ~a~n" csv-dir)
+(printf "    cc:          ~a~n" cc)
+(printf "~n")
+
+(compile-imported-libraries #t)
+(generate-wpo-files #t)
+
+;; Native macOS link: dynamic-link libSystem (libc/libm/pthread bundled).
+;; -Wl,-export_dynamic so dlsym(RTLD_DEFAULT, ...) sees the weak stubs.
+(define (link-cmd binary main-c csv)
+  (format
+   "~a -O2 -I~a -Wl,-export_dynamic -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -lpthread -liconv -lncurses"
+   cc csv binary main-c csv csv csv))
+
+(build-one-binary "bin/jdns.ss"      "jdns-macos-arm64"      csv-dir 'macos '() link-cmd)
+(build-one-binary "bin/jdns-data.ss" "jdns-data-macos-arm64" csv-dir 'macos '() link-cmd)
+
+(printf "=== Build complete ===~n")
+(system "ls -lh jdns-macos-arm64 jdns-data-macos-arm64")
diff --git a/static/build-static.sh b/static/build-static.sh
deleted file mode 100755
index 54cb073..0000000
--- a/static/build-static.sh
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/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
deleted file mode 100644
index cc5cda2..0000000
--- a/static/main.c
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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;
-}